Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ key pin, and matches the separately fetched receipt. See
This is a one-checkpoint demonstration, not continuous or reciprocal witnessing. It does not
prove registry continuity, prevent split views, cover the June entry, or prove payload retention.
The JSON response reports `countersigned-observed`, but that grade is not signed inside this
receipt. The receipt signs a Merkle root without a witness timestamp: the checkpoint timestamp
is the registry signer's assertion, and our capture time is an observer's local record. Do not
present either as a cryptographically authenticated witness time. The receipt binds the
receipt, and this receipt signs a Merkle root without a witness timestamp: the checkpoint
timestamp is the registry signer's assertion, and our capture time is an observer's local
record. Do not present either as a cryptographically authenticated witness time. Both are
properties of a receipt rather than of the profile. A witness may sign a registration time as
a CWT `iat` and its grade under a private-use label; where it does, `verify_witness_receipt`
reports `witness_time_established` and `grade_cryptographically_bound`, and where it does not,
both stay false. Read those two fields rather than this paragraph. The receipt binds the
checkpoint's nine-field signing body by digest; its signature and optional consistency proof
are not included in that witnessed digest. Registry signature verification is a separate check.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ Checkpoint 1 now has an offline-verifiable receipt from an independently operate
The [September 7 evidence packet](docs/evidence/witness-2026-09-07/README.md) includes the
original checkpoint, returned receipt, separately fetched copies, key provenance and verifier.
It proves inclusion of that checkpoint signing-body digest under the pinned witness key.
It does not certify continuity or a witness time; the response's grade is unsigned metadata.
It does not certify continuity. Whether a receipt carries a witness time or a signed grade is
a property of that receipt, so the verifier reports `witness_time_established` and
`grade_cryptographically_bound` rather than asserting either. Both are false for this capture.
The pipeline does not yet submit future checkpoints automatically. Parallel independent
witnesses remain supported as a deployment choice; only one operator is demonstrated here.

Expand Down
71 changes: 71 additions & 0 deletions tests/test_witness_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,74 @@ def test_extra_cbor_bytes_rejected(self):

if __name__ == '__main__':
unittest.main()


class SignedReceiptMetadataTests(unittest.TestCase):
"""The post-fix wire shape: a witness clock and a grade inside the signature.

These receipts are minted here, unlike every test above, because the
captured one predates the change. What is borrowed from the capture is
the inclusion proof and the root it commits to; the protected header and
the signature over it are ours, so nothing here asserts that the witness
has deployed anything.
"""

setUp = WitnessReceiptTests.setUp
check = WitnessReceiptTests.check

def mint(self, headers, *, grade=None):
import cbor2
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
root = bytes.fromhex(self.check()['root'])
envelope = cbor2.loads(base64.b64decode(self.response['receipt_b64'])).value
protected = cbor2.dumps(headers)
key = Ed25519PrivateKey.generate()
signature = key.sign(cbor2.dumps(['Signature1', protected, b'', root]))
receipt = cbor2.dumps(cbor2.CBORTag(18, [protected, envelope[1], None, signature]))
response = copy.deepcopy(self.response)
response['receipt_b64'] = base64.b64encode(receipt).decode()
if grade is not None:
response['grade'] = grade
public = key.public_key().public_bytes_raw().hex()
return self.check(response=response, witness_key=public)

def test_captured_receipt_carries_no_signed_metadata(self):
result = self.check()
self.assertIsNone(result['signed_iat'])
self.assertIsNone(result['signed_grade'])
self.assertFalse(result['limits']['witness_time_established'])

def test_signed_iat_is_surfaced_and_establishes_witness_time(self):
result = self.mint({1: -8, 395: 1, 15: {6: 1788914655}})
self.assertTrue(result['verified'], result)
self.assertEqual(result['signed_iat'], 1788914655)
self.assertTrue(result['limits']['witness_time_established'])
self.assertFalse(result['limits']['grade_cryptographically_bound'])

def test_signed_grade_binds_only_when_it_matches_the_response(self):
agreeing = self.mint({1: -8, 395: 1, -65537: 'countersigned-observed'},
grade='countersigned-observed')
self.assertTrue(agreeing['verified'], agreeing)
self.assertTrue(agreeing['limits']['grade_cryptographically_bound'])
disagreeing = self.mint({1: -8, 395: 1, -65537: 'countersigned-observed'},
grade='mmr-verified')
self.assertTrue(disagreeing['verified'], disagreeing)
self.assertFalse(disagreeing['limits']['grade_cryptographically_bound'])

def test_unreviewed_signed_header_is_refused(self):
result = self.mint({1: -8, 395: 1, 1234: 'anything'})
self.assertFalse(result['verified'])
self.assertIn('unreviewed signed receipt headers', result['error'])

