A code signature stops verifying when the signing certificate expires, unless
the signature carries a trusted timestamp proving it was made while the
certificate was valid. An Intermediate or Issuing node can serve those
timestamps as an RFC 3161 time-stamp authority (TSA), for signtool /tr,
osslsigncode -ts, jarsigner -tsa, cosign --timestamp-server-url and
openssl ts.
Scope: HTTP POST of
application/timestamp-queryto the listener's root path, answered withapplication/timestamp-reply. No other RFC 3161 transport (TCP, e-mail, file) is offered, and no request extension is accepted.
| Field | Value |
|---|---|
| Message imprint | Echoed from the request. SHA-256, SHA-384 and SHA-512 are accepted; SHA-1, MD5 and anything else get badAlg |
| Policy | pki.tsa.policy_oid. A request asking for another policy gets unacceptedPolicy |
| Serial number | 159 random bits, unique per token |
genTime |
UTC, to the millisecond |
| Accuracy | pki.tsa.accuracy_ms (1000 by default) |
| Ordering | Not claimed |
| Nonce | Echoed when the request has one |
| Signer | The TSA certificate, named in a signing-certificate-v2 attribute (RFC 5816) by its SHA-256 hash, issuer and serial. It is carried in the token only when the request sets certReq |
The CA key never signs a token. The node issues a separate TSA certificate
from its CA, with key usage digitalSignature and one extended key usage,
id-kp-timeStamping, marked critical as RFC 3161 requires. Its key has the CA
key's algorithm and lives where the CA key does: in the TPM on a TPM node, in
software (on the encrypted state partition) otherwise. The TSA
certificate is listed by cryptosctl ca list-issued under the profile tsa,
so it can be revoked like any other certificate, and with
pki.revocation_base_url set it carries the CRL, OCSP and caIssuers pointers.
The TSA is off unless the pki.tsa block is present, and it stays off until
you set a policy OID: there is no default, so no two deployments share a policy
by accident. A Root refuses enabled: true.
pki:
tsa:
http_port: 0 # 0 is 318
policy_oid: "1.3.6.1.4.1.32473.1.1" # your own arc; see below
accuracy_ms: 1000 # 0 is 1000, at most 60000
rate_limit:
requests_per_minute: 60 # 0 is 60
burst: 60 # 0 is requests_per_minute
allowed_networks: [10.0.0.0/8, "2001:db8::/32"] # empty answers anyone
certificate:
validity_days: 365 # 0 is 365, the most
rotation_overlap_days: 30 # 0 is 30
max_sync_age_seconds: 3600 # 0 is 3600, at most 86400
max_drift_ppm: 100 # 0 is 100, at most 500
max_clock_error_ms: 1000 # 0 is accuracy_ms, at most accuracy_ms32473 is the example enterprise number from RFC 5612; replace it. A policy
OID is yours to assign under your own IANA Private Enterprise Number: request
one at https://pen.iana.org, then pick an arc under
1.3.6.1.4.1.<PEN> for your TSA policy, for example 1.3.6.1.4.1.<PEN>.1.1,
and record what it means in your PKI policy documents.
Warning
Switching the TSA on or off, or changing any pki.tsa field, takes effect at
the next boot. config apply stores the change and reports that a reboot is
required; until then cryptosctl status shows the TSA configured but not
running (or the reverse) with a pending reboot. Plan the reboot in a
maintenance window: every listener on the node goes down with it.
To switch it off and keep the settings, send the block with enabled: false.
The listener is plain HTTP, as RFC 3161 describes: the token carries its own integrity and nothing in it is confidential. RFC 3161 has no client authentication, so the TSA answers anyone the limits let through:
- A client outside
allowed_networksgets HTTP 403 before its request is read. A bare address in the list means that one host. - Each client has a token bucket:
burstrequests at once, refilled atrequests_per_minute. A client is its IPv4 address, or its IPv6 /64. Over the limit it gets HTTP 429 withRetry-After.
Put the TSA behind your own TLS front end if your signing tools require an
https URL.
A timestamp is only a claim about the time, so the TSA fails closed on its
clock. It trusts the clock for a grace window after the last good time sync,
so one unanswered poll does not stop it. Every request is answered
timeNotAvailable while any of these holds:
| Limit | Refused when | Default |
|---|---|---|
no_good_sync |
no time sync has succeeded this boot, including a node with no time source at all | |
adjustment_refused |
the servers answered the latest round but the clock was not adjusted (they disagreed, or a step was refused); cleared by the next good sync | |
max_sync_age |
the last good sync is older than max_sync_age_seconds |
3600 (1 hour) |
max_clock_error |
the estimated clock error is larger than max_clock_error_ms |
accuracy_ms (1 second) |
The estimated clock error is the clock's error right after the last good
sync, plus max_drift_ppm parts per million of the time since it. That
right-after-sync figure is not the offset the sync corrected: once a sync
steps the clock, what remains is bounded by the server's root distance and
half the round-trip delay, typically a few milliseconds, not the (possibly
much larger) step that was just applied; a slew has not caught up to its
offset yet, so its figure is the full measured offset until the next sync. At
the defaults, a sync left with a 20 ms error gives an estimate of
20 ms + 360 ms = 380 ms an hour later, inside the 1 second bound.
max_drift_ppm defaults to 100, a conservative bound for a server crystal;
500 is the NTP frequency tolerance.
max_clock_error_ms may not be larger than the effective accuracy_ms:
config apply refuses it, because a token must never claim more accuracy than
the clock is trusted to have. Lower it to refuse earlier than the claimed
accuracy.
The refusal's status string names the limit and its value, for example
the TSA time source is not available: max_sync_age=1h0m0s exceeded (1h12m3s),
or just the limit name for no_good_sync and adjustment_refused. The full
reason, including the server and the time-sync error, is in the node log. pki.allow_unsynced_clock does not apply to
the TSA. See time-sync.md for the time sources and the
Clock: status line.
Caution
Set network.ntp_servers (or serve NTP in the DHCP lease) before you switch
the TSA on. Without a time source the TSA refuses every request.
The TSA certificate is valid for validity_days (capped at the CA
certificate's own expiry). rotation_overlap_days before it expires, the node
issues a successor with a new key and signs new tokens with it; the old
certificate stays valid for the rest of its life. A successor is also issued
when the current certificate is revoked, or when the CA key is rotated. The
node checks every hour, so this needs no reboot.
Every TSA certificate the node has signed with stays published after it is replaced or expires, so a token signed before a rotation still verifies. Only the key of a replaced certificate is deleted.
cryptosctl tsa certificates # newest first; CURRENT marks the signing one
cryptosctl tsa certificates --pem # the certificates themselvesOn any machine with OpenSSL 3, with the node's root and intermediate
certificates in chain.pem, build a query:
openssl ts -query -data artifact.bin -sha256 -cert -out req.tsqSend it to the TSA:
Linux / macOS
curl -sS -H 'Content-Type: application/timestamp-query' --data-binary @req.tsq \
-o resp.tsr http://tsa.example.org:318/Windows (PowerShell)
Invoke-WebRequest -Uri http://tsa.example.org:318/ -Method Post `
-ContentType 'application/timestamp-query' -InFile req.tsq -OutFile resp.tsrRead and verify the reply:
openssl ts -reply -in resp.tsr -text
openssl ts -verify -in resp.tsr -queryfile req.tsq -CAfile chain.pemThe last command prints Verification: OK.