@hcs-witness/verify 0.0.0 → 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/dist/check.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Évaluateur JSON Schema minimal : couvre exactement les constructions
3
+ * des schémas exportés (type, const, anyOf, properties, required,
4
+ * additionalProperties, patternProperties, items, pattern, minLength,
5
+ * minimum). Ce n'est PAS un moteur JSON Schema général — les schémas
6
+ * sont fixes et internes au format de preuve HCS Witness.
7
+ */
8
+ const regexCache = new Map();
9
+ function regex(pattern) {
10
+ let r = regexCache.get(pattern);
11
+ if (r === undefined) {
12
+ r = new RegExp(pattern);
13
+ regexCache.set(pattern, r);
14
+ }
15
+ return r;
16
+ }
17
+ function isObject(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function checkType(type, value) {
21
+ switch (type) {
22
+ case "object":
23
+ return isObject(value);
24
+ case "array":
25
+ return Array.isArray(value);
26
+ case "string":
27
+ return typeof value === "string";
28
+ case "integer":
29
+ return typeof value === "number" && Number.isInteger(value);
30
+ case "number":
31
+ return typeof value === "number";
32
+ case "boolean":
33
+ return typeof value === "boolean";
34
+ case "null":
35
+ return value === null;
36
+ default:
37
+ return false;
38
+ }
39
+ }
40
+ export function checkSchema(schema, value) {
41
+ if (schema.anyOf !== undefined) {
42
+ return schema.anyOf.some((s) => checkSchema(s, value));
43
+ }
44
+ if (schema.const !== undefined && value !== schema.const) {
45
+ return false;
46
+ }
47
+ if (schema.type !== undefined && !checkType(schema.type, value)) {
48
+ return false;
49
+ }
50
+ if (typeof value === "string") {
51
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
52
+ return false;
53
+ }
54
+ if (schema.pattern !== undefined && !regex(schema.pattern).test(value)) {
55
+ return false;
56
+ }
57
+ }
58
+ if (typeof value === "number") {
59
+ if (schema.minimum !== undefined && value < schema.minimum) {
60
+ return false;
61
+ }
62
+ }
63
+ if (Array.isArray(value)) {
64
+ const items = schema.items;
65
+ if (items !== undefined &&
66
+ !value.every((item) => checkSchema(items, item))) {
67
+ return false;
68
+ }
69
+ }
70
+ if (isObject(value)) {
71
+ for (const key of schema.required ?? []) {
72
+ if (!(key in value))
73
+ return false;
74
+ }
75
+ const props = schema.properties ?? {};
76
+ const patterns = Object.entries(schema.patternProperties ?? {}).map(([p, s]) => ({ r: regex(p), s }));
77
+ const strict = schema.additionalProperties === false;
78
+ for (const [key, member] of Object.entries(value)) {
79
+ const known = Object.hasOwn(props, key);
80
+ let matched = false;
81
+ if (known) {
82
+ const sub = props[key];
83
+ if (sub !== undefined && !checkSchema(sub, member)) {
84
+ return false;
85
+ }
86
+ }
87
+ for (const { r, s } of patterns) {
88
+ if (r.test(key)) {
89
+ matched = true;
90
+ if (!checkSchema(s, member))
91
+ return false;
92
+ }
93
+ }
94
+ if (!known && !matched && strict) {
95
+ return false;
96
+ }
97
+ }
98
+ }
99
+ return true;
100
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { verifyAnchor } from "./anchor.js";
4
+ import { verifyProof } from "./verify.js";
5
+ const USAGE = "Usage: hcs-witness-verify <proof.json> --keys <keys.json> [--now <rfc3339>] [--json]";
6
+ function parseArgs(args) {
7
+ let proof;
8
+ let keys;
9
+ let now;
10
+ let json = false;
11
+ for (let i = 0; i < args.length; i += 1) {
12
+ const a = args.at(i);
13
+ if (a === "--keys") {
14
+ keys = args.at(i + 1);
15
+ i += 1;
16
+ }
17
+ else if (a === "--now") {
18
+ const raw = args.at(i + 1) ?? "";
19
+ const t = Date.parse(raw);
20
+ if (Number.isNaN(t))
21
+ throw new Error("invalid --now value");
22
+ now = new Date(t);
23
+ i += 1;
24
+ }
25
+ else if (a === "--json") {
26
+ json = true;
27
+ }
28
+ else if (a?.startsWith("-")) {
29
+ throw new Error(`unknown option: ${a}`);
30
+ }
31
+ else if (proof === undefined) {
32
+ proof = a;
33
+ }
34
+ else {
35
+ throw new Error("unexpected extra argument");
36
+ }
37
+ }
38
+ if (proof === undefined || keys === undefined) {
39
+ throw new Error("missing arguments");
40
+ }
41
+ const out = {
42
+ proof,
43
+ keys,
44
+ json,
45
+ };
46
+ if (now !== undefined)
47
+ out.now = now;
48
+ return out;
49
+ }
50
+ function loadJson(path) {
51
+ return JSON.parse(readFileSync(path, "utf8"));
52
+ }
53
+ try {
54
+ const opts = parseArgs(process.argv.slice(2));
55
+ const proof = loadJson(opts.proof);
56
+ const keys = loadJson(opts.keys);
57
+ const result = verifyProof(proof, keys, opts.now === undefined ? undefined : { now: opts.now });
58
+ if (opts.json) {
59
+ process.stdout.write(`${JSON.stringify(result)}\n`);
60
+ }
61
+ else {
62
+ const expired = result.errors.includes("PROOF_EXPIRED");
63
+ const authErrors = result.errors.filter((e) => e !== "PROOF_EXPIRED");
64
+ process.stdout.write(`proof_id: ${result.proof_id ?? "unknown"}\n`);
65
+ if (result.valid) {
66
+ process.stdout.write("status: VALID\n");
67
+ }
68
+ else {
69
+ process.stdout.write(`status: INVALID (${authErrors.join(", ") || "see errors"})\n`);
70
+ }
71
+ if (expired) {
72
+ process.stdout.write("note: proof is EXPIRED (signature authenticity intact)\n");
73
+ }
74
+ // Bloc presence (étape 5) : affiché pour information, JAMAIS
75
+ // utilisé pour la validité cryptographique.
76
+ const presence = proof.presence;
77
+ if (presence !== undefined && presence !== null) {
78
+ const p = presence;
79
+ const synced = (p.flags ?? []).includes("synced_passkey");
80
+ process.stdout.write(`presence: measured, not yet rated (challenge: ${p.challenge?.kind ?? "unknown"}${synced ? ", synced passkey" : ""})
81
+ `);
82
+ }
83
+ const anchor = verifyAnchor(proof);
84
+ if (anchor.present) {
85
+ process.stdout.write(`anchor: merkle inclusion ${anchor.inclusion ? "OK" : "KO"}\n`);
86
+ if (anchor.otsMatchesRoot === false) {
87
+ process.stdout.write("anchor: ots file does NOT attest this root\n");
88
+ }
89
+ if (anchor.bitcoinBlockHeight !== undefined) {
90
+ process.stdout.write(`anchor: bitcoin block ${String(anchor.bitcoinBlockHeight)} (attested by the .ots file)\n`);
91
+ }
92
+ else if (anchor.pendingCalendars.length > 0) {
93
+ process.stdout.write(`anchor: pending (${anchor.pendingCalendars.join(", ")})\n`);
94
+ }
95
+ process.stdout.write("note: block confirmation is to be done with a node or an explorer\n");
96
+ }
97
+ }
98
+ process.exit(result.valid ? 0 : 1);
99
+ }
100
+ catch (error) {
101
+ process.stdout.write(`${error instanceof Error ? error.message : String(error)}\n${USAGE}\n`);
102
+ process.exit(2);
103
+ }
package/dist/hash.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** SHA-256 via node:crypto — retourne un Uint8Array (Buffer). */
2
+ export declare function sha256(data: Uint8Array): Uint8Array;
3
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAEnD"}
package/dist/hash.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ /** SHA-256 via node:crypto — retourne un Uint8Array (Buffer). */
3
+ export function sha256(data) {
4
+ return createHash("sha256").update(data).digest();
5
+ }
@@ -0,0 +1,11 @@
1
+ export { canonicalBytes, NonJsonValueError } from "./jcs.js";
2
+ export { bytesToB64url, b64urlToBytes } from "./base64.js";
3
+ export { checkSchema, type JsonSchema } from "./check.js";
4
+ export { keysetSchema, isKeyset, type Keyset, type KeysetKey, type Es256KeysetKey, type MlDsaKeysetKey, } from "./keyset.js";
5
+ export { verifyProof, ML_DSA_CONTEXT, type VerifyResult, type VerifyErrorCode, type VerifyOptions, } from "./verify.js";
6
+ export { proofSchema, unsignedProofSchema, isProof, isUnsignedProof, signedPayload, type Proof, type Presence, type PresenceChallenge, type ProofAnchor, type ProofSignature, type UnsignedProof, } from "./schema.js";
7
+ export { checkActionBinding, type BindingCheck, type BindingErrorCode, type BindingResult, } from "./binding.js";
8
+ export { merkleLeafHash, merkleNodeHash, merkleRoot, merklePath, verifyMerkleInclusion, } from "./merkle.js";
9
+ export { OtsError, parseOtsFile, serializeOtsFile, serializeTimestamp, allAttestations, bitcoinBlockHeight, type OtsFile, type OtsOp, type OtsAttestation, type OtsTimestamp, } from "./ots.js";
10
+ export { verifyAnchor, type AnchorResult } from "./anchor.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,OAAO,EACP,eAAe,EACf,aAAa,EACb,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,cAAc,EACd,UAAU,EACV,UAAU,EACV,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { canonicalBytes, NonJsonValueError } from "./jcs.js";
2
+ export { bytesToB64url, b64urlToBytes } from "./base64.js";
3
+ export { checkSchema } from "./check.js";
4
+ export { keysetSchema, isKeyset, } from "./keyset.js";
5
+ export { verifyProof, ML_DSA_CONTEXT, } from "./verify.js";
6
+ export { proofSchema, unsignedProofSchema, isProof, isUnsignedProof, signedPayload, } from "./schema.js";
7
+ export { checkActionBinding, } from "./binding.js";
8
+ export { merkleLeafHash, merkleNodeHash, merkleRoot, merklePath, verifyMerkleInclusion, } from "./merkle.js";
9
+ export { OtsError, parseOtsFile, serializeOtsFile, serializeTimestamp, allAttestations, bitcoinBlockHeight, } from "./ots.js";
10
+ export { verifyAnchor } from "./anchor.js";
package/dist/jcs.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare class NonJsonValueError extends Error {
2
+ constructor(path: string, reason: string);
3
+ }
4
+ export declare function canonicalBytes(value: unknown): Uint8Array;
5
+ //# sourceMappingURL=jcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jcs.d.ts","sourceRoot":"","sources":["../src/jcs.ts"],"names":[],"mappings":"AAEA,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIzC;AAmDD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAOzD"}
package/dist/jcs.js ADDED
@@ -0,0 +1,60 @@
1
+ import canonicalize from "canonicalize";
2
+ export class NonJsonValueError extends Error {
3
+ constructor(path, reason) {
4
+ super(`non-JSON value at ${path}: ${reason}`);
5
+ this.name = "NonJsonValueError";
6
+ }
7
+ }
8
+ function assertJsonValue(value, path, seen) {
9
+ if (value === undefined) {
10
+ throw new NonJsonValueError(path, "undefined");
11
+ }
12
+ if (value === null) {
13
+ return;
14
+ }
15
+ switch (typeof value) {
16
+ case "boolean":
17
+ case "string":
18
+ return;
19
+ case "number":
20
+ if (!Number.isFinite(value)) {
21
+ throw new NonJsonValueError(path, "non-finite number");
22
+ }
23
+ return;
24
+ case "object":
25
+ break;
26
+ default:
27
+ throw new NonJsonValueError(path, typeof value);
28
+ }
29
+ const obj = value;
30
+ if (seen.has(obj)) {
31
+ throw new NonJsonValueError(path, "cyclic reference");
32
+ }
33
+ seen.add(obj);
34
+ try {
35
+ if (Array.isArray(obj)) {
36
+ obj.forEach((item, index) => {
37
+ assertJsonValue(item, `${path}[${String(index)}]`, seen);
38
+ });
39
+ return;
40
+ }
41
+ const proto = Object.getPrototypeOf(obj);
42
+ if (proto !== Object.prototype && proto !== null) {
43
+ throw new NonJsonValueError(path, "non-plain object");
44
+ }
45
+ for (const [key, member] of Object.entries(obj)) {
46
+ assertJsonValue(member, `${path}.${key}`, seen);
47
+ }
48
+ }
49
+ finally {
50
+ seen.delete(obj);
51
+ }
52
+ }
53
+ export function canonicalBytes(value) {
54
+ assertJsonValue(value, "$", new Set());
55
+ const out = canonicalize(value);
56
+ if (out === undefined) {
57
+ throw new NonJsonValueError("$", "canonicalize returned undefined");
58
+ }
59
+ return new TextEncoder().encode(out);
60
+ }
@@ -0,0 +1,30 @@
1
+ import { type JsonSchema } from "./check.js";
2
+ /** Schéma JSON du jeu de clés publiques (/.well-known/hcs-keys.json). */
3
+ export declare const keysetSchema: JsonSchema;
4
+ interface KeysetKeyBase {
5
+ use: "sig";
6
+ kid: string;
7
+ status: "active" | "retired";
8
+ not_before: string;
9
+ not_after?: string;
10
+ }
11
+ export interface Es256KeysetKey extends KeysetKeyBase {
12
+ kty: "EC";
13
+ crv: "P-256";
14
+ x: string;
15
+ y: string;
16
+ alg: "ES256";
17
+ }
18
+ export interface MlDsaKeysetKey extends KeysetKeyBase {
19
+ kty: "AKP";
20
+ alg: "ML-DSA-65";
21
+ pub: string;
22
+ }
23
+ export type KeysetKey = Es256KeysetKey | MlDsaKeysetKey;
24
+ export interface Keyset {
25
+ _comment?: string;
26
+ keys: KeysetKey[];
27
+ }
28
+ export declare function isKeyset(value: unknown): value is Keyset;
29
+ export {};
30
+ //# sourceMappingURL=keyset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyset.d.ts","sourceRoot":"","sources":["../src/keyset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAuB1D,yEAAyE;AACzE,eAAO,MAAM,YAAY,EAAE,UAsC1B,CAAC;AAEF,UAAU,aAAa;IACrB,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,EAAE,OAAO,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,cAAc,CAAC;AAExD,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD"}
package/dist/keyset.js ADDED
@@ -0,0 +1,62 @@
1
+ import { checkSchema } from "./check.js";
2
+ const b64url = { type: "string", pattern: "^[A-Za-z0-9_-]+$" };
3
+ const rfc3339Utc = {
4
+ type: "string",
5
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$",
6
+ };
7
+ const keyBase = {
8
+ use: { const: "sig", type: "string" },
9
+ kid: { type: "string", minLength: 1 },
10
+ status: {
11
+ anyOf: [
12
+ { const: "active", type: "string" },
13
+ { const: "retired", type: "string" },
14
+ ],
15
+ },
16
+ not_before: rfc3339Utc,
17
+ not_after: rfc3339Utc,
18
+ };
19
+ const keyBaseRequired = ["use", "kid", "status", "not_before"];
20
+ /** Schéma JSON du jeu de clés publiques (/.well-known/hcs-keys.json). */
21
+ export const keysetSchema = {
22
+ type: "object",
23
+ required: ["keys"],
24
+ additionalProperties: false,
25
+ properties: {
26
+ _comment: { type: "string" },
27
+ keys: {
28
+ type: "array",
29
+ items: {
30
+ anyOf: [
31
+ {
32
+ type: "object",
33
+ required: ["kty", "crv", "x", "y", "alg", ...keyBaseRequired],
34
+ additionalProperties: false,
35
+ properties: {
36
+ kty: { const: "EC", type: "string" },
37
+ crv: { const: "P-256", type: "string" },
38
+ x: b64url,
39
+ y: b64url,
40
+ alg: { const: "ES256", type: "string" },
41
+ ...keyBase,
42
+ },
43
+ },
44
+ {
45
+ type: "object",
46
+ required: ["kty", "alg", "pub", ...keyBaseRequired],
47
+ additionalProperties: false,
48
+ properties: {
49
+ kty: { const: "AKP", type: "string" },
50
+ alg: { const: "ML-DSA-65", type: "string" },
51
+ pub: b64url,
52
+ ...keyBase,
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ },
59
+ };
60
+ export function isKeyset(value) {
61
+ return checkSchema(keysetSchema, value);
62
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Arbre de Merkle de type RFC 6962 (Certificate Transparency) avec
3
+ * séparation de domaine :
4
+ * feuille = SHA-256(0x00 ‖ SHA-256(signedPayload))
5
+ * nœud = SHA-256(0x01 ‖ gauche ‖ droite)
6
+ * Nombre impair de feuilles : la dernière est promue (règle DTR, le
7
+ * plus grand k = 2^m < n), jamais dupliquée.
8
+ */
9
+ export declare function merkleLeafHash(signedPayload: Uint8Array): Uint8Array;
10
+ export declare function merkleNodeHash(left: Uint8Array, right: Uint8Array): Uint8Array;
11
+ /** Racine Merkle d'une liste de feuilles déjà hachées (merkleLeafHash). */
12
+ export declare function merkleRoot(leaves: readonly Uint8Array[]): Uint8Array;
13
+ /**
14
+ * Chemin d'inclusion (audit path) de la feuille `index`, dans l'ordre
15
+ * de remontée vers la racine : PATH(m, D[n]) = PATH(m, D[0:k]) : MTH(D[k:n])
16
+ * pour m < k (RFC 6962 §2.1.1).
17
+ */
18
+ export declare function merklePath(leaves: readonly Uint8Array[], index: number): Uint8Array[];
19
+ /**
20
+ * Vérification hors ligne : la feuille `leaf` (déjà hachée par
21
+ * merkleLeafHash) remonte-t-elle à `root` via `path` ? Miroir exact de
22
+ * merklePath : chaque entrée du chemin est consommée une fois, dans
23
+ * l'ordre.
24
+ */
25
+ export declare function verifyMerkleInclusion(leaf: Uint8Array, index: number, leafCount: number, path: readonly Uint8Array[], root: Uint8Array): boolean;
26
+ //# sourceMappingURL=merkle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merkle.d.ts","sourceRoot":"","sources":["../src/merkle.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAAC,aAAa,EAAE,UAAU,GAAG,UAAU,CAEpE;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,UAAU,GAChB,UAAU,CAEZ;AAmCD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,KAAK,EAAE,MAAM,GACZ,UAAU,EAAE,CAqBd;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,IAAI,EAAE,UAAU,GACf,OAAO,CAsCT"}
package/dist/merkle.js ADDED
@@ -0,0 +1,120 @@
1
+ import { sha256 } from "./hash.js";
2
+ /**
3
+ * Arbre de Merkle de type RFC 6962 (Certificate Transparency) avec
4
+ * séparation de domaine :
5
+ * feuille = SHA-256(0x00 ‖ SHA-256(signedPayload))
6
+ * nœud = SHA-256(0x01 ‖ gauche ‖ droite)
7
+ * Nombre impair de feuilles : la dernière est promue (règle DTR, le
8
+ * plus grand k = 2^m < n), jamais dupliquée.
9
+ */
10
+ export function merkleLeafHash(signedPayload) {
11
+ return sha256(Uint8Array.from([0x00, ...sha256(signedPayload)]));
12
+ }
13
+ export function merkleNodeHash(left, right) {
14
+ return sha256(Uint8Array.from([0x01, ...left, ...right]));
15
+ }
16
+ function bytesEq(a, b) {
17
+ return a.length === b.length && a.every((x, i) => x === b.at(i));
18
+ }
19
+ /** Plus grande puissance de 2 strictement inférieure à n (n > 1). */
20
+ function splitK(n) {
21
+ let k = 1;
22
+ while (k * 2 < n) {
23
+ k *= 2;
24
+ }
25
+ return k;
26
+ }
27
+ /** Hash d'un sous-arbre DTR sur des feuilles déjà hachées. */
28
+ function subtreeHash(leaves) {
29
+ const n = leaves.length;
30
+ if (n === 0) {
31
+ throw new Error("empty subtree");
32
+ }
33
+ if (n === 1) {
34
+ const leaf = leaves[0];
35
+ if (leaf === undefined) {
36
+ throw new Error("missing leaf");
37
+ }
38
+ return leaf;
39
+ }
40
+ const k = splitK(n);
41
+ return merkleNodeHash(subtreeHash(leaves.slice(0, k)), subtreeHash(leaves.slice(k)));
42
+ }
43
+ /** Racine Merkle d'une liste de feuilles déjà hachées (merkleLeafHash). */
44
+ export function merkleRoot(leaves) {
45
+ return subtreeHash(leaves);
46
+ }
47
+ /**
48
+ * Chemin d'inclusion (audit path) de la feuille `index`, dans l'ordre
49
+ * de remontée vers la racine : PATH(m, D[n]) = PATH(m, D[0:k]) : MTH(D[k:n])
50
+ * pour m < k (RFC 6962 §2.1.1).
51
+ */
52
+ export function merklePath(leaves, index) {
53
+ const n = leaves.length;
54
+ if (index < 0 || index >= n) {
55
+ throw new Error("leaf index out of range");
56
+ }
57
+ const path = [];
58
+ const walk = (lo, count, idx) => {
59
+ if (count === 1) {
60
+ return;
61
+ }
62
+ const k = splitK(count);
63
+ if (idx < k) {
64
+ walk(lo, k, idx);
65
+ path.push(subtreeHash(leaves.slice(lo + k, lo + count)));
66
+ }
67
+ else {
68
+ walk(lo + k, count - k, idx - k);
69
+ path.push(subtreeHash(leaves.slice(lo, lo + k)));
70
+ }
71
+ };
72
+ walk(0, n, index);
73
+ return path;
74
+ }
75
+ /**
76
+ * Vérification hors ligne : la feuille `leaf` (déjà hachée par
77
+ * merkleLeafHash) remonte-t-elle à `root` via `path` ? Miroir exact de
78
+ * merklePath : chaque entrée du chemin est consommée une fois, dans
79
+ * l'ordre.
80
+ */
81
+ export function verifyMerkleInclusion(leaf, index, leafCount, path, root) {
82
+ if (leafCount < 1 || index < 0 || index >= leafCount) {
83
+ return false;
84
+ }
85
+ let consumed = 0;
86
+ const walk = (count, idx) => {
87
+ if (count === 1) {
88
+ return leaf;
89
+ }
90
+ const k = splitK(count);
91
+ // Miroir de merklePath : le sous-arbre porteur d'abord (il consomme
92
+ // les entrées profondes), puis le sibling de CE niveau.
93
+ let current;
94
+ let sibling;
95
+ if (idx < k) {
96
+ current = walk(k, idx);
97
+ sibling = path.at(consumed);
98
+ if (sibling === undefined) {
99
+ throw new Error("path too short");
100
+ }
101
+ consumed += 1;
102
+ return merkleNodeHash(current, sibling);
103
+ }
104
+ current = walk(count - k, idx - k);
105
+ sibling = path.at(consumed);
106
+ if (sibling === undefined) {
107
+ throw new Error("path too short");
108
+ }
109
+ consumed += 1;
110
+ return merkleNodeHash(sibling, current);
111
+ };
112
+ let computed;
113
+ try {
114
+ computed = walk(leafCount, index);
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ return consumed === path.length && bytesEq(computed, root);
120
+ }
package/dist/ots.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Client OpenTimestamps minimal (ADR 0009) — format .ots uniquement.
3
+ * Périmètre strict : opérations sha256 / append / prepend,
4
+ * attestations pending et Bitcoin. Toute autre opération ou
5
+ * attestation est une erreur explicite, jamais ignorée. Aucun réseau.
6
+ *
7
+ * Référence du format : opentimestamps/python-opentimestamps,
8
+ * opentimestamps/core/{timestamp,op,notary,serialize}.py.
9
+ */
10
+ export declare class OtsError extends Error {
11
+ readonly code: string;
12
+ constructor(code: string, message: string);
13
+ }
14
+ export interface OtsOp {
15
+ readonly kind: "sha256" | "append" | "prepend";
16
+ readonly arg?: Uint8Array;
17
+ }
18
+ export interface OtsAttestation {
19
+ readonly kind: "pending" | "bitcoin";
20
+ readonly uri?: string;
21
+ readonly height?: number;
22
+ }
23
+ export interface OtsTimestamp {
24
+ readonly msg: Uint8Array;
25
+ readonly attestations: OtsAttestation[];
26
+ readonly ops: {
27
+ op: OtsOp;
28
+ stamp: OtsTimestamp;
29
+ }[];
30
+ }
31
+ export interface OtsFile {
32
+ readonly fileDigest: Uint8Array;
33
+ readonly timestamp: OtsTimestamp;
34
+ }
35
+ /** Lit un fichier .ots complet. Jamais d'exception non gérée : OtsError. */
36
+ export declare function parseOtsFile(input: Uint8Array): OtsFile;
37
+ /** Sérialise un Timestamp (ordre canonique : attestations puis ops). */
38
+ export declare function serializeTimestamp(stamp: OtsTimestamp): Uint8Array;
39
+ /** Fabrique un fichier .ots : magic ‖ version ‖ sha256 ‖ digest ‖ timestamp. */
40
+ export declare function serializeOtsFile(fileDigest: Uint8Array, timestampBytes: Uint8Array): Uint8Array;
41
+ /** Toutes les attestations de l'arbre (avec le msg attesté). */
42
+ export declare function allAttestations(stamp: OtsTimestamp): {
43
+ msg: Uint8Array;
44
+ attestation: OtsAttestation;
45
+ }[];
46
+ /** Plus petite hauteur de bloc Bitcoin attestée, si présente. */
47
+ export declare function bitcoinBlockHeight(stamp: OtsTimestamp): number | undefined;
48
+ //# sourceMappingURL=ots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ots.d.ts","sourceRoot":"","sources":["../src/ots.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAmCD,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,cAAc,EAAE,CAAC;IACxC,QAAQ,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,YAAY,CAAA;KAAE,EAAE,CAAC;CACpD;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;CAClC;AAwLD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CA4BvD;AAyFD,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CAwClE;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,UAAU,GACzB,UAAU,CAWZ;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG;IACpD,GAAG,EAAE,UAAU,CAAC;IAChB,WAAW,EAAE,cAAc,CAAC;CAC7B,EAAE,CASF;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAK1E"}