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

Classes: Check, Result

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_digest for the precise rule and the reason it has to work this way.

"integrity"
DIGEST_KEY =

receipt_digest, not event_digest. The two are different values and answer different questions: event_digest was computed over the embedded event's canonical body when the event was written; receipt_digest covers 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

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

Returns:

  • (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.message}"
  )
  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