def test_cwt_claims_map_carries_iat_and_nothing_else(self):
for claims in ({6: 1788914655, 1: 'issuer'}, {}, {6: -1}, {6: 'today'}):
with self.subTest(claims=claims):
result = self.mint({1: -8, 395: 1, 15: claims})
self.assertFalse(result['verified'], result)

def test_neither_field_present_is_the_pre_fix_shape(self):
result = self.mint({1: -8, 395: 1})
self.assertTrue(result['verified'], result)
self.assertIsNone(result['signed_iat'])
self.assertIsNone(result['signed_grade'])
self.assertFalse(any(result['limits'].values()))
55 changes: 53 additions & 2 deletions tools/verify_witness_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,49 @@ def unique(pairs):
return json.loads(Path(path).read_bytes(), object_pairs_hook=unique)



# COSE protected-header labels this adapter accepts. alg and vds are the
# stage-1 profile. 15 is the CWT Claims map (RFC 9597 s2), carrying 6, iat
# (RFC 8392 s3.1.6), the witness clock at registration. -65537 is private
# use: a witness may put its grade there, and a private-use label has no
# registered meaning, so it is read as an opaque string and only counts
# once it agrees with the grade the response reports. Anything else is
# refused rather than ignored: unreviewed signed metadata is not neutral.
# Duplicate labels collapse in the CBOR decoder rather than raising; that is
# tolerable here only because these bytes are inside the witness signature.
CWT_CLAIMS = 15
CWT_IAT = 6
PRIVATE_GRADE = -65537
ALLOWED_PROTECTED = {1, 395, CWT_CLAIMS, PRIVATE_GRADE}


def read_protected(protected, decode):
"""Return (iat, grade) from the protected header, refusing anything else."""
if not isinstance(protected, (bytes, bytearray)) or not protected:
raise ValueError('receipt protected header must be a non-empty bstr')
headers = decode(protected)
if not isinstance(headers, dict):
raise ValueError('receipt protected header must decode to a map')
if headers.get(1) != -8 or headers.get(395) != 1:
raise ValueError('unsupported receipt algorithm or verifiable data structure')
unknown = set(headers) - ALLOWED_PROTECTED
if unknown:
raise ValueError('unreviewed signed receipt headers: ' + ', '.join(map(str, sorted(unknown, key=str))))
iat = None
if CWT_CLAIMS in headers:
claims = headers[CWT_CLAIMS]
if not isinstance(claims, dict) or set(claims) != {CWT_IAT}:
raise ValueError('CWT claims map must carry iat and nothing else')
iat = claims[CWT_IAT]
if type(iat) is not int or iat <= 0:
raise ValueError('CWT iat must be a positive integer')
grade = None
if PRIVATE_GRADE in headers:
grade = headers[PRIVATE_GRADE]
if not isinstance(grade, str) or not grade:
raise ValueError('private-use grade label must be a non-empty text string')
return iat, grade

def verify(checkpoint, response, *, registry_key, witness_key, expected_log_id):
import cbor2
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
Expand Down Expand Up @@ -84,8 +127,9 @@ def verify(checkpoint, response, *, registry_key, witness_key, expected_log_id):
if envelope is None or len(envelope) != 4:
raise ValueError('receipt must be tagged COSE_Sign1')
protected, unprotected, payload, signature = envelope
if protected != cbor2.dumps({1: -8, 395: 1}) or payload is not None:
if payload is not None:
raise ValueError('unsupported receipt protected headers or attached payload')
signed_iat, signed_grade = read_protected(protected, cbor2.loads)
if set(unprotected) != {396} or set(unprotected[396]) != {-1} or len(unprotected[396][-1]) != 1:
raise ValueError('expected exactly one inclusion proof')
checks['receipt_profile'] = True
Expand All @@ -101,7 +145,14 @@ def verify(checkpoint, response, *, registry_key, witness_key, expected_log_id):
checks['response_coordinates'] = True
result.update(verified=True, checkpoint_signing_digest=digest.hex(), entry_hash=entry_hash,
root=verified.root, leaf_index=verified.leaf_index, tree_size=verified.tree_size,
reported_grade=response.get('grade'), witness_key=witness_key)
reported_grade=response.get('grade'), signed_iat=signed_iat,
signed_grade=signed_grade, witness_key=witness_key)
# A signed iat is a witness clock inside the covered bytes. A signed
# grade counts only when it is the grade the response reports: a
# private-use label agreeing with untrusted metadata is what binds it.
result['limits']['witness_time_established'] = signed_iat is not None
result['limits']['grade_cryptographically_bound'] = (
signed_grade is not None and signed_grade == response.get('grade'))
except Exception as exc:
result['error'] = type(exc).__name__ + ': ' + str(exc)
return result
Expand Down