Skip to content

Profile · RFC 9943 / RFC 9942

A receipt is a SCITT Signed Statement.

The IETF standardised the shape this product had been building towards: signed statements, registered with a transparency service, answered with receipts. RFC 9943 fixes the envelope and leaves the content of a statement to profiles. This page is that profile. A verifier that speaks SCITT can check a Lyntway receipt and its inclusion in the log without knowing anything else about us.

Reproducible vectors, with both keys derived from stated seeds, live at testdata/scitt/vectors.json in the public tools repository. Every hex value is lowercase; every CBOR value is deterministically encoded per RFC 8949 §4.2.1.

1 · The Signed Statement

One COSE_Sign1, tag 18, over the receipt you already hold.

#6.18([ protected: bstr, unprotected: {}, payload: bstr, signature: bstr ])

Payload. The receipt's canonical JSON (RFC 8785), verbatim, including its own signature object. The envelope signs the document the customer holds, not a re-serialisation of it.

Protected header, four entries in encoded-key order:

1   alg          -8            EdDSA (RFC 9053)
3   content type  50            uint: CoAP content-format application/json (RFC 9943 §6 allows tstr / uint)
4   kid           bstr key_id   RFC 9943 §6: kid MUST be present when neither x5t nor x5chain is
15  CWT Claims    {1: iss, 2: sub}   RFC 9943 §6: MUST include Issuer (1) and Subject (2)

iss is the receipt's issuer.key_id — the identifier a Lyntway verifier already resolves to a public key, so a SCITT verifier looks up the same thing. sub is the receipt's id. Both duplicate a payload field, and a verifier MUST reject an envelope whose claims disagree with its payload. Envelopes issued before this profile carry no label 15 and remain valid: absent claims are absent, not wrong.

Unprotected header. Empty as issued (RFC 9943 §6.3). Signature. Ed25519 over RFC 9052 §4.4 ["Signature1", protected, h'', payload].

2 · The entry and the leaf

The log holds the receipt digest, not the receipt.

receipt_digest = SHA-256( SigningInput(receipt) )   as 64 lowercase hex characters
entry          = those 64 ASCII bytes                (not the 32 raw bytes, not the COSE statement)
leaf_hash      = SHA-256( 0x00 || entry )            RFC 9162 §2.1.1
node_hash      = SHA-256( 0x01 || left || right )

SigningInput is the canonical receipt with signature and anchors removed — the same string a successor receipt records as its chain.prev_hash. This has been the log's leaf definition since it was written; it is documented rather than changed, because changing it would invalidate every root already published.

3 · The COSE Receipt

Signed by the log, payload detached, root recomputed by you.

#6.18([ protected: bstr, unprotected: { 396: {...} }, payload: null, signature: bstr ])

protected:
  1    alg         -8
  4    kid         bstr log key id
  15   CWT Claims  {1: log id, 2: entry}      RFC 9943 §6 requires claims on Receipts too;
                                              §5.1.2: iss is the transparency service's one identity
  395  vds         1                          RFC9162_SHA256 (RFC 9942 table 2); protected, because it
                                              tells the verifier how to read the proof
unprotected:
  396  vdp  { -1: [ bstr .cbor [ tree_size: uint, leaf_index: uint, inclusion_path: [ * bstr ] ] ] }
payload: detached (f6). The signed payload is the 32-byte root MTH(D[tree_size]),
         which appears nowhere inside the receipt bytes.

Verification (RFC 9942 §5.2.1): hash the entry as a leaf, walk inclusion_path per RFC 9162 §2.1.3.2 with leaf_index and tree_size to a candidate root; then verify the signature over ["Signature1", protected, h'', root] with the key named by kid. A receipt for any other entry fails at the second step, because no root the log ever signed comes out of the first. For a tree of size one the path is empty, which RFC 9162 defines and RFC 9942's CDDL does not admit; this profile encodes the empty array.

A consistency receipt has the same shape with the proof under key -2 as [tree_size_1, tree_size_2, consistency_path] and the newer root as its detached payload. A Transparent Statement is the Signed Statement with receipts placed in its unprotected header under label 394; its protected header, payload and signature are byte-identical to the statement alone.

Where to get each piece

GET /.well-known/lyntway-keys.json           issuer and retired keys
GET /v1/log/sth                                signed tree head
GET /v1/log/receipt?digest=<hex>&size=<n>      RFC 9942 inclusion receipt, application/cose
GET /v1/log/receipt/consistency?from=&to=      RFC 9942 consistency receipt
GET /v1/log/proof/inclusion?digest=&size=      the same proof as JSON, for our own verifier

Offline, with the command-line verifier: lyntway-verify -keys keys.json -inclusion receipt.cose -log-keys keys.json receipt.json.

What this profile is not

It is not a claim that Lyntway is a registered or audited SCITT transparency service, and it does not expose a registration policy: the service registers only receipts it issued itself. It does not certify anything about the AI system a receipt describes. It makes one promise — that an implementer holding the RFCs and the vectors can check our evidence without our help — and the vectors are how you hold us to it.

Back to the verification guide →