@oxy.so/protocol 1.0.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/chain/continuity.js +54 -0
- package/dist/cjs/chain/engine.js +34 -0
- package/dist/cjs/chain/recordStore.js +25 -0
- package/dist/cjs/chain/types.js +22 -0
- package/dist/cjs/chain/verify.js +82 -0
- package/dist/cjs/envelope/canonicalJson.js +107 -0
- package/dist/cjs/envelope/recordId.js +60 -0
- package/dist/cjs/envelope/sign.js +75 -0
- package/dist/cjs/envelope/signingInput.js +32 -0
- package/dist/cjs/identity/resolver.js +50 -0
- package/dist/cjs/index.js +71 -0
- package/dist/cjs/node/constants.js +85 -0
- package/dist/cjs/node/didWebResolver.js +126 -0
- package/dist/cjs/node/httpFetch.js +61 -0
- package/dist/cjs/node/index.js +71 -0
- package/dist/cjs/node/nodeApp.js +344 -0
- package/dist/cjs/node/nodeClient.js +204 -0
- package/dist/cjs/node/rateLimit.js +187 -0
- package/dist/cjs/node/verifyRecord.js +51 -0
- package/dist/cjs/platform/crypto.js +186 -0
- package/dist/cjs/platform/crypto.native.js +204 -0
- package/dist/cjs/platform/expoTypes.js +24 -0
- package/dist/cjs/platform/platform.js +33 -0
- package/dist/cjs/secp256k1.js +148 -0
- package/dist/cjs/transparency/checkpoint.js +79 -0
- package/dist/cjs/transparency/tree.js +197 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/chain/continuity.js +51 -0
- package/dist/esm/chain/engine.js +31 -0
- package/dist/esm/chain/recordStore.js +24 -0
- package/dist/esm/chain/types.js +19 -0
- package/dist/esm/chain/verify.js +78 -0
- package/dist/esm/envelope/canonicalJson.js +104 -0
- package/dist/esm/envelope/recordId.js +56 -0
- package/dist/esm/envelope/sign.js +69 -0
- package/dist/esm/envelope/signingInput.js +29 -0
- package/dist/esm/identity/resolver.js +47 -0
- package/dist/esm/index.js +36 -0
- package/dist/esm/node/constants.js +82 -0
- package/dist/esm/node/didWebResolver.js +122 -0
- package/dist/esm/node/httpFetch.js +55 -0
- package/dist/esm/node/index.js +28 -0
- package/dist/esm/node/nodeApp.js +336 -0
- package/dist/esm/node/nodeClient.js +198 -0
- package/dist/esm/node/rateLimit.js +182 -0
- package/dist/esm/node/verifyRecord.js +48 -0
- package/dist/esm/platform/crypto.js +145 -0
- package/dist/esm/platform/crypto.native.js +196 -0
- package/dist/esm/platform/expoTypes.js +23 -0
- package/dist/esm/platform/platform.js +29 -0
- package/dist/esm/secp256k1.js +137 -0
- package/dist/esm/transparency/checkpoint.js +73 -0
- package/dist/esm/transparency/tree.js +189 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/chain/continuity.d.ts +28 -0
- package/dist/types/chain/engine.d.ts +27 -0
- package/dist/types/chain/recordStore.d.ts +85 -0
- package/dist/types/chain/types.d.ts +79 -0
- package/dist/types/chain/verify.d.ts +45 -0
- package/dist/types/envelope/canonicalJson.d.ts +44 -0
- package/dist/types/envelope/recordId.d.ts +30 -0
- package/dist/types/envelope/sign.d.ts +47 -0
- package/dist/types/envelope/signingInput.d.ts +33 -0
- package/dist/types/identity/resolver.d.ts +67 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/node/constants.d.ts +80 -0
- package/dist/types/node/didWebResolver.d.ts +47 -0
- package/dist/types/node/httpFetch.d.ts +60 -0
- package/dist/types/node/index.d.ts +28 -0
- package/dist/types/node/nodeApp.d.ts +120 -0
- package/dist/types/node/nodeClient.d.ts +135 -0
- package/dist/types/node/rateLimit.d.ts +95 -0
- package/dist/types/node/verifyRecord.d.ts +41 -0
- package/dist/types/platform/crypto.d.ts +93 -0
- package/dist/types/platform/crypto.native.d.ts +77 -0
- package/dist/types/platform/expoTypes.d.ts +99 -0
- package/dist/types/platform/platform.d.ts +25 -0
- package/dist/types/secp256k1.d.ts +45 -0
- package/dist/types/transparency/checkpoint.d.ts +71 -0
- package/dist/types/transparency/tree.d.ts +135 -0
- package/package.json +157 -0
- package/src/__tests__/canonicalJson.test.ts +116 -0
- package/src/__tests__/chain.test.ts +279 -0
- package/src/__tests__/didWebResolver.test.ts +132 -0
- package/src/__tests__/envelope.test.ts +267 -0
- package/src/__tests__/nodeApp.test.ts +410 -0
- package/src/__tests__/nodeClient.test.ts +177 -0
- package/src/__tests__/nodeHarness.ts +151 -0
- package/src/__tests__/optionalNativePeers.test.ts +233 -0
- package/src/__tests__/rateLimit.test.ts +268 -0
- package/src/__tests__/runnerGuard.test.ts +85 -0
- package/src/__tests__/secp256k1.test.ts +118 -0
- package/src/__tests__/transparency.test.ts +353 -0
- package/src/chain/continuity.ts +59 -0
- package/src/chain/engine.ts +43 -0
- package/src/chain/recordStore.ts +98 -0
- package/src/chain/types.ts +85 -0
- package/src/chain/verify.ts +102 -0
- package/src/envelope/canonicalJson.ts +120 -0
- package/src/envelope/recordId.ts +63 -0
- package/src/envelope/sign.ts +86 -0
- package/src/envelope/signingInput.ts +48 -0
- package/src/identity/resolver.ts +90 -0
- package/src/index.ts +101 -0
- package/src/node/constants.ts +105 -0
- package/src/node/didWebResolver.ts +162 -0
- package/src/node/httpFetch.ts +88 -0
- package/src/node/index.ts +87 -0
- package/src/node/nodeApp.ts +471 -0
- package/src/node/nodeClient.ts +322 -0
- package/src/node/rateLimit.ts +233 -0
- package/src/node/verifyRecord.ts +60 -0
- package/src/platform/crypto.native.ts +251 -0
- package/src/platform/crypto.ts +172 -0
- package/src/platform/expoTypes.ts +99 -0
- package/src/platform/platform.ts +31 -0
- package/src/secp256k1.ts +207 -0
- package/src/transparency/checkpoint.ts +109 -0
- package/src/transparency/tree.ts +258 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform Detection — runtime predicates.
|
|
4
|
+
*
|
|
5
|
+
* Detects the host runtime WITHOUT importing from 'react-native', so the
|
|
6
|
+
* protocol's crypto modules can be used in web / Node.js / React Native
|
|
7
|
+
* environments without bundlers failing on react-native imports.
|
|
8
|
+
*
|
|
9
|
+
* Only the two predicates the protocol's platform-crypto loaders need live
|
|
10
|
+
* here. Richer platform detection (`getPlatformOS`, `isWeb`, `isNative`, …)
|
|
11
|
+
* is an SDK concern and stays in `@oxy.so/core`.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.isReactNative = isReactNative;
|
|
15
|
+
exports.isNodeJS = isNodeJS;
|
|
16
|
+
/**
|
|
17
|
+
* Check if running in React Native.
|
|
18
|
+
*
|
|
19
|
+
* Selects the React Native crypto variant (`expo-crypto` /
|
|
20
|
+
* `expo-secure-store` / async-storage) over the Node/web variant.
|
|
21
|
+
*/
|
|
22
|
+
function isReactNative() {
|
|
23
|
+
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if running in Node.js.
|
|
27
|
+
*
|
|
28
|
+
* Gates use of Node's built-in `crypto` (the synchronous SHA-256 path and
|
|
29
|
+
* `randomBytes`) and the `await import('node:crypto')` loader.
|
|
30
|
+
*/
|
|
31
|
+
function isNodeJS() {
|
|
32
|
+
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* secp256k1 primitives shared by every Oxy runtime.
|
|
4
|
+
*
|
|
5
|
+
* This module owns the curve binding and the wire formats used across Oxy:
|
|
6
|
+
* 32-byte private keys, compressed or uncompressed SEC1 public keys,
|
|
7
|
+
* RFC 6979 deterministic ECDSA signatures encoded as DER, and the 32-byte
|
|
8
|
+
* ECDH x-coordinate. Callers never receive a library-specific key object.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.normalizeSecp256k1PrivateKey = normalizeSecp256k1PrivateKey;
|
|
12
|
+
exports.isValidSecp256k1PrivateKey = isValidSecp256k1PrivateKey;
|
|
13
|
+
exports.isValidSecp256k1PublicKey = isValidSecp256k1PublicKey;
|
|
14
|
+
exports.generateSecp256k1KeyPair = generateSecp256k1KeyPair;
|
|
15
|
+
exports.deriveSecp256k1PublicKey = deriveSecp256k1PublicKey;
|
|
16
|
+
exports.normalizeSecp256k1PublicKey = normalizeSecp256k1PublicKey;
|
|
17
|
+
exports.signSecp256k1Digest = signSecp256k1Digest;
|
|
18
|
+
exports.verifySecp256k1Digest = verifySecp256k1Digest;
|
|
19
|
+
exports.deriveSecp256k1SharedSecret = deriveSecp256k1SharedSecret;
|
|
20
|
+
const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
|
|
21
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
22
|
+
const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
23
|
+
const UNCOMPRESSED_PUBLIC_KEY = /^04[0-9a-fA-F]{128}$/;
|
|
24
|
+
function bytesToHex(bytes) {
|
|
25
|
+
let result = "";
|
|
26
|
+
for (const byte of bytes) {
|
|
27
|
+
result += byte.toString(16).padStart(2, "0");
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function hexToBytes(value, label) {
|
|
32
|
+
if (value.length === 0 || value.length % 2 !== 0 || !HEX.test(value)) {
|
|
33
|
+
throw new Error(`${label} must be an even-length hexadecimal string`);
|
|
34
|
+
}
|
|
35
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
36
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
37
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Normalize a legacy short/cased scalar to canonical 32-byte lowercase hex.
|
|
43
|
+
* The scalar must be within the secp256k1 order; zero and overflow are rejected.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeSecp256k1PrivateKey(privateKeyHex) {
|
|
46
|
+
if (typeof privateKeyHex !== "string" ||
|
|
47
|
+
privateKeyHex.length === 0 ||
|
|
48
|
+
privateKeyHex.length > 64 ||
|
|
49
|
+
!HEX.test(privateKeyHex)) {
|
|
50
|
+
throw new Error("secp256k1 private key must contain 1 to 64 hexadecimal characters");
|
|
51
|
+
}
|
|
52
|
+
const normalized = privateKeyHex.toLowerCase().padStart(64, "0");
|
|
53
|
+
const privateKey = hexToBytes(normalized, "secp256k1 private key");
|
|
54
|
+
if (!secp256k1_js_1.secp256k1.utils.isValidSecretKey(privateKey)) {
|
|
55
|
+
throw new Error("secp256k1 private key is outside the valid scalar range");
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
/** True when a value is a valid secp256k1 scalar (legacy short hex accepted). */
|
|
60
|
+
function isValidSecp256k1PrivateKey(privateKeyHex) {
|
|
61
|
+
try {
|
|
62
|
+
normalizeSecp256k1PrivateKey(privateKeyHex);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function parsePrivateKey(privateKeyHex) {
|
|
70
|
+
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), "secp256k1 private key");
|
|
71
|
+
}
|
|
72
|
+
function parsePublicKey(publicKeyHex) {
|
|
73
|
+
if (typeof publicKeyHex !== "string" ||
|
|
74
|
+
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
|
|
75
|
+
!UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
|
|
76
|
+
throw new Error("secp256k1 public key must be a compressed or uncompressed SEC1 hex key");
|
|
77
|
+
}
|
|
78
|
+
const publicKey = hexToBytes(publicKeyHex, "secp256k1 public key");
|
|
79
|
+
// Parsing validates the SEC1 prefix, coordinate range, and curve equation.
|
|
80
|
+
secp256k1_js_1.secp256k1.Point.fromBytes(publicKey);
|
|
81
|
+
return publicKey;
|
|
82
|
+
}
|
|
83
|
+
function parseDigest(digestHex) {
|
|
84
|
+
if (typeof digestHex !== "string" || digestHex.length !== 64) {
|
|
85
|
+
throw new Error("secp256k1 digest must be exactly 32 bytes of hexadecimal data");
|
|
86
|
+
}
|
|
87
|
+
return hexToBytes(digestHex, "secp256k1 digest");
|
|
88
|
+
}
|
|
89
|
+
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
90
|
+
function isValidSecp256k1PublicKey(publicKeyHex) {
|
|
91
|
+
try {
|
|
92
|
+
parsePublicKey(publicKeyHex);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Generate a canonical private key and its uncompressed public key. */
|
|
100
|
+
function generateSecp256k1KeyPair() {
|
|
101
|
+
const privateKey = secp256k1_js_1.secp256k1.utils.randomSecretKey();
|
|
102
|
+
return {
|
|
103
|
+
privateKey: bytesToHex(privateKey),
|
|
104
|
+
publicKey: bytesToHex(secp256k1_js_1.secp256k1.getPublicKey(privateKey, false)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Derive a compressed or uncompressed SEC1 public key from a private scalar. */
|
|
108
|
+
function deriveSecp256k1PublicKey(privateKeyHex, compressed = false) {
|
|
109
|
+
return bytesToHex(secp256k1_js_1.secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed));
|
|
110
|
+
}
|
|
111
|
+
/** Parse and re-encode a public key in canonical lowercase SEC1 form. */
|
|
112
|
+
function normalizeSecp256k1PublicKey(publicKeyHex, compressed = false) {
|
|
113
|
+
const point = secp256k1_js_1.secp256k1.Point.fromBytes(parsePublicKey(publicKeyHex));
|
|
114
|
+
return point.toHex(compressed);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Sign a 32-byte digest with deterministic RFC 6979 ECDSA and return DER hex.
|
|
118
|
+
*
|
|
119
|
+
* `lowS` defaults to false because historical Oxy signatures used elliptic's
|
|
120
|
+
* default and therefore may occupy either half of the curve order. Verification
|
|
121
|
+
* accepts both forms; callers that require low-S normalization opt in explicitly.
|
|
122
|
+
*/
|
|
123
|
+
function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
|
|
124
|
+
const signature = secp256k1_js_1.secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
|
|
125
|
+
lowS: options.lowS ?? false,
|
|
126
|
+
});
|
|
127
|
+
return signature.toHex("der");
|
|
128
|
+
}
|
|
129
|
+
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
130
|
+
function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
|
|
131
|
+
const publicKey = parsePublicKey(publicKeyHex);
|
|
132
|
+
const digest = parseDigest(digestHex);
|
|
133
|
+
const signature = hexToBytes(signatureDerHex, "secp256k1 DER signature");
|
|
134
|
+
// Parse once up front so malformed or non-DER input is rejected explicitly.
|
|
135
|
+
secp256k1_js_1.secp256k1.Signature.fromBytes(signature, "der");
|
|
136
|
+
return secp256k1_js_1.secp256k1.verify(signature, digest, publicKey, {
|
|
137
|
+
format: "der",
|
|
138
|
+
lowS: false,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
142
|
+
function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
|
|
143
|
+
const encodedPoint = secp256k1_js_1.secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);
|
|
144
|
+
if (encodedPoint.length !== 33) {
|
|
145
|
+
throw new Error("secp256k1 ECDH returned an unexpected point encoding");
|
|
146
|
+
}
|
|
147
|
+
return encodedPoint.slice(1);
|
|
148
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transparency checkpoint — the signed, hash-linked commitment to a tree root.
|
|
4
|
+
*
|
|
5
|
+
* A checkpoint is what the operator PUBLISHES: "at `periodEnd` I committed to
|
|
6
|
+
* `root` over `treeSize` subjects, and the previous checkpoint hashed to
|
|
7
|
+
* `prevCheckpointHash`". The `prevCheckpointHash` link makes the checkpoint
|
|
8
|
+
* sequence itself append-only: once any checkpoint is anchored publicly, none of
|
|
9
|
+
* its ancestors can be rewritten without breaking the chain of hashes.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the signature covers ONLY the five fields
|
|
12
|
+
*
|
|
13
|
+
* The signing input is derived from exactly `{index, periodEnd, treeSize, root,
|
|
14
|
+
* prevCheckpointHash}` — never from the surrounding storage document, and never
|
|
15
|
+
* from other signatures. That is what makes a checkpoint CO-SIGNABLE: the
|
|
16
|
+
* operator and any number of independent witnesses (e.g. user-run
|
|
17
|
+
* `@oxy.so/node` deployments) each sign the identical bytes with their own key,
|
|
18
|
+
* with zero coordination and in any order. Two conflicting roots for one
|
|
19
|
+
* `index`, each carrying valid signatures, is then transferable proof of
|
|
20
|
+
* equivocation that needs no cooperation from the operator to demonstrate.
|
|
21
|
+
*
|
|
22
|
+
* Extra fields on the object passed in are ignored by design, so handing this a
|
|
23
|
+
* database document cannot change what was signed.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.checkpointSigningInput = checkpointSigningInput;
|
|
27
|
+
exports.checkpointHash = checkpointHash;
|
|
28
|
+
exports.signCheckpoint = signCheckpoint;
|
|
29
|
+
exports.verifyCheckpointSignature = verifyCheckpointSignature;
|
|
30
|
+
const canonicalJson_1 = require("../envelope/canonicalJson");
|
|
31
|
+
const recordId_1 = require("../envelope/recordId");
|
|
32
|
+
const sign_1 = require("../envelope/sign");
|
|
33
|
+
const secp256k1_1 = require("../secp256k1");
|
|
34
|
+
/** Domain prefix for checkpoint signing bytes. */
|
|
35
|
+
const CHECKPOINT_PREFIX = 'oxy.transparency.checkpoint.v1:';
|
|
36
|
+
/** The one signature algorithm the protocol emits. */
|
|
37
|
+
const ALG = 'ES256K-DER-SHA256';
|
|
38
|
+
/**
|
|
39
|
+
* The exact bytes every co-signer signs: the canonical JSON of the five signed
|
|
40
|
+
* fields under the checkpoint domain prefix. Key order in the input object is
|
|
41
|
+
* irrelevant; extra properties are dropped.
|
|
42
|
+
*/
|
|
43
|
+
function checkpointSigningInput(fields) {
|
|
44
|
+
return `${CHECKPOINT_PREFIX}${(0, canonicalJson_1.canonicalize)({
|
|
45
|
+
index: fields.index,
|
|
46
|
+
periodEnd: fields.periodEnd,
|
|
47
|
+
treeSize: fields.treeSize,
|
|
48
|
+
root: fields.root,
|
|
49
|
+
prevCheckpointHash: fields.prevCheckpointHash,
|
|
50
|
+
})}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The content address of a checkpoint — what the NEXT checkpoint's
|
|
54
|
+
* `prevCheckpointHash` references.
|
|
55
|
+
*/
|
|
56
|
+
async function checkpointHash(fields) {
|
|
57
|
+
return (0, recordId_1.sha256)(checkpointSigningInput(fields));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Sign a checkpoint with an explicit private key. Used by the operator and,
|
|
61
|
+
* identically, by every witness that co-signs the same checkpoint.
|
|
62
|
+
*/
|
|
63
|
+
async function signCheckpoint(fields, privateKeyHex) {
|
|
64
|
+
return {
|
|
65
|
+
publicKey: (0, secp256k1_1.deriveSecp256k1PublicKey)(privateKeyHex),
|
|
66
|
+
alg: ALG,
|
|
67
|
+
signature: await (0, sign_1.signMessage)(checkpointSigningInput(fields), privateKeyHex),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify one signature over a checkpoint's signed fields.
|
|
72
|
+
*
|
|
73
|
+
* Confirms the signature matches the embedded `publicKey` for exactly these
|
|
74
|
+
* fields. It does NOT establish that the key belongs to a trusted operator or an
|
|
75
|
+
* accepted witness — that policy lives with the verifier's key list.
|
|
76
|
+
*/
|
|
77
|
+
async function verifyCheckpointSignature(fields, signature) {
|
|
78
|
+
return (0, sign_1.verifySignature)(checkpointSigningInput(fields), signature.signature, signature.publicKey);
|
|
79
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transparency Merkle tree — leaf hashing, root computation, and inclusion
|
|
4
|
+
* proofs over a snapshot of chain heads.
|
|
5
|
+
*
|
|
6
|
+
* ## What this is for
|
|
7
|
+
*
|
|
8
|
+
* A per-subject hash chain proves nobody edited a record. It does NOT prove the
|
|
9
|
+
* SERVER didn't serve two different histories to two parties (equivocation), or
|
|
10
|
+
* quietly drop a record. That gap is closed by committing to every subject's
|
|
11
|
+
* chain head at once, publishing the commitment, and letting anyone verify that
|
|
12
|
+
* their own head is inside it — which is exactly what this tree does. The
|
|
13
|
+
* commitment (the root) is what gets signed into a checkpoint (`./checkpoint`)
|
|
14
|
+
* and anchored on a public chain.
|
|
15
|
+
*
|
|
16
|
+
* ## Shape
|
|
17
|
+
*
|
|
18
|
+
* The tree follows RFC 6962 (Certificate Transparency) so the algorithms are
|
|
19
|
+
* standard and independently reimplementable: leaves and interior nodes are
|
|
20
|
+
* hashed under DISTINCT domain prefixes (a leaf hash can never be reinterpreted
|
|
21
|
+
* as an interior node), an odd level splits so the LEFT subtree is the largest
|
|
22
|
+
* power of two below the size, and a single-leaf tree's root is the leaf itself.
|
|
23
|
+
*
|
|
24
|
+
* A built tree keeps every LEVEL, not just its leaves, because that is what
|
|
25
|
+
* makes serving proofs cheap: {@link inclusionProof} reads siblings straight out
|
|
26
|
+
* of the levels instead of re-hashing subtrees, so one build amortizes over all
|
|
27
|
+
* the proofs cut from it (a checkpoint is built once and proved thousands of
|
|
28
|
+
* times, once per subject that audits it).
|
|
29
|
+
*
|
|
30
|
+
* Crucially, {@link verifyInclusionProof} needs only the verifier's OWN leaf,
|
|
31
|
+
* its index, the tree size, the audit path, and the signed root — never the
|
|
32
|
+
* other leaves. So a device or node can audit its own history against a
|
|
33
|
+
* published checkpoint without downloading anyone else's data.
|
|
34
|
+
*
|
|
35
|
+
* The leaf order is fixed by {@link buildTransparencyTreeFromHeads}: ascending
|
|
36
|
+
* by `subjectDid` in UTF-16 code-unit order. Order is part of the commitment, so
|
|
37
|
+
* every implementation MUST sort identically or the roots diverge.
|
|
38
|
+
*/
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.EMPTY_TRANSPARENCY_ROOT = void 0;
|
|
41
|
+
exports.transparencyLeafHash = transparencyLeafHash;
|
|
42
|
+
exports.buildTransparencyTree = buildTransparencyTree;
|
|
43
|
+
exports.buildTransparencyTreeFromHeads = buildTransparencyTreeFromHeads;
|
|
44
|
+
exports.inclusionProof = inclusionProof;
|
|
45
|
+
exports.verifyInclusionProof = verifyInclusionProof;
|
|
46
|
+
const recordId_1 = require("../envelope/recordId");
|
|
47
|
+
const canonicalJson_1 = require("../envelope/canonicalJson");
|
|
48
|
+
/** Domain prefix for a leaf hash. Distinct from {@link NODE_PREFIX}. */
|
|
49
|
+
const LEAF_PREFIX = 'oxy.transparency.leaf.v1:';
|
|
50
|
+
/** Domain prefix for an interior node hash. Distinct from {@link LEAF_PREFIX}. */
|
|
51
|
+
const NODE_PREFIX = 'oxy.transparency.node.v1:';
|
|
52
|
+
/** The pre-image of {@link EMPTY_TRANSPARENCY_ROOT}. */
|
|
53
|
+
const EMPTY_PREIMAGE = 'oxy.transparency.empty.v1';
|
|
54
|
+
/**
|
|
55
|
+
* The root of a tree with zero leaves: `sha256("oxy.transparency.empty.v1")`.
|
|
56
|
+
*
|
|
57
|
+
* Hard-coded because hashing is async and this is needed as a value; a Jest
|
|
58
|
+
* regression test pins it against the live hash of {@link EMPTY_PREIMAGE}.
|
|
59
|
+
*/
|
|
60
|
+
exports.EMPTY_TRANSPARENCY_ROOT = '315338df4bc34de7d057b583a082268016888323c79c0376586406a64f441b1e';
|
|
61
|
+
/**
|
|
62
|
+
* Hash one subject's head into a leaf.
|
|
63
|
+
*
|
|
64
|
+
* The pre-image is the canonical JSON of the three committed fields under the
|
|
65
|
+
* leaf domain prefix — so a DID containing a delimiter-like character cannot
|
|
66
|
+
* forge another subject's leaf (JSON escaping makes the encoding unambiguous),
|
|
67
|
+
* and a leaf can never collide with an interior node.
|
|
68
|
+
*/
|
|
69
|
+
async function transparencyLeafHash(entry) {
|
|
70
|
+
const preimage = (0, canonicalJson_1.canonicalize)({
|
|
71
|
+
subjectDid: entry.subjectDid,
|
|
72
|
+
seq: entry.seq,
|
|
73
|
+
headRecordId: entry.headRecordId,
|
|
74
|
+
});
|
|
75
|
+
return (0, recordId_1.sha256)(`${LEAF_PREFIX}${preimage}`);
|
|
76
|
+
}
|
|
77
|
+
/** Hash an interior node over its ordered children. */
|
|
78
|
+
async function nodeHash(left, right) {
|
|
79
|
+
return (0, recordId_1.sha256)(`${NODE_PREFIX}${left}${right}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build a tree over already-hashed leaves, in the given order.
|
|
83
|
+
*
|
|
84
|
+
* Levels are built bottom-up, each one pairing the level below and carrying a
|
|
85
|
+
* trailing odd node up unchanged — which yields exactly RFC 6962's tree, whose
|
|
86
|
+
* recursive definition splits at the largest power of two below the size.
|
|
87
|
+
* `transparency.test.ts` pins that equivalence against a direct transcription of
|
|
88
|
+
* the RFC's MTH for every size up to 40, so the shape can never silently drift.
|
|
89
|
+
*
|
|
90
|
+
* The order IS part of the commitment — prefer
|
|
91
|
+
* {@link buildTransparencyTreeFromHeads}, which owns the canonical ordering.
|
|
92
|
+
*/
|
|
93
|
+
async function buildTransparencyTree(leaves) {
|
|
94
|
+
const levels = [[...leaves]];
|
|
95
|
+
let current = levels[0];
|
|
96
|
+
while (current.length > 1) {
|
|
97
|
+
const pairCount = Math.floor(current.length / 2);
|
|
98
|
+
const parents = await Promise.all(Array.from({ length: pairCount }, (_unused, pair) => nodeHash(current[pair * 2], current[pair * 2 + 1])));
|
|
99
|
+
if (current.length % 2 === 1) {
|
|
100
|
+
parents.push(current[current.length - 1]);
|
|
101
|
+
}
|
|
102
|
+
levels.push(parents);
|
|
103
|
+
current = parents;
|
|
104
|
+
}
|
|
105
|
+
const top = levels[levels.length - 1];
|
|
106
|
+
return {
|
|
107
|
+
root: top.length === 1 ? top[0] : exports.EMPTY_TRANSPARENCY_ROOT,
|
|
108
|
+
treeSize: leaves.length,
|
|
109
|
+
levels,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the canonical tree for a snapshot of chain heads.
|
|
114
|
+
*
|
|
115
|
+
* Sorts ascending by `subjectDid` in UTF-16 code-unit order — NEVER
|
|
116
|
+
* `localeCompare`, whose order is locale-dependent and would make two
|
|
117
|
+
* verifiers compute different roots from identical data.
|
|
118
|
+
*
|
|
119
|
+
* Throws on a duplicate `subjectDid`: a snapshot must commit to exactly one head
|
|
120
|
+
* per subject, and silently keeping one of two would hide the other's history.
|
|
121
|
+
*/
|
|
122
|
+
async function buildTransparencyTreeFromHeads(entries) {
|
|
123
|
+
const ordered = [...entries].sort((a, b) => a.subjectDid < b.subjectDid ? -1 : a.subjectDid > b.subjectDid ? 1 : 0);
|
|
124
|
+
const indexBySubject = {};
|
|
125
|
+
ordered.forEach((entry, index) => {
|
|
126
|
+
if (indexBySubject[entry.subjectDid] !== undefined) {
|
|
127
|
+
throw new Error(`Duplicate subject in transparency snapshot: ${entry.subjectDid}`);
|
|
128
|
+
}
|
|
129
|
+
indexBySubject[entry.subjectDid] = index;
|
|
130
|
+
});
|
|
131
|
+
const leaves = await Promise.all(ordered.map(transparencyLeafHash));
|
|
132
|
+
return { ...(await buildTransparencyTree(leaves)), indexBySubject };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The audit path proving `index` is committed in `tree` (RFC 6962 PATH): each
|
|
136
|
+
* step is the sibling subtree hash, leaf-adjacent first.
|
|
137
|
+
*
|
|
138
|
+
* Pure index arithmetic over the tree's levels — no hashing, so cutting a proof
|
|
139
|
+
* costs O(log n) array reads however large the checkpoint is.
|
|
140
|
+
*/
|
|
141
|
+
function inclusionProof(tree, index) {
|
|
142
|
+
if (!Number.isInteger(index) || index < 0 || index >= tree.treeSize) {
|
|
143
|
+
throw new Error(`Leaf index ${index} is outside a tree of ${tree.treeSize} leaves`);
|
|
144
|
+
}
|
|
145
|
+
const proof = [];
|
|
146
|
+
let position = index;
|
|
147
|
+
for (let level = 0; level < tree.levels.length - 1; level += 1) {
|
|
148
|
+
const nodes = tree.levels[level];
|
|
149
|
+
const sibling = position % 2 === 0 ? position + 1 : position - 1;
|
|
150
|
+
// A trailing odd node is carried up unpaired, so it contributes no step.
|
|
151
|
+
if (sibling < nodes.length) {
|
|
152
|
+
proof.push(nodes[sibling]);
|
|
153
|
+
}
|
|
154
|
+
position = Math.floor(position / 2);
|
|
155
|
+
}
|
|
156
|
+
return proof;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify an audit path against a committed root (RFC 6962 §2.1.1).
|
|
160
|
+
*
|
|
161
|
+
* Recomputes the root from the leaf upward using only the path, so the verifier
|
|
162
|
+
* never needs another subject's data. Returns `false` for every failure mode —
|
|
163
|
+
* tampered leaf, replayed index, truncated path, foreign root — rather than
|
|
164
|
+
* throwing, so callers treat auditing as a boolean.
|
|
165
|
+
*/
|
|
166
|
+
async function verifyInclusionProof(check) {
|
|
167
|
+
const { leaf, index, treeSize, proof, root } = check;
|
|
168
|
+
if (!Number.isInteger(index) || !Number.isInteger(treeSize)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (index < 0 || treeSize <= 0 || index >= treeSize) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
let fn = index;
|
|
175
|
+
let sn = treeSize - 1;
|
|
176
|
+
let computed = leaf;
|
|
177
|
+
for (const sibling of proof) {
|
|
178
|
+
if (sn === 0) {
|
|
179
|
+
// The path claims more levels than the tree has.
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
183
|
+
computed = await nodeHash(sibling, computed);
|
|
184
|
+
while (fn % 2 === 0 && fn !== 0) {
|
|
185
|
+
fn = Math.floor(fn / 2);
|
|
186
|
+
sn = Math.floor(sn / 2);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
computed = await nodeHash(computed, sibling);
|
|
191
|
+
}
|
|
192
|
+
fn = Math.floor(fn / 2);
|
|
193
|
+
sn = Math.floor(sn / 2);
|
|
194
|
+
}
|
|
195
|
+
// `sn > 0` means the path was truncated before reaching the root.
|
|
196
|
+
return sn === 0 && computed === root;
|
|
197
|
+
}
|