Time-Stamping Authority (RFC 3161)¶
secsy-pki can act as an RFC 3161
Time-Stamp Authority (TSA): it answers a TimeStampReq (a hash of some data,
plus an optional nonce and policy) with a signed TimeStampToken that binds the
submitted hash to a trusted time. Tokens are signed by a dedicated HSM-backed
TSA key routed through the key provider — the private key never leaves the
device.
Typical uses: proving a document/artifact existed at a point in time, signing- time attestations for code-signing and long-term signature validation (CAdES / PAdES / XAdES time-stamps), and audit/compliance evidence.
How it works¶
client ── TimeStampReq (application/timestamp-query) ──▶ POST /tsa
│
parse + validate (hash alg, nonce, policy)
│
build TSTInfo (imprint, genTime, serial)
│
sign a CMS SignedData on the HSM
│
client ◀─ TimeStampResp (application/timestamp-reply) ────┘
A token is a CMS SignedData whose encapsulated content is a TSTInfo
(id-ct-TSTInfo). The single SignerInfo carries the ESS
signing-certificate-v2 attribute (RFC 5035) binding the token to the TSA
certificate, so conforming verifiers — including openssl ts -verify — accept
it. The TSA certificate and its issuer chain are embedded only when the request
sets certReq.
Key properties:
- HSM-backed signing. Every token is signed through
crypto.Signer; the TSA private key is non-extractable on a PKCS#11 token. - Nonce echo. When a request carries a nonce it is copied verbatim into the token, defeating replay.
- Message-imprint echo. The exact hash algorithm and digest from the request are re-emitted in the token, so a verifier can confirm the token covers its data.
- Random serials. Each token gets a random 128-bit serial number.
- Rejections are signed-free. A malformed or unacceptable request yields a
token-less
TimeStampRespwith the appropriatePKIFailureInfo(badAlg,badDataFormat,unacceptedPolicy, …), returned with HTTP 200.
The TSA key must be RSA: the CMS SignedData is signed with RSA PKCS#1 v1.5,
which is what the shared CMS builder produces and what maximizes verifier
interop.
1. Provision the TSA key and certificate¶
The secsy-ca tsa-key command generates (or reuses) a dedicated RSA key in the
key provider and issues a TSA certificate under an existing CA. The certificate
carries id-kp-timeStamping as its sole, critical extended key usage
(RFC 3161 §2.3) and digitalSignature key usage.
$ secsy-ca -config config.yaml tsa-key \
-ca my-intermediate \
-label tsa-signer \
-cn "Example TSA" \
-validity-days 1185 \
-out tsa.pem -chain
Provisioned TSA certificate: serial=… key=tsa-signer ca=my-intermediate not_after=…
Flags:
| Flag | Default | Meaning |
|---|---|---|
-ca |
(required) | Issuing CA id or label |
-label |
tsa |
Provider key label of the TSA signing key |
-key-type |
rsa-2048 |
rsa-2048 or rsa-4096 (RSA only) |
-cn / -o |
Time-Stamp Authority |
Subject common name / organization |
-validity-days |
1185 |
Certificate lifetime (capped at the issuing CA's expiry) |
-out |
(stdout) | Where to write the certificate PEM |
-chain |
false |
Append the issuing CA chain to the output |
Re-running with the same -label reuses the existing key and reissues the
certificate (rotate the cert without rotating the key). The command re-parses
and re-validates the issued certificate before writing it, so a bad build fails
loudly at provisioning time.
2. Enable the endpoint¶
Add a tsa block to config.yaml:
tsa:
enabled: true
path: /tsa # URL the endpoint mounts under
key_label: tsa-signer # provider label of the (RSA) TSA key
certificate_file: /etc/secsy/tsa.pem # written by `secsy-ca tsa-key`
ca_label: my-intermediate # issuer chain source (when the file is leaf-only)
policy_oid: 1.3.6.1.4.1.99999.1.1 # your owned TSA policy OID
accuracy_seconds: 1 # genTime accuracy bound (omit block for none)
accuracy_millis: 0
accuracy_micros: 0
ordering: false # assert strict time ordering of tokens
signature_digest: sha256 # CMS signature hash (sha256|sha384|sha512)
accepted_hashes: [sha256, sha384, sha512] # message-imprint algs (sha1 opt-in)
include_tsa_name: false # embed the signer subject as the tsa GeneralName
Notes:
certificate_filemay contain just the TSA leaf (the chain is then loaded fromca_id/ca_label) or the full chain (leaf first).policy_oiddefaults to a built-in example OID (2.999.1.1); set an owned OID in production. If a request names a policy, the TSA asserts it only if it matches, else rejects withunacceptedPolicy.accepted_hashesdefaults to SHA-256/384/512. SHA-1 must be listed explicitly to be accepted.- The endpoint is anonymous and public (like OCSP/CRL). It is subject to the rate limiting & HSM concurrency guard; time-stamping is gated behind the concurrency guard because it signs on the HSM.
3. Request a time-stamp¶
Any RFC 3161 client works. With OpenSSL:
# 1. Build a query over the data (SHA-256, ask for the TSA cert back).
$ openssl ts -query -data document.pdf -sha256 -cert -out request.tsq
# 2. Submit it to the /tsa endpoint.
$ curl -s -H "Content-Type: application/timestamp-query" \
--data-binary @request.tsq \
https://pki.example.com/tsa -o response.tsr
# 3. Inspect the token.
$ openssl ts -reply -in response.tsr -text
Status: Granted.
Policy OID: 1.3.6.1.4.1.99999.1.1
Hash Algorithm: sha256
Serial number: 0x…
Time stamp: … GMT
Accuracy: 0x01 seconds, …
# 4. Verify it against the CA (the chain in tsa.pem or the issuing CA cert).
$ openssl ts -verify -data document.pdf -in response.tsr -CAfile tsa.pem
Verification: OK
Audit & metrics¶
- Every request appends a
tsa.timestampevent to the tamper-evident audit log (actortsa:anonymous):successwith the token serial on grant,deniedwith the failure reason on rejection. - Prometheus counter
secsy_timestamp_requests_total{result}partitions requests intogranted|rejected|error. The HSM concurrency guard'ssecsy_hsm_guard_*metrics also cover the TSA path.
Trusted time source (fail-closed drift detection)¶
By default genTime comes from the host wall clock. A rewound or drifted host
clock would therefore emit correctly-signed but false timestamps. To guard
against that, configure a trusted external time source
(time.source: authenticated NTP/NTS or Roughtime): before signing, the host
clock is cross-checked against it and the TSA returns a timeNotAvailable
rejection instead of a token when the drift exceeds the threshold. It is opt-in
and off by default, so the plain host-clock behavior above is unchanged until you
enable it.
Standards & interop¶
- RFC 3161 (Time-Stamp Protocol) and RFC 5816/5035 (ESS
signing-certificate-v2). - Transport per RFC 3161 §3.4:
POST /tsawithapplication/timestamp-queryrequest andapplication/timestamp-replyresponse. - Verified against
openssl ts -verifyfor both software- and SoftHSM-signed tokens (seeinternal/tsatests).