UMFR receipts: specification v0.1
1. What a receipt is
A report UMFR delivers carries a receipt. The report’s content is a canonical object, the record, attached inside the PDF as umfr-dd-object.json. The receipt states the record’s SHA-256 digest and an Ed25519 signature over that digest, and is attached beside it as umfr-receipt.json. The report also prints both in its reproducibility receipt. The signature sits beside the record, never inside it, so signing changes no digest.
2. What a valid signature proves, and what it does not
A valid signature proves that the record was issued under the key the receipt names, and that the record has not changed since. It does not prove that any figure in the record is true, that the source documents are genuine, or anything about the borrower. A receipt whose signature reads “unsigned” was issued by a build that held no signing key: its digest still reproduces from the record, and nothing in it shows which key issued it.
3. The record and its digest
The record is serialised in canonical form: object keys sorted at every level by UTF-16 code units (the order JavaScript’s sort gives), array order kept, no whitespace, JSON escaping as JSON.stringify writes it, encoded as UTF-8. It carries no wall-clock time. The package digest is the SHA-256 of the record file’s bytes, written as 64 lowercase hexadecimal characters.
A verifier checks two things about the record: its bytes hash to the package digest, and parsing it and serialising it again in canonical form gives the same bytes. The second check is what makes the digest a digest of the content rather than of one way of writing it.
4. The signature and the receipt file
The signature is Ed25519 (RFC 8032, without pre-hashing) over the UTF-8 bytes of exactly three lines:
umfr-receipt/0.1 <kid> <package digest>
separated by a single line feed, with no line feed at the end. The 64-byte signature is written in base64url without padding. The receipt file is canonical JSON with these fields:
{
"packageDigest": "<64 lowercase hex>",
"record": "umfr-dd-object.json",
"signature": { "alg": "Ed25519", "kid": "<kid>", "spec": "umfr-receipt/0.1", "state": "signed", "value": "<base64url>" },
"spec": "umfr-receipt/0.1"
}An unsigned receipt carries "signature": { "state": "unsigned" } in place of the signature: the state is written, never left out.
5. The key file
The public keys are listed at https://umfr.io/.well-known/umfr-receipt-key.json. Each entry gives kid, alg, publicKey (the raw 32-byte Ed25519 key in base64url), validFrom, validTo, status (active, retired or revoked) and revoked (the date a revoked key was revoked, otherwise null).
A retired key stays listed, so a receipt it signed can still be checked. A revoked key stays listed with its date; a signature under it is reported as made under a revoked key and should not be relied on, because a receipt carries no time and a verifier cannot tell whether it was signed before the revocation. A lost key is replaced by publishing a new key and retiring or revoking the old one. There is no fallback key: a build that has no valid key issues unsigned receipts.
The key in use:
kid umfr-receipt-2026-10-a alg Ed25519 publicKey Czl1OfcWGTAQzag6gTOPylsuMjNxPWgIO2_iSDD2h1k validFrom 2026-10-12
6. Where the parts sit in a delivered report
In the report, the reproducibility receipt prints the package digest, then either the signature line (the algorithm, the spec, the kid, and the signature value) with a sentence on what it proves and what it does not, or “Signature: unsigned”. The PDF carries the record as umfr-dd-object.json and the receipt as umfr-receipt.json. Any PDF reader that lists attachments can extract both.
7. Checking a receipt without UMFR
The verifier is two files under the MIT licence, each with its own licence file. It makes no network call: download the key file yourself and pass it in.
- umfr-receipt-verify.mjs: the verifier, one file with no dependencies, for a browser or Node 20+ (licence)
- umfr-verify.mjs: the command line, kept in the same folder (licence)
- test-vectors.json: the vectors in section 9
node umfr-verify.mjs report.pdf --keys umfr-receipt-key.json node umfr-verify.mjs --record umfr-dd-object.json --receipt umfr-receipt.json --keys umfr-receipt-key.json
It exits 0 only when the digest reproduces, the record is canonical and the signature is valid under a key that is not revoked; otherwise it exits 1 and says which check failed. In a browser, import the module and call verifyReport(pdfBytes, keyFile) or verifyParts({ record, receipt }, keyFile).
8. Identifier fields (reserved)
v0.1 records carry no identifier fields. Two are reserved, and will sit inside the record (so under the digest) in an object named identifiers:
cik: the SEC Central Index Key of the filer, ten digits with leading zeros, as the filing states it.lei: the 20-character Legal Entity Identifier (ISO 17442), present only where the filing itself carries it. It is never looked up or inferred.
A verifier treats them as ordinary record content; they change nothing in sections 3 to 7.
9. Test vectors
The vectors are signed with two TEST keys. They are published here on purpose, private halves included, so a signature can be reproduced; neither is in the published key file, so nothing they sign verifies against it. Check every vector against this TEST key file:
{
"spec": "https://umfr.io/docs/receipt",
"keys": [
{
"kid": "umfr-receipt-TEST-a",
"alg": "Ed25519",
"publicKey": "4nVL9FlYwmf0VXu5tuV77wrJpW92RrgSWBzOMOAFt5w",
"validFrom": "2026-10-12",
"validTo": null,
"status": "active",
"revoked": null
},
{
"kid": "umfr-receipt-TEST-b",
"alg": "Ed25519",
"publicKey": "e8FtchzZyHogPXx-8HCTnMlCesQIUvpA7zD4NWBu8TI",
"validFrom": "2026-09-01",
"validTo": "2026-10-01",
"status": "revoked",
"revoked": "2026-10-01"
}
]
}The TEST private keys (PKCS8 DER, base64), for reproducing the signatures:
umfr-receipt-TEST-a MC4CAQAwBQYDK2VwBCIEIMbxJykfUj6i4m1y3r2ybC/Ate4W4nGlD7/LHWxY9Ozf umfr-receipt-TEST-b MC4CAQAwBQYDK2VwBCIEIKd00c6LaTVwCMKV4UNwJ14eGb/+C9d5Cn4c607Znh8H
1. signed and intact
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.25"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-a","spec":"umfr-receipt/0.1","state":"signed","value":"z9eO1cfe7ef_5FQHPnNegDB0K3jkMR3viLmtdveuyxUolU4wTGskGoDmh_HzpohO9R4DWXUsEqkPTGbQLrYmAA"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":true,"signature":"valid","ok":true}2. record altered after signing (one figure changed)
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.26"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-a","spec":"umfr-receipt/0.1","state":"signed","value":"z9eO1cfe7ef_5FQHPnNegDB0K3jkMR3viLmtdveuyxUolU4wTGskGoDmh_HzpohO9R4DWXUsEqkPTGbQLrYmAA"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":false,"recordCanonical":true,"signature":"valid","ok":false}3. signature made over a different digest
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.25"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-a","spec":"umfr-receipt/0.1","state":"signed","value":"s95Xg5K2P5NYzNVKfukkKovCqvscYxj1xPdQQOcDFtCuD7GxsnaVSSNQPSmMdcPqif0TRk3-GnttQElUF7szCA"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":true,"signature":"invalid","ok":false}4. unsigned
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.25"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"state":"unsigned"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":true,"signature":"unsigned","ok":false}5. signed under a key the key file does not list
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.25"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-unlisted","spec":"umfr-receipt/0.1","state":"signed","value":"cP5NRmvhq-jGIJJAE4PGW-pVSW0XPiabm1GqS887bueYjCZVcMsiuIgYTDKx5GLI2TgLIm0ZiGXhjldfyvslCQ"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":true,"signature":"unknown-key","ok":false}6. signed under a key the key file marks revoked
record {"example":"umfr-receipt/0.1 test vector","figures":[{"reason":"not recomputable from reported line items","state":"declined","term":"Consolidated EBITDA"},{"state":"verified","term":"Total Net Leverage Ratio","value":"3.25"}]}
receipt {"packageDigest":"75715708c52fb6419372f3f6215345cc610ea7bf2bbb551683c7df6517c7790e","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-b","spec":"umfr-receipt/0.1","state":"signed","value":"7Z0kzATr-RGDHbPWzjRwB8SIhMHaYA8KTeZPNAQhK1iMEMKtlUn-v-fXxNkN3XDrYcFS3R6HQZM8GShX6y65Ag"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":true,"signature":"revoked-key","ok":false}7. record not in canonical form (digest and signature over its bytes as given)
record {
"example": "umfr-receipt/0.1 test vector",
"figures": [
{
"term": "Consolidated EBITDA",
"state": "declined",
"reason": "not recomputable from reported line items"
},
{
"term": "Total Net Leverage Ratio",
"state": "verified",
"value": "3.25"
}
]
}
receipt {"packageDigest":"1e663b02e0b237955717e2b96f80e3d4c63d68916bb0c5abe91abcb122087293","record":"umfr-dd-object.json","signature":{"alg":"Ed25519","kid":"umfr-receipt-TEST-a","spec":"umfr-receipt/0.1","state":"signed","value":"cL-NOV3PoiRPEdUCn0RWbaAKEG5pzYor47zauMhq_i0-brtY_hJCwDeoO-BkNGQx_U43WEOx8f9URE1fOVp3DA"},"spec":"umfr-receipt/0.1"}
expect {"digestReproduces":true,"recordCanonical":false,"signature":"valid","ok":false}10. Versions
umfr-receipt/0.1 is the first published version. A change to any rule on this page is a new version, and the version is named in every signed message and every receipt file, so a receipt is always checked under the rules it was issued under.