Verifying an audit-trail reason signature
An audit-trail response contains a readable JSON array, a detached RSA signature, the signer certificate chain, and an independent RFC 3161 timestamp. Verification establishes that the signed content is unchanged, that the signature was created by the stated certificate, and that the signature existed no later than the trusted timestamp.
1. Signed response structure
The relevant part of a response has the following shape:
{
"reason": [ ... ],
"reasonSignature": {
"protected": {
"version": 2,
"alg": "RS256",
"canonicalization": "RFC8785",
"signMode": "SHA256withRSA",
"signedAt": "2026-09-06T13:24:47.158548463Z",
"certSha256": "Wj_Bn4-_TYGNFIE4OeaxOXd-924NKzbNRe0m6Iv2U-I",
"issuer": "Betrust",
"environment": "test",
"keyAlias": "betrust-aatl-qualified-2029",
"documentation": "https://www.oksign.be/en/blog/verify-signature-json/",
"critical": [
"version", "alg", "canonicalization",
"signedAt", "certSha256"
],
"rootissuerurl": "http://trust.quovadisglobal.com/qvrca1g3.crt"
},
"x5c": [
"base64-end-certificate",
"base64-intermediate-certificate"
],
"signature": "base64url-rsa-signature",
"timestamp": {
"type": "RFC3161",
"target": "signature",
"hashAlgorithm": "SHA-256",
"token": "base64-rfc3161-token"
}
}
} | Field | Meaning |
|---|---|
reason | The JSON array whose integrity is protected. |
protected | Metadata included in the JSON signature calculation. |
protected.signMode | Implementation-level name for the signature operation. For this example it is SHA256withRSA, consistent with alg = RS256. |
protected.issuer | Informational label for the signing organization. |
protected.environment | Informational label for the deployment environment that produced the signature. |
protected.keyAlias | Informational identifier for the signing-key configuration. |
protected.documentation | Informational link to documentation for this signature format or service. |
protected.rootissuerurl | Informational certificate-source hint. It must not be fetched or trusted automatically. |
x5c | End certificate first, followed by available intermediate CA certificates. These certificates are evidence, not trust anchors. |
signature | Unpadded Base64url encoding of the RSA signature. |
timestamp.token | Base64 encoding of a DER RFC 3161 timestamp token covering the decoded signature bytes. |
2. Required trust configuration
The verifier needs two independently configured sets of trust anchors:
- Signer trust: root CA certificates trusted to issue the audit-trail signing certificate.
- TSA trust: root CA certificates trusted to issue the Timestamp Authority certificate.
Configured Timestamp Authorities
The service normally requests its RFC 3161 timestamp from the primary Timestamp Authority. If that service is unavailable, it may use the backup Timestamp Authority.
| Role | RFC 3161 endpoint |
|---|---|
| Primary | https://tsa.firmaprofesional.com/tsaec |
| Backup | https://tsa.esigna.es/TSA/ |
The RFC 3161 token cryptographically identifies the TSA that actually issued it through the embedded TSA signer certificate. The endpoint used to request the token is therefore operational information and is not a trust decision. The verifier must accept the token only when the TSA signer certificate builds to one of the verifier's independently configured TSA trust anchors.
API documentation may link to each provider's official certificate repository to help administrators obtain the applicable root and intermediate certificates. Such links are onboarding and maintenance aids only. Administrators must authenticate the source and verify the certificate fingerprints through an independent approved channel before adding a root to the TSA trust store. Verification software must not automatically download or trust a root certificate from a URL in the response, the timestamp token, or a certificate extension.
3. Verification procedure
-
Parse and validate the envelope
Parse the response as JSON and obtain
reasonandreasonSignature. Reject malformed input, unsupported versions, unsupported algorithms, missing required fields, duplicate JSON property names, and unknown fields declared critical.Preserve every member of
protectedexactly as received. All protected members, including non-critical informational fields, are covered by the signature. Thecriticalarray tells a verifier which protected fields it must understand; fields absent from that array must still be retained during canonicalization.For format version 2, require:
alg = RS256canonicalization = RFC8785- If
signModeis present, it is consistent withalg(for this example,SHA256withRSA). timestamp.type = RFC3161timestamp.target = signaturetimestamp.hashAlgorithm = SHA-256
-
Decode the signer certificates
Decode each
x5centry using standard Base64 and parse it as an X.509 certificate. The first certificate is the end-entity signer. Check that its public key is RSA and that its Key Usage permitsdigitalSignatureorcontentCommitment. Apply the Extended Key Usage and certificate-policy requirements defined by the relying party. -
Bind the end certificate to the protected metadata
Calculate SHA-256 over the complete DER encoding of the first certificate. Compare the result, using a constant-time comparison, with the unpadded Base64url-decoded
protected.certSha256. -
Reconstruct the signed JSON value
Create a temporary JSON object with exactly these two members:
{ "protected": <received protected object>, "reason": <received reason array> }Do not add application fields to this temporary object. Do not remove or reinterpret protected fields before canonicalization.
Values such asissuer,environment,keyAlias,documentation, androotissuerurlare authenticated only as values supplied by the signer. They must not be used as trust anchors or as authority to download certificates or change verifier policy. -
Canonicalize and verify the JSON signature
Canonicalize the temporary object according to RFC 8785 (JCS), encode the canonical result as UTF-8, and verify the decoded signature using
SHA256withRSA. TheRS256identifier means RSASSA-PKCS1-v1_5 with SHA-256.Object-property order and insignificant whitespace do not affect JCS output. Array order, property presence, JSON types, strings, and values remain significant. -
Verify the RFC 3161 timestamp token
Decode
timestamp.tokenusing standard Base64 and parse the resulting DER value as an RFC 3161 timestamp token. Then:- Calculate SHA-256 over the decoded
signaturebytes. - Compare it with the timestamp token's message imprint.
- Verify the timestamp token's CMS signature.
- Check the TSA certificate's timestamping Extended Key Usage.
- Build and validate its certificate path against independently trusted TSA roots approved for the primary or backup provider.
- Apply the configured TSA revocation and certificate-policy rules at the token generation time.
- Reject a generation time unreasonably later than the verifier's clock.
- Calculate SHA-256 over the decoded
-
Validate the JSON signer at the trusted timestamp time
Once the timestamp is trusted, use its generation time as the PKIX validation time for the JSON signer certificate path. Build the path from the end certificate and intermediates in
x5cto an independently configured signer trust anchor. Apply revocation and policy requirements appropriate for historical validation.The protected
signedAtis a signer assertion. It should not be later than the trusted RFC 3161 time beyond the permitted clock tolerance, but it is not itself the trusted source of time.
4. High-level Java invocation
When using the supplied Java implementation, the caller parses the JSON and provides separate PKIX configurations for the signer and the TSA:
JSONObject auditTrail = new JSONObject(receivedJson); JSONArray reason = auditTrail.getJSONArray("reason"); JSONObject reasonSignature =
auditTrail.getJSONObject("reasonSignature"); PKIXParameters signerTrust = createSignerPkixParameters(); PKIXParameters tsaTrust = createTsaPkixParameters(); VerificationResult result = ReasonSignature.verifyReason(
reason,
reasonSignature,
signerTrust,
tsaTrust
); System.out.println("Signer: "
+ result.signerCertificate().getSubjectX500Principal()); System.out.println("Timestamp: " + result.timestampedAt()); System.out.println("TSA: "
+ result.tsaCertificate().getSubjectX500Principal()); A successful return means that all checks implemented by the configured verifier completed successfully. A verification or path-building exception must be treated as a failed verification, not as a warning.
5. What successful verification establishes
reason and protected metadata are unchanged; the
RSA signature matches the validated end certificate; the timestamp token
covers that signature; and a trusted TSA asserts that the signature
existed at the returned generation time.
It does not, by itself, establish:
- that every statement inside the audit trail is factually correct;
- that an embedded or linked root certificate should be trusted;
- that the protected
signedAtis an independently trusted time; - legal non-repudiation without the surrounding identity, policy, key-control, revocation, and evidentiary processes.
6. Operational requirements
- Retain the complete response, including
x5cand the timestamp token. - Retain or archive the certificate and revocation evidence needed for historical validation.
- Place strict size and nesting limits on untrusted JSON, certificates, and timestamp tokens.
- Do not fetch arbitrary certificate URLs from untrusted input without SSRF protections and an explicit allowlist.
- Continue supporting older signature-format versions for historical records, using their original documented verification rules.
- Never log TSA credentials, authorization headers, private keys, or other signing secrets.