Timestamp tokens¶
Warning
No timestamp token is validated by this library. RFC 9921
defines two header parameters that carry an RFC 3161 TimeStampToken, a CMS SignedData a Time Stamping
Authority signed over the hash of some bytes. This library reads the token out of the right bucket, parses its
TSTInfo far enough to find the MessageImprint and the time, and checks that the imprint is the hash of the
bytes RFC 9921 says it must be: the payload, or the signature. A true result means exactly this: the token is
about this message. It does not mean the token is genuine. The TSA's signature over the TSTInfo, the TSA's
certificate chain and the TSA's policy are what make the time worth anything, and this library checks none of
them, as it validates no chain for x5chain: spomky-labs/pki-framework has no CMS layer, and the DER is exposed
for the application, or a CMS implementation, to validate.
The two parameters¶
RFC 9921 §2 defines two modes, and §3 gives each its own header parameter so that their semantics never mix:
| Name | Label | Bucket | MessageImprint is the hash of |
Accessor |
|---|---|---|---|---|
3161-ttc, "Timestamp, Then COSE" |
269 (CoseHeaders::LABEL_3161_TTC) |
protected, "MUST" (§3.2) | the payload bytes, "This does not include the bstr wrapping" | get3161Ttc(): ?string |
3161-ctt, "COSE, Then Timestamp" |
270 (CoseHeaders::LABEL_3161_CTT) |
unprotected, "MUST" (§3.1) | the CBOR-encoded signature field of a COSE_Sign1, or the CBOR-encoded signatures field of a COSE_Sign |
get3161Ctt(): ?string |
Both hold "a DER-encoded TST [RFC3161] wrapped in a CBOR byte string", and both accessors return those DER bytes as
carried, or null when the message does not carry the parameter. The bucket is enforced, not preferred:
get3161Ttc()reads the protected bucket only and rejects a message that carries label 269 in the unprotected one. The token is obtained before signing and the signature commits to it; that is the point of the mode (§1.1: a transparency service registers the signed parts of a statement, so the token has to be one of them), and a token the signature does not cover proves nothing about what was signed.get3161Ctt()reads the unprotected bucket only and rejects a message that carries label 270 in the protected one: the token is computed over the signature, and the signature over the protected bucket, so a token under the signature would have to predate what it timestamps.- A value that is not a byte string, or is an empty one, is rejected by both. Nothing inside the bytes is read by
the accessors; the raw lookups
getProtectedHeaderParameter(CoseHeaders::LABEL_3161_TTC)andgetUnprotectedHeaderParameter(CoseHeaders::LABEL_3161_CTT)remain the lenient form.
The message imprint¶
Cose\Structure\Timestamp\MessageImprint is the MessageImprint of RFC 3161 §2.4.1,
SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage OCTET STRING }: the one thing a TimeStampReq
says about the data, and the one thing the token echoes back. It knows the bytes each mode hashes:
use Cose\Algorithm\Hash\SHA256;
use Cose\Structure\Timestamp\MessageImprint;
// 3161-ttc: the payload bytes, and only them (RFC 9921 §3.2)
$imprint = MessageImprint::ttc(SHA256::create(), $payload);
$input = MessageImprint::ttcInput($payload); // === $payload
// 3161-ctt: the CBOR-encoded signature field of a COSE_Sign1, 0x5840 || 64 bytes for ES256 (§3.1.1),
// or the CBOR-encoded signatures array of a COSE_Sign, entries and their header buckets included (§3.1.2)
$imprint = MessageImprint::ctt(SHA256::create(), $coseSign1);
$input = MessageImprint::cttInput($coseSign1); // "\x58\x40" . $signatureBytes
$imprint->toDER(); // the SEQUENCE to place in a TimeStampReq
$imprint->getHashAlgorithmOid(); // "2.16.840.1.101.3.4.2.1"
$imprint->getHashedMessage(); // 32 raw bytes
MessageImprint::fromDER($der)->equals($imprint); // hash_equals() on the digest
ttc() and ctt() take a Hash and not a FilterOnlyHash: the imprint stands for the data, which is the
integrity use of RFC 9054 §2, so SHA-1 cannot be picked by a sender. The class maps the RFC 9054 algorithms to
their object identifiers and back (hashAlgorithmOid(), hashAlgorithmIdentifier()): SHA-256, SHA-384, SHA-512,
SHA-512/256, SHAKE128 and SHAKE256 (RFC 5754 and RFC 8702), and SHA-1, listed so that a token hashed with it is
recognized and refused for what it is. SHA-256/64 has no OID. cttInput() returns the encoding the message holds, so
a message decoded from the wire yields the wire bytes, whatever their length encoding.
Building the TimeStampReq around the imprint (a nonce, a policy, certReq) and sending it to a TSA is the
application's, and so is reading the TimeStampResp: the library never performs network access.
examples/18-rfc3161-timestamps.php builds the request of RFC 9921
Appendix A.1 byte for byte with pki-framework's ASN.1 types.
Binding a token to a message¶
Cose\Structure\Timestamp\TimeStampToken::fromDER() reads a token far enough to bind it: the outer CMS
ContentInfo has to be id-signedData, the encapsulated content id-ct-TSTInfo, the TSTInfo version 1; then
getMessageImprint(), getPolicy(), getSerialNumber(), getGenTime() (a UTC DateTimeImmutable) and
getNonce() are read, getTstInfo() is the decoded structure for the fields that are not, and toDER() is the
token as carried, for whatever validates it next. Trailing bytes, a wrong content type and a malformed TSTInfo
are rejected.
Cose\Structure\Timestamp\TimestampBinding is the check of RFC 9921 §4:
"the receiver MUST make sure that the MessageImprint in the embedded timestamp token matches a hash of either the
payload, signature, or signature fields, depending on the mode of use and type of COSE structure".
use Cose\Algorithm\Manager;
use Cose\Algorithm\Hash\SHA256;
use Cose\Structure\CoseHeaders;
use Cose\Structure\Timestamp\TimeStampToken;
use Cose\Structure\Timestamp\TimestampBinding;
$binding = TimestampBinding::create(Manager::create()->add(SHA256::create()));
$headers = CoseHeaders::fromMessage($message); // CBOR\Tag\CoseSign1Tag or CoseSignTag
// once the COSE signature has been verified (doc/Signing.md):
$binding->matchesTtc($headers, $payloadBytes); // the 3161-ttc token is over the payload
$binding->matchesCtt($headers, $message); // the 3161-ctt token is over the signature(s) field
$binding->matches($headers, $message); // every token the message carries, each under its rule;
// pass the payload as third argument when it is detached
$token = TimeStampToken::fromDER($headers->get3161Ctt());
$token->getGenTime(); // when the signature existed, if the TSA is to be believed
$token->toDER(); // 5453 bytes for the CMS validation this library does not do
The hash algorithm is the token's, resolved the way every identifier from the wire is: the OID is mapped to its RFC
9054 identifier, the identifier looked up in the Manager of the application, and the result has to be a Hash.
An OID that is not an RFC 9054 algorithm, an identifier the Manager does not register, and SHA-1, which is
Filter Only, are each an InvalidArgumentException that says which; a mismatch is false, compared with
hash_equals(). A message carrying neither parameter is "not a timestamped message", an exception rather than a
false that would read as a failed check.
What follows from the RFC¶
- The two modes do not say the same thing. §5.1: "Implementers MUST clearly differentiate between TSA
timestamps proving the existence of payload data at an earlier point in time (TTC) and timestamps explicitly
providing evidence of the existence of the cryptographic signature (CTT)." A
3161-ttctoken says the payload existed atgenTimeand nothing about when it was signed; a3161-ctttoken says the signature existed atgenTime. "Validators must not interpret protected-header payload timestamps as proof of signature creation time." The two accessors and the twomatches*()methods are separate for that reason, andmatches()reports which ones held only through what the message carries: readget3161Ttc()andget3161Ctt()to know what atrueproves. A message may carry both. iat,nbfandexpare claims, not proofs. A CWT carries times the signer asserts about itself (CBOR Web Tokens); a TSA'sgenTimeis a third party's word about when some bytes existed. Neither replaces the other, and §5.1's warning applies to both: a payload timestamp, from a TSA or from a claim, is not when the signature was made.- A
3161-ctttoken can be stripped. It sits in the unprotected bucket, so "an attacker could manipulate the unprotected header by removing or replacing the timestamp" (§5); the RFC's answer is that the message "should be integrity protected during transit and at rest". A message without the parameter is a message without a timestamp, which is whatnullsays. - The signature is verified first. As for a hash envelope or a receipt, the binding says the token is about this message; only the verified COSE signature says whose message it is, and only the validated CMS signature says the TSA's time is real. Three checks, three answers.
The imprints are checked against the values RFC 9921 prints in §3.1.1 and §3.1.2 for the COSE_Sign1 and
COSE_Sign of RFC 9052 Appendix C, and against the token of Appendix A.1, issued by freetsa.org over the payload,
vendored under tests/fixtures/rfc9921/. The token of Appendix A.2 is vendored too,
and the test suite shows it does not bind to the message it is attached to: the RFC's example generator hashed the
error output of a CBOR diagnostic tool instead of the signature, a slip documented in the fixtures' README; the
normative text of §3.1 and the worked computation of §3.1.1 are what this library implements.