@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/CHANGELOG.md +36 -0
- package/LICENSE +202 -0
- package/README.md +145 -2
- package/bin/hcs-witness-verify.mjs +2 -0
- package/dist/anchor.d.ts +20 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +82 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +46 -0
- package/dist/binding.d.ts +22 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +38 -0
- package/dist/check.d.ts +22 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +100 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +103 -0
- package/dist/hash.d.ts +3 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/jcs.d.ts +5 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +60 -0
- package/dist/keyset.d.ts +30 -0
- package/dist/keyset.d.ts.map +1 -0
- package/dist/keyset.js +62 -0
- package/dist/merkle.d.ts +26 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +120 -0
- package/dist/ots.d.ts +48 -0
- package/dist/ots.d.ts.map +1 -0
- package/dist/ots.js +386 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +201 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +145 -0
- package/package.json +69 -4
package/dist/verify.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
2
|
+
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
|
|
3
|
+
import { b64urlToBytes } from "./base64.js";
|
|
4
|
+
import { canonicalBytes } from "./jcs.js";
|
|
5
|
+
import { isKeyset, } from "./keyset.js";
|
|
6
|
+
import { isProof, signedPayload } from "./schema.js";
|
|
7
|
+
export const ML_DSA_CONTEXT = new TextEncoder().encode("gateguard:proof:v1");
|
|
8
|
+
const SIGNALS_MAX_BYTES = 8192;
|
|
9
|
+
const ES256_SIG_BYTES = 64;
|
|
10
|
+
const MLDSA_SIG_BYTES = 3309;
|
|
11
|
+
/** Ordre du groupe P-256 (secp256r1) — constante publique NIST. */
|
|
12
|
+
const P256_ORDER = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
13
|
+
function parseTime(value) {
|
|
14
|
+
const t = Date.parse(value);
|
|
15
|
+
return Number.isNaN(t) ? Number.NaN : t;
|
|
16
|
+
}
|
|
17
|
+
function keyCoversIssuance(key, issuedMs) {
|
|
18
|
+
const nb = parseTime(key.not_before);
|
|
19
|
+
if (Number.isNaN(nb) || issuedMs < nb)
|
|
20
|
+
return false;
|
|
21
|
+
const na = key.not_after === undefined ? undefined : parseTime(key.not_after);
|
|
22
|
+
return na === undefined || Number.isNaN(na) || issuedMs <= na;
|
|
23
|
+
}
|
|
24
|
+
export function verifyProof(proof, keyset, opts) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const fail = (codes) => ({
|
|
27
|
+
valid: false,
|
|
28
|
+
errors: codes,
|
|
29
|
+
});
|
|
30
|
+
if (typeof proof !== "object" || proof === null || Array.isArray(proof)) {
|
|
31
|
+
return fail(["SCHEMA_INVALID"]);
|
|
32
|
+
}
|
|
33
|
+
const v = proof.v;
|
|
34
|
+
if (v !== 1 && v !== 2) {
|
|
35
|
+
return fail(v === undefined ? ["SCHEMA_INVALID"] : ["UNSUPPORTED_VERSION"]);
|
|
36
|
+
}
|
|
37
|
+
if (!isProof(proof)) {
|
|
38
|
+
return fail(["SCHEMA_INVALID"]);
|
|
39
|
+
}
|
|
40
|
+
const p = proof;
|
|
41
|
+
const issuedMs = parseTime(p.issued_at);
|
|
42
|
+
const expiresMs = parseTime(p.expires_at);
|
|
43
|
+
if (Number.isNaN(issuedMs) || Number.isNaN(expiresMs)) {
|
|
44
|
+
errors.push("SCHEMA_INVALID");
|
|
45
|
+
}
|
|
46
|
+
else if (expiresMs <= issuedMs) {
|
|
47
|
+
errors.push("TIMESTAMPS_INCOHERENT");
|
|
48
|
+
}
|
|
49
|
+
if (p.signals !== undefined) {
|
|
50
|
+
try {
|
|
51
|
+
if (canonicalBytes(p.signals).length > SIGNALS_MAX_BYTES) {
|
|
52
|
+
errors.push("SCHEMA_INVALID");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
errors.push("SCHEMA_INVALID");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!isKeyset(keyset)) {
|
|
60
|
+
errors.push("KEYSET_INVALID");
|
|
61
|
+
return { valid: false, errors, proof_id: p.proof_id };
|
|
62
|
+
}
|
|
63
|
+
const ks = keyset;
|
|
64
|
+
const esKey = ks.keys.find((k) => k.alg === "ES256" && k.kid === p.sig.es256.kid);
|
|
65
|
+
const mlKey = ks.keys.find((k) => k.alg === "ML-DSA-65" && k.kid === p.sig.mldsa65.kid);
|
|
66
|
+
if (esKey === undefined)
|
|
67
|
+
errors.push("ES256_KID_UNKNOWN");
|
|
68
|
+
if (mlKey === undefined)
|
|
69
|
+
errors.push("MLDSA_KID_UNKNOWN");
|
|
70
|
+
if (!Number.isNaN(issuedMs) &&
|
|
71
|
+
((esKey !== undefined && !keyCoversIssuance(esKey, issuedMs)) ||
|
|
72
|
+
(mlKey !== undefined && !keyCoversIssuance(mlKey, issuedMs)))) {
|
|
73
|
+
errors.push("KEY_NOT_VALID_AT_ISSUANCE");
|
|
74
|
+
}
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = signedPayload(p);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
errors.push("SCHEMA_INVALID");
|
|
81
|
+
return { valid: false, errors, proof_id: p.proof_id };
|
|
82
|
+
}
|
|
83
|
+
if (esKey !== undefined) {
|
|
84
|
+
const sig = b64urlToBytes(p.sig.es256.value);
|
|
85
|
+
if (sig?.length !== ES256_SIG_BYTES) {
|
|
86
|
+
errors.push("ES256_SIG_INVALID");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const s = BigInt(`0x${[...sig.subarray(32)].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
|
|
90
|
+
if (s > P256_ORDER / 2n) {
|
|
91
|
+
errors.push("ES256_HIGH_S");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
let ok = false;
|
|
95
|
+
try {
|
|
96
|
+
const pub = createPublicKey({
|
|
97
|
+
key: {
|
|
98
|
+
kty: "EC",
|
|
99
|
+
crv: "P-256",
|
|
100
|
+
x: esKey.x,
|
|
101
|
+
y: esKey.y,
|
|
102
|
+
},
|
|
103
|
+
format: "jwk",
|
|
104
|
+
});
|
|
105
|
+
// Signature brute r‖s (IEEE P1363) sur SHA-256(payload).
|
|
106
|
+
ok = cryptoVerify("sha256", payload, { key: pub, dsaEncoding: "ieee-p1363" }, sig);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
ok = false;
|
|
110
|
+
}
|
|
111
|
+
if (!ok)
|
|
112
|
+
errors.push("ES256_SIG_INVALID");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (mlKey !== undefined) {
|
|
117
|
+
const sig = b64urlToBytes(p.sig.mldsa65.value);
|
|
118
|
+
const pub = b64urlToBytes(mlKey.pub);
|
|
119
|
+
if (sig?.length !== MLDSA_SIG_BYTES) {
|
|
120
|
+
errors.push("MLDSA_SIG_INVALID");
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
let ok = false;
|
|
124
|
+
try {
|
|
125
|
+
ok =
|
|
126
|
+
pub === null
|
|
127
|
+
? false
|
|
128
|
+
: ml_dsa65.verify(sig, payload, pub, {
|
|
129
|
+
context: ML_DSA_CONTEXT,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
ok = false;
|
|
134
|
+
}
|
|
135
|
+
if (!ok)
|
|
136
|
+
errors.push("MLDSA_SIG_INVALID");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (opts?.now !== undefined &&
|
|
140
|
+
!Number.isNaN(expiresMs) &&
|
|
141
|
+
opts.now.getTime() > expiresMs) {
|
|
142
|
+
errors.push("PROOF_EXPIRED");
|
|
143
|
+
}
|
|
144
|
+
return { valid: errors.length === 0, errors, proof_id: p.proof_id };
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hcs-witness/verify",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Offline verification library and CLI for HCS Witness proofs. Zero network access.",
|
|
5
6
|
"license": "Apache-2.0",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
7
|
+
"author": "AI Solution",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"hcs-witness-verify": "bin/hcs-witness-verify.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"bin",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"CHANGELOG.md"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/hcs-witness/verify.git"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"hcs-witness",
|
|
40
|
+
"proof",
|
|
41
|
+
"verification",
|
|
42
|
+
"offline",
|
|
43
|
+
"es256",
|
|
44
|
+
"ml-dsa-65",
|
|
45
|
+
"opentimestamps",
|
|
46
|
+
"merkle"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.json",
|
|
50
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:cov": "vitest run --coverage",
|
|
53
|
+
"format:check": "prettier --check .",
|
|
54
|
+
"format": "prettier --write .",
|
|
55
|
+
"lint": "eslint . --max-warnings 0",
|
|
56
|
+
"knip": "knip"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@noble/post-quantum": "0.7.1",
|
|
60
|
+
"canonicalize": "5.1.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "24.19.2",
|
|
64
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
65
|
+
"eslint": "9.39.5",
|
|
66
|
+
"typescript-eslint": "8.72.1",
|
|
67
|
+
"fast-check": "4.10.2",
|
|
68
|
+
"knip": "6.44.1",
|
|
69
|
+
"prettier": "3.9.10",
|
|
70
|
+
"typescript": "6.0.3",
|
|
71
|
+
"vitest": "5.1.0"
|
|
72
|
+
}
|
|
8
73
|
}
|