@postcept/receipt 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ needs nothing from Postcept except the published public key. You don't have to
6
6
  trust an API to trust the result.
7
7
 
8
8
  The format and signing scheme are written up in [SPEC.md](./SPEC.md). This is the
9
- canonical JS implementation of that spec; the Postcept API signs with a
9
+ canonical JS implementation of that spec. The Postcept API signs with a
10
10
  byte-identical implementation in Python.
11
11
 
12
12
  ## Install
@@ -39,6 +39,7 @@ on untrusted data.
39
39
  | `verifyBadge` | Verify a VCR-audit badge. |
40
40
  | `verifyReceiptInLog` | Verify a receipt's transparency-log inclusion and signed tree head. |
41
41
  | `verifyInclusion` / `verifySignedTreeHead` | Verify a Merkle inclusion proof or an STH on its own. |
42
+ | `verifyConsistency` | Verify the log only appended between two signed tree heads. |
42
43
  | `receiptSigningBody` | The exact body the signature covers, for anyone re-implementing this. |
43
44
  | `badgeSigningBody` | The badge signing body. |
44
45
  | `canonicalize` | Deterministic JSON encoding (sorted keys, ASCII-escaped). |
package/SPEC.md CHANGED
@@ -28,10 +28,10 @@ receipt JSON as transmitted. To reproduce the signed bytes:
28
28
 
29
29
  1. Build the signing body object for the receipt's `version` (§3).
30
30
  2. Encode it as **canonical JSON**:
31
- - object keys sorted lexicographically (by UTF-16 code unit);
32
- - no insignificant whitespace (separators `,` and `:`);
33
- - every non-ASCII character escaped as `\uXXXX` (i.e. `ensure_ascii`);
34
- - numbers as their shortest round-trip form; integers without a decimal point.
31
+ - object keys sorted lexicographically (by UTF-16 code unit)
32
+ - no insignificant whitespace (separators `,` and `:`)
33
+ - every non-ASCII character escaped as `\uXXXX` (i.e. `ensure_ascii`)
34
+ - numbers as their shortest round-trip form, integers without a decimal point.
35
35
  3. UTF-8 encode the resulting string. Those are the signed bytes.
36
36
 
37
37
  This is what Python's `json.dumps(body, sort_keys=True, separators=(",", ":"))`
@@ -131,7 +131,7 @@ same way across languages even at whole-number rates.
131
131
  ## 6. Machine-readable schema
132
132
 
133
133
  A JSON Schema for the receipt object ships at `@postcept/receipt/schema`
134
- (`schema/receipt.schema.json`). It validates shape only; §2 to §4 govern the
134
+ (`schema/receipt.schema.json`). It validates shape only. §2 to §4 govern the
135
135
  signature.
136
136
 
137
137
  ## 7. Transparency log
@@ -153,6 +153,16 @@ removed or back-dated, without taking Postcept's word for it.
153
153
  `GET /v1/transparency/proof/{receipt_id}`. Verify it by recomputing the root from
154
154
  the leaf and audit path, comparing to the STH `root_hash`, then checking the STH
155
155
  signature.
156
-
157
- Reference: `verifyReceiptInLog`, `verifyInclusion`, `verifySignedTreeHead`,
158
- `receiptLeafHash` in this package.
156
+ - **Consistency proof:** `{ first_size, second_size, first_root, second_root, proof
157
+ [hex], sth }`. Served at `GET /v1/transparency/consistency?first={m}&second={n}`
158
+ (`second` defaults to the current tree size). It proves the log at `second_size`
159
+ is an append-only extension of the log at `first_size`. The earlier leaves are
160
+ unchanged, and nothing was removed, reordered, or back-dated (RFC 6962 §2.1.2). A
161
+ holder of an earlier signed tree head fetches a proof against it: verify the proof
162
+ connects the two roots, then check that `first_root` equals the root they
163
+ remembered and that the returned head is validly signed. Without this, inclusion
164
+ proofs alone only bind a receipt to *whatever* root the operator serves now. The
165
+ consistency proof is what makes "append-only" checkable rather than trusted.
166
+
167
+ Reference: `verifyReceiptInLog`, `verifyInclusion`, `verifyConsistency`,
168
+ `verifySignedTreeHead`, `receiptLeafHash` in this package.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { asciiJson, base64ToBytes, canonicalize, TIMESTAMP_SPELLINGS } from "./canonical.js";
2
2
  export { type Postcondition, type Receipt, receiptSigningBody, verifyReceipt } from "./receipt.js";
