Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(headers): 3161-ttc and 3161-ctt timestamp tokens, carried and bound, not validated (RFC 9921) (#217) - #225

Merged
Spomky merged 1 commit into
4.9.xfrom
feat/issue-217-rfc3161-timestamps
Sep 13, 2026
Merged

feat(headers): 3161-ttc and 3161-ctt timestamp tokens, carried and bound, not validated (RFC 9921) (#217)#225
Spomky merged 1 commit into
4.9.xfrom
feat/issue-217-rfc3161-timestamps

Conversation

@Spomky

@Spomky Spomky commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Target branch: 4.9.x
Resolves issue #217 (Closes #217)
Part of #202

  • It is a Bug fix
  • It is a New feature
  • Breaks BC
  • Includes Deprecations

What

The two header parameters of RFC 9921, 3161-ttc (269) and 3161-ctt (270), each carrying an RFC 3161 TimeStampToken, read from the bucket the RFC mandates and bound to the message the way §4 requires. Everything is additive: two constants, two accessors, one new namespace with three classes; no existing public signature changes, no new dependency (the ASN.1 layer is pki-framework's, already required).

No timestamp token is validated by this library. That sentence opens doc/Timestamps.md and the docblocks of TimeStampToken and TimestampBinding. A true result means exactly: the token is about this message, i.e. its MessageImprint is the hash of the payload (TTC) or of the signature field (CTT). The TSA's CMS signature, certificate chain and policy are the application's to validate with a CMS implementation, as the chain of x5chain is; pki-framework has no CMS layer and the DER is exposed for it.

CoseHeaders

  • get3161Ttc(): ?stringprotected bucket only (§3.2 "MUST"); a label 269 in the unprotected bucket, alone or as a copy, rejects the message: a token the signature does not cover proves nothing about what was signed (§1.1, the transparency use case is why the mode exists).
  • get3161Ctt(): ?stringunprotected bucket only (§3.1 "MUST"); a label 270 in the protected bucket rejects the message: a token over the signature cannot be under the signature.
  • Both return the DER bytes as carried, null when absent; a value that is not a byte string, and an empty byte string, are rejected; indefinite-length byte strings are read. Nothing inside the bytes is parsed by the accessors. LABEL_3161_TTC, LABEL_3161_CTT.

Cose\Structure\Timestamp

Class Role
MessageImprint RFC 3161 §2.4.1, SEQUENCE { hashAlgorithm, hashedMessage }, and the bytes each mode hashes into it. ttcInput($payload) / ttc(Hash, $payload): the payload bytes, "This does not include the bstr wrapping" (§3.2). cttInput($message) / ctt(Hash, $message): the CBOR-encoded signature field of a COSE_Sign1, head included ("including the heading bytes 0x5840", §3.1.1), or the CBOR-encoded signatures array of a COSE_Sign (§3.1.2), as the message carries them (an indefinite-length signature hashes with its framing). hashAlgorithmOid() / hashAlgorithmIdentifier() map the RFC 9054 algorithms to their OIDs (RFC 5754, RFC 8702; SHA-1 listed so a SHA-1 token is refused as SHA-1, SHA-256/64 has none). toASN1() / toDER() for the TimeStampReq the application builds, fromASN1() / fromDER() to read one back (parameters absent or NULL per RFC 5754 §2, anything else rejected), equals() with hash_equals(). The senders' constructors take Hash, not FilterOnlyHash: the imprint stands for the data.
TimeStampToken fromDER(): the CMS ContentInfo must be id-signedData, the eContentType id-ct-TSTInfo, the TSTInfo version 1; trailing bytes at either level rejected; every decode failure is an InvalidArgumentException. Reads getMessageImprint(), getPolicy(), getSerialNumber() (decimal string, 160-bit serials), getGenTime() (UTC DateTimeImmutable), getNonce() (found among the optional fields by type); getTstInfo() is the decoded Sequence for the rest, toDER() the bytes as carried. Verifies nothing.
TimestampBinding create(Manager). matchesTtc(CoseHeaders, $payload), matchesCtt(CoseHeaders, $message), matches(CoseHeaders, $message, ?$detachedPayload) (every token the message carries, each under its own rule; a message with neither is "not a timestamped message", an exception, not a false that would read as a failed check), tokenMatches(TimeStampToken, $input) for callers holding both. The token's OID → RFC 9054 identifier → Manager → must be a Hash: an unknown OID, an unregistered identifier, and SHA-1 (FilterOnlyHash) each throw naming the OID; a mismatch is false via hash_equals().

Acceptance criteria of #217

  • The RFC 9921 examples: the imprint recomputed by the library equals the hashedMessage inside the committed TST for both modes and both structures. Rfc9921ExamplesTest: §3.1.1 (CTT, COSE_Sign1) gives 44c2419d… and §3.1.2 (CTT, COSE_Sign) gives 803fada2…, over the RFC 9052 Appendix C.2.1 and C.1.1 messages, whose signatures are first checked to verify with key "11" so the inputs are the genuine ones; Appendix A.1 (TTC): the message is rebuilt from the appendix, round-tripped through the wire, the token read out of the protected bucket, decoded to the printed policy/serial/genTime, and its imprint 09e638d4… binds to the payload. Appendix A.2 (CTT) does not bind, and that is the RFC's slip, not the library's: see below.
  • 269 in the unprotected bucket and 270 in the protected bucket are rejectedCoseHeadersTest (+8: both structures, alone and as a copy, non-bstr and empty values, indefinite-length) and TimestampBindingTest::aTokenInTheWrongBucketIsRejectedBeforeAnyBinding.
  • A token whose TSTInfo imprint does not match the message fails matches() — other payload, CBOR-wrapped payload, one-byte-different signature, the COSE_Sign1 imprint against the COSE_Sign, a swapped token in a message carrying both modes, wrong-length digests (TimestampBindingTest).
  • SHA-1 refused through the registry (FilterOnlyHash), SHA-512 tokens checked with SHA-512, detached payloads supplied by the caller.
  • Docs say in the first paragraph what is not validated, and §5.1 is applied: separate accessors and checks per mode, "TTC: payload existed at genTime. CTT: signature existed at genTime. iat/exp: what the signer claims."
  • examples/18-rfc3161-timestamps.php hashes the payload, prints the TimeStampReq (built with pki-framework's ASN.1 types, byte-identical to the request behind Appendix A.1), inserts the canned A.1 token and binds it; then the CTT flow over a fresh signature with an unsigned stand-in TSTInfo (no fixture can cover a randomized ECDSA signature), what each mode proves, both misplacements, a SHA-1 token, the A.2 token, and what is left to a CMS implementation.

RFC 9921 Appendix A.2 is inconsistent with §3.1.1, and the cause is identified

The committed 3161-ctt token carries the imprint dd9471ef…; §3.1.1 computes 44c2419d… = SHA-256(0x5840 ‖ signature) for the very same message, which this PR reproduces. The draft's source repository shows why: example/ctt/create-example.sh extracts the signature with awk '/signature/ {print $4}' and pipes it through diag2cbor.rb; commit 58407a7b (2025-05) line-folded in.diag, so awk yields only the first line, h'8eb33e4c…c4d (unterminated); diag2cbor.rb prints a parse error, set -o pipefail does not reach into the <(…) substitution, and when commit 086f7f79 (2025-08-29) regenerated the token, openssl ts -query -data hashed that error text. sha256("*** can't parse h'8eb3…c4d\n*** Expected one of …") = dd9471ef…, reproduced verbatim in Rfc9921ExamplesTest (cbor-diag 0.11.8). The A.1 script hashes a literal string and is unaffected; the tokens are byte-identical to the repository's .der files. An erratum against Appendix A.2 was reported to the RFC Editor on 2026-09-13 with this account as its notes (listed at https://www.rfc-editor.org/errata/rfc9921 once screened). Full account in tests/fixtures/rfc9921/README.md; RELEASES.md has the short version.

Tests

  • tests/Structure/Timestamp/: MessageImprintTest (both modes, both structures, indefinite-length input, the seven OIDs both ways, SHAKE parameter-less identifiers, absent SHA-2 parameters, twelve malformed shapes), TimeStampTokenTest (both appendix tokens, eight optional-field combinations around the nonce, 2¹⁶⁰−1 / 2¹²⁸−1 integers, twenty malformed tokens with their messages), TimestampBindingTest, Rfc9921ExamplesTest; TokenBuilding builds unsigned tokens (the library reads the TSTInfo only, so a signed one would test nothing this library does).
  • CoseHeadersTest (+8), RfcReferencesTest knows RFC 9921, ExamplesTest runs examples/18.
  • 3564 tests, 32402 assertions, green on the CI image (phpqa 8.4); PHPStan (max), ECS, Rector, Deptrac, parallel-lint, composer validate --strict, composer normalize clean.
  • The Rector job was already red on 4.9.x (RfcReferencesTest:356, assertArrayNotHasKey()), which skipped every test job; the mechanical fix is included so this PR's CI runs.

Docs and examples

README (This library implements, chapter table, the "verifies, it does not decide" line), doc/README.md (intro, chapter table, tests table, references incl. RFC 3161), new doc/Timestamps.md, RELEASES.md, composer.json keyword RFC9921, examples/18-rfc3161-timestamps.php + index, tests/fixtures/rfc9921/ (two tokens, extract.py, README with provenance and the A.2 account).

@Spomky Spomky added this to the 4.9.0 milestone Sep 13, 2026
@Spomky Spomky added enhancement New feature or request compliance Issue related to the compliance of the project with the specs headers Header parameters and message structures labels Sep 13, 2026
…und, not validated (RFC 9921) (#217)

CoseHeaders::get3161Ttc() reads label 269 from the protected bucket only
and get3161Ctt() reads label 270 from the unprotected bucket only, each
as the DER bytes of the RFC 3161 TimeStampToken it wraps; a token in the
other bucket, a value that is not a byte string and an empty byte string
are rejected, as RFC 9921 §3.1 and §3.2 place the parameters.

Cose\Structure\Timestamp holds the three new classes. MessageImprint is
the structure of RFC 3161 §2.4.1 and the bytes each mode hashes into it:
the payload without its CBOR head for TTC, the CBOR-encoded signature
field of a COSE_Sign1 or the CBOR-encoded signatures field of a
COSE_Sign for CTT, with the RFC 9054 hash algorithms mapped to their
OIDs and toDER() for the TimeStampReq the application builds.
TimeStampToken::fromDER() is a minimal parser of the CMS SignedData down
to the TSTInfo (imprint, policy, serial number, genTime, nonce) with the
content types and the version checked and nothing verified.
TimestampBinding is the check of §4, matchesTtc(), matchesCtt() and
matches(), the token's hash algorithm resolved through the application's
Manager and required to be a Hash, so that a SHA-1 token fails even when
SHA-1 is registered for x5t. The TSA's CMS signature, certificate chain
and policy are the application's, as the chain of x5chain is.

The two tokens of RFC 9921 Appendix A are vendored under
tests/fixtures/rfc9921/. The imprints of §3.1.1 and §3.1.2 are
reproduced over the COSE_Sign1 and COSE_Sign of RFC 9052 Appendix C,
whose signatures verify with key "11", and the Appendix A.1 token binds
to its payload. The Appendix A.2 token does not bind to its own message:
its imprint is the SHA-256 of the error output of diag2cbor.rb on the
line-folded in.diag of the draft's example generator, not of the
signature field; the test suite reproduces that byte for byte and the
fixtures' README has the account. No erratum exists at this time.

tests/RfcReferencesTest.php also takes the assertArrayNotHasKey() that
Rector had been asking for on 4.9.x, so that the Rector job passes and
the test jobs run.
@Spomky
Spomky force-pushed the feat/issue-217-rfc3161-timestamps branch from b7b8483 to 9e2c853 Compare September 13, 2026 17:07
@Spomky
Spomky merged commit b974bf8 into 4.9.x Sep 13, 2026
18 checks passed
@Spomky Spomky self-assigned this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compliance Issue related to the compliance of the project with the specs enhancement New feature or request headers Header parameters and message structures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(headers): RFC 3161 timestamp tokens 3161-ttc and 3161-ctt, carried and bound but not validated (RFC 9921)

1 participant