Skip to content

fix: correct JSON serialization and error handling in VaultHttp#10

Open
spbsoluble wants to merge 6 commits intorelease-1.0from
fix/vault-http-json-serialization
Open

fix: correct JSON serialization and error handling in VaultHttp#10
spbsoluble wants to merge 6 commits intorelease-1.0from
fix/vault-http-json-serialization

Conversation

@spbsoluble
Copy link
Copy Markdown
Contributor

Summary

Fixes two bugs in VaultHttp that together prevented certificate enrollment from succeeding when the gateway called into Vault/OpenBao.

Bug 1: AddJsonBody(string) double-encodes pre-serialized JSON → HTTP 400

PostAsync serialized the request body to a JSON string with JsonSerializer.Serialize, then passed that string to request.AddJsonBody(). In RestSharp ≥ 106, AddJsonBody re-serializes whatever object it receives. When the argument is already a string, RestSharp encodes it as a JSON string literal — wrapping the content in quotes and escaping the inner characters.

Vault/OpenBao received "{\"csr\":\"...\"}" (a JSON-encoded string) instead of {"csr":"..."} (a JSON object) and returned HTTP 400 "error parsing JSON".

Fix: Replaced request.AddJsonBody(serializedParams) with request.AddStringBody(serializedParams, ContentType.Json). AddStringBody treats its first argument as the raw body and does not re-serialize it.

Bug 2: ThrowOnAnyError = true made the BadRequest error-parsing block dead code

RestClientOptions was constructed with ThrowOnAnyError = true. This causes RestSharp to throw an exception on any non-2xx response before returning to the caller. PostAsync had explicit handling for HttpStatusCode.BadRequest that deserialized the Vault error list and produced a descriptive exception message — but that block was unreachable because RestSharp always threw first, and the actual Vault error body was swallowed.

Fix: Removed ThrowOnAnyError = true from RestClientOptions. Added response.ThrowIfError() after the explicit BadRequest handler so that non-2xx responses that are not BadRequest still surface as exceptions, while BadRequest responses go through the Vault error-body parsing path.

Multi-target: dropped EOL net6.0, added net8.0 and net10.0

The project previously targeted net6.0, which reached end-of-life in November 2024. Updated to multi-target net8.0 (LTS) and net10.0 (current). AppendTargetFrameworkToOutputPath=False is preserved from the original project configuration.

How to reproduce Bug 1

# Double-encoded body (what the bug sent) — returns 400 "error parsing JSON"
curl -sk -X POST https://<vault>/v1/<mount>/sign/<role> \
  -H "X-Vault-Token: $TOKEN" \
  -d '"{"csr":"...","common_name":"...","format":"pem_bundle"}"'

# Correct JSON object — returns 200
curl -sk -X POST https://<vault>/v1/<mount>/sign/<role> \
  -H "X-Vault-Token: $TOKEN" \
  -d '{"csr":"...","common_name":"...","format":"pem_bundle"}'

Test plan

  • Build the project targeting net8.0 and net10.0 — both should compile cleanly
  • Deploy to a gateway instance backed by a Vault/OpenBao PKI engine
  • Trigger PFX enrollment from Keyfactor Command through the AnyCA REST Gateway — should return HTTP 200 with a valid certificate
  • Confirm that a Vault HTTP 400 response (e.g., invalid CSR) surfaces as an exception with the Vault error message in the gateway logs rather than a generic RestSharp exception

fiddlermikey and others added 4 commits January 8, 2025 09:53
AddJsonBody(string) in RestSharp re-serializes the argument, wrapping a
pre-serialized JSON string in quotes and escaping it. Vault receives a
JSON string literal instead of a JSON object and returns HTTP 400.
Fixed by using AddStringBody(string, ContentType.Json) which sends the
raw body without re-encoding.

ThrowOnAnyError = true caused RestSharp to throw before the explicit
BadRequest handler ran, hiding the Vault error body from logs. Removed
ThrowOnAnyError and added response.ThrowIfError() after the BadRequest
block so all other non-2xx responses still surface as exceptions.

Also drops the EOL net6.0 target; project now multi-targets net8.0 and net10.0.
…nd ValidateProductInfo

Direct Dictionary.get_Item calls throw KeyNotFoundException when the
gateway does not populate all parameter keys before calling validation
(observed on PUT/POST config/configuration). Replace with TryGetValue
throughout.

Also relaxes the ValidateProductInfo RoleName check: RoleName is
optional since the Enroll path already falls back to ProductID when
RoleName is absent, so the validator no longer rejects configs that
omit it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants