@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 +2 -1
- package/SPEC.md +18 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/transparency.d.ts +20 -0
- package/dist/transparency.js +65 -0
- package/package.json +1 -1
- package/schema/receipt.schema.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
-
|
|
158
|
-
`
|
|
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";
|
package/dist/transparency.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/transparency.js
CHANGED
|
@@ -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
|
@@ -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."
|