Module: Clickwrap::ReceiptVerifier
- Defined in:
- lib/clickwrap/receipt_verifier.rb
Overview
Verifies an exported canonical receipt.
=========================================================================== WHAT A SUCCESSFUL VERIFICATION HERE DOES AND DOES NOT ESTABLISH.
It DOES establish:
* that the file is well-formed JSON in a receipt schema this verifier
knows by name;
* that its bytes are canonical under RFC 8785, so two readers digest the
same thing;
* that the digest the receipt carries matches the receipt body it
travels with, so accidental or ordinary modification of those bytes is
detected;
* that each document file you supplied hashes to the digest the receipt
recorded for it, so the bundle is internally consistent; and
* that any chain links present are consistent with each other.
It does NOT establish:
* that the receipt was not fabricated. A self-contained file verifying
against itself proves internal consistency and nothing about origin. A
party who controlled the application, the database, and the export
could have produced every byte in it, including the digest, and this
verifier would say "verified" — because the only thing it can compare
the bytes against is the bytes;
* WHEN anything happened. `recorded_at_by_server` is a time an
application server wrote down. It is not attested by anyone else;
* WHO acted. An actor reference identifies a record in someone's
database, not a person;
* that any of it is legally sufficient, adequately presented, or
admissible anywhere. That is not a property of a file.
Origin and time evidence come from things this verifier cannot supply on its own: an independent publication holding the exact event-chain snapshot somewhere the database operator does not control, an RFC 3161 or trust-service timestamp, or a provider's own signed receipt. Where those exist, they are reported as exactly the assurance they supply, and where they do not, their absence is visible rather than papered over by a green check mark.
Defined Under Namespace
Constant Summary collapse
- KNOWN_SCHEMAS =
Every receipt schema this verifier can read.
THIS LIST ONLY EVER GROWS. A released evidence format is permanent: a new gem version may stop creating an old schema, but it must never stop verifying one, because the receipts already exported under it are out in the world and their whole value is that they still check out years later. A format change means a new entry here and a new branch of verification logic — never a silent reinterpretation of an old name.
["clickwrap.receipt.v1"].freeze
- INTEGRITY_KEY =
The key that holds the digest, and which is therefore excluded from the bytes the digest covers. See
body_covered_by_digestfor the precise rule and the reason it has to work this way. "integrity"- DIGEST_KEY =
receipt_digest, notevent_digest. The two are different values and answer different questions:event_digestwas computed over the embedded event's canonical body when the event was written;receipt_digestcovers the entire exported projection. The standalone verifier checks both (or a documented disposition in place of a removed event payload), and excludes only this self-referential receipt field when checking the latter. "receipt_digest"- PROVES =
Printed with every result, so nobody has to go looking for the caveat.
"A successful verification shows this receipt is internally consistent and its bytes have " \ "not changed since the digest was taken. It does not show who produced it, when, or that " \ "a party controlling every source could not have fabricated the whole file. Independent " \ "anchors and provider signatures are what add origin and time evidence."
Class Method Summary collapse
-
.body_covered_by_digest(body) ⇒ Object
The exact bytes a receipt's
integrity.receipt_digestcovers: the whole receipt with only that self-referential field removed. - .known_schema?(schema) ⇒ Boolean
-
.verify(canonical_json_string, documents: {}) ⇒ Object
Verifies
canonical_json_string. -
.verify!(canonical_json_string, documents: {}) ⇒ Object
Same, but raises rather than returning a failed result.
Class Method Details
.body_covered_by_digest(body) ⇒ Object
The exact bytes a receipt's integrity.receipt_digest covers: the whole
receipt with only that self-referential field removed. Every other
integrity claim—including event digest, chain position, tier, and claim
sentence—is covered and cannot be edited for free.
The exclusion is not a convenience. A digest cannot cover itself: the moment the digest is written into the object, the object's bytes change and the digest no longer matches them, and no amount of recomputation converges. So the digest is taken over the body before this one field is attached, and verification reproduces that by removing the field again.
The exclusion is precisely integrity.receipt_digest and nothing else.
Every other key — including the rest of integrity,
verifier_instructions, and any host
x_-prefixed extension — is inside the digest. A key that were excluded
without being named here would be a key anyone could edit freely, which
is the opposite of the point.
235 236 237 238 239 240 |
# File 'lib/clickwrap/receipt_verifier.rb', line 235 def body_covered_by_digest(body) covered = deep_copy(body) integrity = covered[INTEGRITY_KEY] integrity.delete(DIGEST_KEY) if integrity.is_a?(Hash) covered end |
.known_schema?(schema) ⇒ Boolean
216 |
# File 'lib/clickwrap/receipt_verifier.rb', line 216 def known_schema?(schema) = KNOWN_SCHEMAS.include?(schema.to_s) |
.verify(canonical_json_string, documents: {}) ⇒ Object
Verifies canonical_json_string.
documents: maps a document key (or "key@version") to the exact bytes
of that document. Any document not supplied is reported not_supplied
rather than assumed fine.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/clickwrap/receipt_verifier.rb', line 172 def verify(canonical_json_string, documents: {}) checks = [] schema = nil body = parse(canonical_json_string, checks) return Result.new(schema: nil, checks: checks) if body.nil? schema = body["schema"] return Result.new(schema: schema, checks: checks) unless check_schema(schema, checks) check_canonical_bytes(canonical_json_string, body, checks) check_receipt_digest(body, checks) check_event_digest(body, checks) check_lifecycle_successors(body, checks) check_integrity_attestations(body, checks) check_documents(body, documents, checks) check_chain_linkage(body, checks) Result.new(schema: schema, checks: checks) rescue StandardError => error checks << Check.new( name: "verifier_input", passed: false, detail: "The receipt could not be verified safely because #{error.class}: #{error.}" ) Result.new(schema: schema, checks: checks) end |
.verify!(canonical_json_string, documents: {}) ⇒ Object
Same, but raises rather than returning a failed result. Useful in a script where a bad receipt should stop the run.
An unknown schema gets its own error class, because it is a different problem with a different fix: the receipt is probably fine and this verifier is too old to read it.
206 207 208 209 210 211 212 213 214 |
# File 'lib/clickwrap/receipt_verifier.rb', line 206 def verify!(canonical_json_string, documents: {}) result = verify(canonical_json_string, documents: documents) return result if result.success? unknown = result.failures.find { |check| check.name == "known_schema" } raise UnknownReceiptSchema, unknown.detail if unknown raise ReceiptInvalid, result.to_s end |