3
3
  export { type AuditBadge, badgeSigningBody, verifyBadge } from "./badge.js";
4
- export { type InclusionProof, type SignedTreeHead, receiptLeafHash, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
4
+ export { type ConsistencyProof, type InclusionProof, type SignedTreeHead, receiptLeafHash, verifyConsistency, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
5
5
  export { type EvidenceExportLike, type EvidenceManifest, evidenceContentDigest, verifyEvidenceExport, verifyEvidenceManifest, } from "./evidence.js";
package/dist/index.js CHANGED
@@ -4,5 +4,5 @@
4
4
  export { asciiJson, base64ToBytes, canonicalize, TIMESTAMP_SPELLINGS } from "./canonical.js";
5
5
  export { receiptSigningBody, verifyReceipt } from "./receipt.js";
6
6
  export { badgeSigningBody, verifyBadge } from "./badge.js";
7
- export { receiptLeafHash, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
7
+ export { receiptLeafHash, verifyConsistency, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
8
8
  export { evidenceContentDigest, verifyEvidenceExport, verifyEvidenceManifest, } from "./evidence.js";
@@ -15,10 +15,30 @@ export interface InclusionProof {
15
15
  audit_path: string[];
16
16
  sth: SignedTreeHead;
17
17
  }
18
+ export interface ConsistencyProof {
19
+ first_size: number;
20
+ second_size: number;
21
+ first_root: string;
22
+ second_root: string;
23
+ proof: string[];
24
+ sth: SignedTreeHead;
25
+ }
18
26
  /** The RFC 6962 leaf hash for a receipt (hex), reconstructible from the receipt. */
19
27
  export declare function receiptLeafHash(receipt: Receipt): Promise<string>;
20
28
  /** Verify a Merkle inclusion proof against its signed root. */
21
29
  export declare function verifyInclusion(proof: InclusionProof): Promise<boolean>;
30
+ /**
31
+ * Verify an RFC 6962 consistency proof. It shows the log at `second_size` is an
32
+ * append-only extension of the log at `first_size`, with nothing removed,
33
+ * reordered, or back-dated. Confirms the proof connects `first_root` to
34
+ * `second_root`, and that `second_root` matches the signed head. Check the head's
35
+ * signature separately with verifySignedTreeHead, and compare `first_root` against
36
+ * the earlier head you saved.
37
+ *
38
+ * The walk is the RFC 9162 §2.1.4.2 verification algorithm. The awaits fold sibling
39
+ * hashes in sequence, so they cannot run in parallel.
40
+ */
41
+ export declare function verifyConsistency(proof: ConsistencyProof): Promise<boolean>;
22
42
  /** Verify the signed tree head's Ed25519 signature against the published key. */
23
43
  export declare function verifySignedTreeHead(sth: SignedTreeHead, publicKeyB64: string): Promise<boolean>;
24
44
  /**
@@ -42,6 +42,71 @@ export async function verifyInclusion(proof) {
42
42
  return false;
43
43
  }
44
44
  }
45
+ const isPowerOfTwo = (n) => (n & (n - 1)) === 0;
46
+ /**
47
+ * Verify an RFC 6962 consistency proof. It shows the log at `second_size` is an
48
+ * append-only extension of the log at `first_size`, with nothing removed,
49
+ * reordered, or back-dated. Confirms the proof connects `first_root` to
50
+ * `second_root`, and that `second_root` matches the signed head. Check the head's
51
+ * signature separately with verifySignedTreeHead, and compare `first_root` against
52
+ * the earlier head you saved.
53
+ *
54
+ * The walk is the RFC 9162 §2.1.4.2 verification algorithm. The awaits fold sibling
55
+ * hashes in sequence, so they cannot run in parallel.
56
+ */
57
+ /* eslint-disable no-await-in-loop */
58
+ export async function verifyConsistency(proof) {
59
+ try {
60
+ const first = proof.first_size;
61
+ const second = proof.second_size;
62
+ if (bytesToHex(hexToBytes(proof.second_root)) !== proof.sth.root_hash)
63
+ return false;
64
+ if (first < 0 || first > second)
65
+ return false;
66
+ if (first === second)
67
+ return proof.proof.length === 0 && proof.first_root === proof.second_root;
68
+ if (first === 0)
69
+ return proof.proof.length === 0;
70
+ const nodes = proof.proof.map(hexToBytes);
71
+ // A power-of-two prefix has no left-hand sibling in the path, so its own root is
72
+ // the seed the walk starts from.
73
+ if (isPowerOfTwo(first))
74
+ nodes.unshift(hexToBytes(proof.first_root));
75
+ if (nodes.length === 0)
76
+ return false;
77
+ let fn = first - 1;
78
+ let sn = second - 1;
79
+ while (fn & 1) {
80
+ fn >>= 1;
81
+ sn >>= 1;
82
+ }
83
+ let fr = nodes[0];
84
+ let sr = nodes[0];
85
+ for (let i = 1; i < nodes.length; i += 1) {
86
+ const c = nodes[i];
87
+ if (sn === 0)
88
+ return false;
89
+ if (fn & 1 || fn === sn) {
90
+ fr = await nodeHash(c, fr);
91
+ sr = await nodeHash(c, sr);
92
+ while ((fn & 1) === 0 && fn !== 0) {
93
+ fn >>= 1;
94
+ sn >>= 1;
95
+ }
96
+ }
97
+ else {
98
+ sr = await nodeHash(sr, c);
99
+ }
100
+ fn >>= 1;
101
+ sn >>= 1;
102
+ }
103
+ return sn === 0 && bytesToHex(fr) === proof.first_root && bytesToHex(sr) === proof.second_root;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ /* eslint-enable no-await-in-loop */
45
110
  /** Verify the signed tree head's Ed25519 signature against the published key. */
46
111
  export async function verifySignedTreeHead(sth, publicKeyB64) {
47
112
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postcept/receipt",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Reference implementation for verifying Postcept Receipts and VCR-audit badges, independently and in any environment.",
5
5
  "license": "MIT",
6
6
  "author": "Postcept",
@@ -17,7 +17,7 @@
17
17
  "signature"
18
18
  ],
19
19
  "properties": {
20
- "id": { "type": "string", "description": "Receipt id, e.g. pcpt_rcpt_" },
20
+ "id": { "type": "string", "description": "Receipt id, e.g. pcpt_rcpt_..." },
21
21
  "org_id": {
22
22
  "type": ["string", "null"],
23
23
  "description": "The organization the receipt belongs to (signed in v2)."
@@ -58,7 +58,7 @@
58
58
  "enum": ["1", "2"],
59
59
  "description": "Signing-body version. Absent is treated as \"1\"."
60
60
  },
61
- "signing_key_id": { "type": "string", "description": "Key fingerprint, e.g. ed25519:…" },
61
+ "signing_key_id": { "type": "string", "description": "Key fingerprint, e.g. ed25519:..." },
62
62
  "signature": {
63
63
  "type": "string",
64
64
  "description": "Base64 Ed25519 signature over the canonical signing body for this version."