@intyga/verify 0.0.0-bootstrap.0 → 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/CHANGELOG.md +167 -0
- package/LICENSE +201 -0
- package/README.md +296 -2
- package/dist/approval-policy.d.ts +37 -0
- package/dist/approval-policy.js +149 -0
- package/dist/index.d.ts +803 -0
- package/dist/index.js +2236 -0
- package/dist/ledger-anchor.d.ts +195 -0
- package/dist/ledger-anchor.js +314 -0
- package/dist/ledger-bundle.d.ts +235 -0
- package/dist/ledger-bundle.js +419 -0
- package/dist/ledger-chain.d.ts +58 -0
- package/dist/ledger-chain.js +121 -0
- package/dist/ledger-evidence.d.ts +193 -0
- package/dist/ledger-evidence.js +613 -0
- package/dist/ledger-leaf.d.ts +25 -0
- package/dist/ledger-leaf.js +44 -0
- package/dist/ledger-merkle.d.ts +46 -0
- package/dist/ledger-merkle.js +183 -0
- package/dist/ledger-proof.d.ts +40 -0
- package/dist/ledger-proof.js +29 -0
- package/dist/ledger-rekor.d.ts +49 -0
- package/dist/ledger-rekor.js +187 -0
- package/dist/ledger-rfc3161.d.ts +25 -0
- package/dist/ledger-rfc3161.js +345 -0
- package/dist/ledger-signature.d.ts +16 -0
- package/dist/ledger-signature.js +61 -0
- package/package.json +57 -4
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { anchorDigest, isWellFormedAnchor } from "./ledger-anchor.js";
|
|
7
|
+
const LIMIT = 1024 * 1024;
|
|
8
|
+
const FAILURE = "RFC 3161 token could not be verified under the supplied TSA trust and time policy";
|
|
9
|
+
// Only framing and TSTInfo time are read here. CMS, ESSCertID, signature, imprint, EKU and X.509
|
|
10
|
+
// validation belong to OpenSSL; this is deliberately not a second cryptographic ASN.1 stack.
|
|
11
|
+
function field(b, offset) {
|
|
12
|
+
const tag = b[offset], first = b[offset + 1];
|
|
13
|
+
if (tag === undefined || first === undefined)
|
|
14
|
+
throw new Error(FAILURE);
|
|
15
|
+
let start = offset + 2, length = first;
|
|
16
|
+
if (first >= 128) {
|
|
17
|
+
const n = first & 127;
|
|
18
|
+
if (n === 0 || n > 4 || b[start] === 0 || start + n > b.length)
|
|
19
|
+
throw new Error(FAILURE);
|
|
20
|
+
length = 0;
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
const byte = b[start + i];
|
|
23
|
+
if (byte === undefined)
|
|
24
|
+
throw new Error(FAILURE);
|
|
25
|
+
length = length * 256 + byte;
|
|
26
|
+
}
|
|
27
|
+
if (length < 128)
|
|
28
|
+
throw new Error(FAILURE);
|
|
29
|
+
start += n;
|
|
30
|
+
}
|
|
31
|
+
if (start + length > b.length)
|
|
32
|
+
throw new Error(FAILURE);
|
|
33
|
+
return { tag, start, end: start + length };
|
|
34
|
+
}
|
|
35
|
+
function sequence(b) {
|
|
36
|
+
const f = field(b, 0);
|
|
37
|
+
if (f.tag !== 0x30 || f.end !== b.length)
|
|
38
|
+
throw new Error(FAILURE);
|
|
39
|
+
return b.subarray(f.start, f.end);
|
|
40
|
+
}
|
|
41
|
+
function children(b) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let offset = 0; offset < b.length;) {
|
|
44
|
+
const f = field(b, offset);
|
|
45
|
+
if (out.length >= 8)
|
|
46
|
+
throw new Error(FAILURE);
|
|
47
|
+
out.push({ tag: f.tag, value: b.subarray(f.start, f.end) });
|
|
48
|
+
offset = f.end;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function cmsDigest(token) {
|
|
53
|
+
const algorithm = (b) => {
|
|
54
|
+
const a = children(b);
|
|
55
|
+
if (a[0]?.tag !== 6 || a.length > 2 || (a[1] && (a[1].tag !== 5 || a[1].value.length !== 0)))
|
|
56
|
+
throw new Error(FAILURE);
|
|
57
|
+
const oid = a[0].value.toString("hex");
|
|
58
|
+
if (!["608648016503040201", "608648016503040202", "608648016503040203"].includes(oid))
|
|
59
|
+
throw new Error(FAILURE);
|
|
60
|
+
return oid;
|
|
61
|
+
};
|
|
62
|
+
const content = children(sequence(token));
|
|
63
|
+
if (content.length !== 2 ||
|
|
64
|
+
content[0]?.tag !== 6 ||
|
|
65
|
+
content[0].value.toString("hex") !== "2a864886f70d010702" ||
|
|
66
|
+
content[1]?.tag !== 0xa0)
|
|
67
|
+
throw new Error(FAILURE);
|
|
68
|
+
const data = children(sequence(content[1].value));
|
|
69
|
+
if (data[0]?.tag !== 2 || data[1]?.tag !== 0x31 || data[2]?.tag !== 0x30)
|
|
70
|
+
throw new Error(FAILURE);
|
|
71
|
+
const declared = children(data[1].value);
|
|
72
|
+
if (declared.length !== 1 || declared[0]?.tag !== 0x30)
|
|
73
|
+
throw new Error(FAILURE);
|
|
74
|
+
const digest = algorithm(declared[0].value);
|
|
75
|
+
let i = 3;
|
|
76
|
+
if (data[i]?.tag === 0xa0)
|
|
77
|
+
i++;
|
|
78
|
+
if (data[i]?.tag === 0xa1)
|
|
79
|
+
i++;
|
|
80
|
+
const signerInfos = data[i];
|
|
81
|
+
if (data.length !== i + 1 || signerInfos?.tag !== 0x31)
|
|
82
|
+
throw new Error(FAILURE);
|
|
83
|
+
const signers = children(signerInfos.value);
|
|
84
|
+
if (signers.length !== 1 || signers[0]?.tag !== 0x30)
|
|
85
|
+
throw new Error(FAILURE);
|
|
86
|
+
const signer = children(signers[0].value);
|
|
87
|
+
if (signer[0]?.tag !== 2 ||
|
|
88
|
+
![0x30, 0x80].includes(signer[1]?.tag ?? -1) ||
|
|
89
|
+
signer[2]?.tag !== 0x30 ||
|
|
90
|
+
algorithm(signer[2].value) !== digest)
|
|
91
|
+
throw new Error(FAILURE);
|
|
92
|
+
}
|
|
93
|
+
function timestamp(info, now) {
|
|
94
|
+
const b = sequence(info);
|
|
95
|
+
let offset = 0;
|
|
96
|
+
for (const tag of [2, 6, 0x30, 2]) {
|
|
97
|
+
const f = field(b, offset);
|
|
98
|
+
if (f.tag !== tag)
|
|
99
|
+
throw new Error(FAILURE);
|
|
100
|
+
if (offset === 0 && (f.end - f.start !== 1 || b[f.start] !== 1))
|
|
101
|
+
throw new Error(FAILURE);
|
|
102
|
+
offset = f.end;
|
|
103
|
+
}
|
|
104
|
+
const f = field(b, offset);
|
|
105
|
+
if (f.tag !== 0x18)
|
|
106
|
+
throw new Error(FAILURE);
|
|
107
|
+
const t = b.subarray(f.start, f.end).toString("utf8");
|
|
108
|
+
if (!/^\d{14}(?:\.\d*[1-9])?Z$/.test(t))
|
|
109
|
+
throw new Error(FAILURE);
|
|
110
|
+
const iso = `${t.slice(0, 4)}-${t.slice(4, 6)}-${t.slice(6, 8)}T${t.slice(8, 10)}:${t.slice(10, 12)}:${t.slice(12, 14)}.000Z`;
|
|
111
|
+
const d = new Date(iso), seconds = d.getTime() / 1000;
|
|
112
|
+
if (!Number.isFinite(seconds) ||
|
|
113
|
+
d.toISOString() !== iso ||
|
|
114
|
+
seconds < 0 ||
|
|
115
|
+
seconds > now ||
|
|
116
|
+
(t.includes(".") && seconds === now))
|
|
117
|
+
throw new Error(FAILURE);
|
|
118
|
+
return seconds;
|
|
119
|
+
}
|
|
120
|
+
function boundedText(value, required = false) {
|
|
121
|
+
return value === undefined
|
|
122
|
+
? !required
|
|
123
|
+
: typeof value === "string" &&
|
|
124
|
+
Buffer.byteLength(value, "utf8") <= LIMIT &&
|
|
125
|
+
(!required || value.length > 0);
|
|
126
|
+
}
|
|
127
|
+
/** Offline RFC 3161 verification. No shell, downloads, OS trust store, or implicit TSA trust. */
|
|
128
|
+
export function verifyRfc3161Anchor(anchor, trust) {
|
|
129
|
+
try {
|
|
130
|
+
if (anchor.kind !== "RFC3161" ||
|
|
131
|
+
!isWellFormedAnchor(anchor) ||
|
|
132
|
+
!["ES256", "Ed25519", "RSA-PSS"].includes(anchor.algorithm) ||
|
|
133
|
+
typeof anchor.issuer !== "string" ||
|
|
134
|
+
typeof anchor.timestamp !== "string")
|
|
135
|
+
return { ok: false, reason: FAILURE };
|
|
136
|
+
return verifyRfc3161Timestamp(anchor.evidence, anchorDigest(anchor), trust);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { ok: false, reason: FAILURE };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function runSync(command) {
|
|
143
|
+
const r = spawnSync(command.executable, command.args, {
|
|
144
|
+
cwd: command.cwd,
|
|
145
|
+
env: command.env,
|
|
146
|
+
timeout: 5000,
|
|
147
|
+
killSignal: "SIGKILL",
|
|
148
|
+
stdio: "ignore",
|
|
149
|
+
});
|
|
150
|
+
return !r.error && r.status === 0;
|
|
151
|
+
}
|
|
152
|
+
function runAsync(command) {
|
|
153
|
+
return new Promise((resolveResult) => {
|
|
154
|
+
const child = spawn(command.executable, command.args, {
|
|
155
|
+
cwd: command.cwd,
|
|
156
|
+
env: command.env,
|
|
157
|
+
stdio: "ignore",
|
|
158
|
+
});
|
|
159
|
+
let failed = false;
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
failed = true;
|
|
162
|
+
child.kill("SIGKILL");
|
|
163
|
+
}, 5000);
|
|
164
|
+
child.on("error", () => {
|
|
165
|
+
failed = true;
|
|
166
|
+
});
|
|
167
|
+
// close follows error or exit and ensures the child has been reaped before temp cleanup.
|
|
168
|
+
child.on("close", (code) => {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolveResult(!failed && code === 0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function verifyRfc3161Timestamp(evidence, digest, trust, request) {
|
|
175
|
+
const checks = timestampChecks(evidence, digest, trust, request);
|
|
176
|
+
try {
|
|
177
|
+
let next = checks.next();
|
|
178
|
+
while (!next.done) {
|
|
179
|
+
if (!runSync(next.value))
|
|
180
|
+
return { ok: false, reason: FAILURE };
|
|
181
|
+
next = checks.next();
|
|
182
|
+
}
|
|
183
|
+
return next.value;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return { ok: false, reason: FAILURE };
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
checks.return({ ok: false, reason: FAILURE });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Asynchronous producer equivalent: identical checks without blocking the gateway event loop. */
|
|
193
|
+
export async function verifyRfc3161TimestampAsync(evidence, digest, trust, request) {
|
|
194
|
+
const checks = timestampChecks(evidence, digest, trust, request);
|
|
195
|
+
try {
|
|
196
|
+
let next = checks.next();
|
|
197
|
+
while (!next.done) {
|
|
198
|
+
if (!(await runAsync(next.value)))
|
|
199
|
+
return { ok: false, reason: FAILURE };
|
|
200
|
+
next = checks.next();
|
|
201
|
+
}
|
|
202
|
+
return next.value;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return { ok: false, reason: FAILURE };
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
checks.return({ ok: false, reason: FAILURE });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function* timestampChecks(evidence, digest, trust, request) {
|
|
212
|
+
let dir;
|
|
213
|
+
try {
|
|
214
|
+
const now = trust?.verificationTime ?? Math.ceil(Date.now() / 1000);
|
|
215
|
+
if (!trust ||
|
|
216
|
+
!boundedText(trust.caPem, true) ||
|
|
217
|
+
!boundedText(trust.crlPem) ||
|
|
218
|
+
!boundedText(trust.untrustedPem) ||
|
|
219
|
+
!/^[0-9a-f]{64}$/.test(trust.signerCertificateSha256) ||
|
|
220
|
+
!["crl", "unchecked"].includes(trust.revocation) ||
|
|
221
|
+
(trust.revocation === "crl" && !boundedText(trust.crlPem, true)) ||
|
|
222
|
+
!Number.isSafeInteger(now) ||
|
|
223
|
+
now < 0 ||
|
|
224
|
+
now > 253402300799 ||
|
|
225
|
+
(trust.opensslPath !== undefined &&
|
|
226
|
+
(typeof trust.opensslPath !== "string" || !trust.opensslPath || trust.opensslPath.includes("\0"))) ||
|
|
227
|
+
!(digest instanceof Uint8Array) ||
|
|
228
|
+
digest.length !== 32 ||
|
|
229
|
+
(request !== undefined && (request.length === 0 || request.length > LIMIT)) ||
|
|
230
|
+
typeof evidence !== "string" ||
|
|
231
|
+
evidence.length > Math.ceil(LIMIT / 3) * 4)
|
|
232
|
+
return { ok: false, reason: FAILURE };
|
|
233
|
+
const token = Buffer.from(evidence, "base64");
|
|
234
|
+
if (token.length === 0 || token.length > LIMIT || token.toString("base64") !== evidence)
|
|
235
|
+
return { ok: false, reason: FAILURE };
|
|
236
|
+
cmsDigest(token);
|
|
237
|
+
// `dir` (outer) is what `finally` removes; `workDir` is the same path, typed as present.
|
|
238
|
+
const workDir = mkdtempSync(join(tmpdir(), "intyga-tsa-"));
|
|
239
|
+
dir = workDir;
|
|
240
|
+
const path = (name) => join(workDir, name);
|
|
241
|
+
const write = (name, value) => writeFileSync(path(name), value, { mode: 0o600 });
|
|
242
|
+
const read = (name) => {
|
|
243
|
+
if (statSync(path(name)).size > LIMIT)
|
|
244
|
+
throw new Error(FAILURE);
|
|
245
|
+
return readFileSync(path(name));
|
|
246
|
+
};
|
|
247
|
+
mkdirSync(path("empty"), { mode: 0o700 });
|
|
248
|
+
write("openssl.cnf", "");
|
|
249
|
+
write("token.der", token);
|
|
250
|
+
write("trust.pem", `${trust.caPem}\n${trust.crlPem ?? ""}`);
|
|
251
|
+
if (trust.untrustedPem)
|
|
252
|
+
write("intermediates.pem", trust.untrustedPem);
|
|
253
|
+
write("query.tsq", Buffer.concat([
|
|
254
|
+
Buffer.from("30360201013031300d060960864801650304020105000420", "hex"),
|
|
255
|
+
Buffer.from(digest),
|
|
256
|
+
]));
|
|
257
|
+
const executable = trust.opensslPath ?? "openssl";
|
|
258
|
+
const openssl = !isAbsolute(executable) && /[\\/]/.test(executable) ? resolve(executable) : executable;
|
|
259
|
+
const command = (args) => ({
|
|
260
|
+
executable: openssl,
|
|
261
|
+
args,
|
|
262
|
+
cwd: workDir,
|
|
263
|
+
env: {
|
|
264
|
+
...process.env,
|
|
265
|
+
OPENSSL_CONF: path("openssl.cnf"),
|
|
266
|
+
SSL_CERT_FILE: path("trust.pem"),
|
|
267
|
+
SSL_CERT_DIR: path("empty"),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
yield command([
|
|
271
|
+
"cms",
|
|
272
|
+
"-verify",
|
|
273
|
+
"-binary",
|
|
274
|
+
"-inform",
|
|
275
|
+
"DER",
|
|
276
|
+
"-in",
|
|
277
|
+
path("token.der"),
|
|
278
|
+
"-noverify",
|
|
279
|
+
"-signer",
|
|
280
|
+
path("signer.pem"),
|
|
281
|
+
"-out",
|
|
282
|
+
path("info.der"),
|
|
283
|
+
]);
|
|
284
|
+
const signer = read("signer.pem").toString("ascii");
|
|
285
|
+
const cert = /^-----BEGIN CERTIFICATE-----\r?\n([A-Za-z0-9+/=\r\n]+)-----END CERTIFICATE-----\s*$/.exec(signer);
|
|
286
|
+
const body = cert?.[1];
|
|
287
|
+
if (body === undefined)
|
|
288
|
+
throw new Error(FAILURE);
|
|
289
|
+
const der = Buffer.from(body.replace(/\s/g, ""), "base64");
|
|
290
|
+
if (createHash("sha256").update(der).digest("hex") !== trust.signerCertificateSha256)
|
|
291
|
+
throw new Error(FAILURE);
|
|
292
|
+
const genTime = timestamp(read("info.der"), now);
|
|
293
|
+
// `ts -verify` never loads OpenSSL's default trust locations; it trusts only what is passed. No
|
|
294
|
+
// `-CAstore`: OpenSSL 3.0 loads a store URI eagerly and fails on an empty one (3.5 is lazy), which
|
|
295
|
+
// made every valid token fail closed on Ubuntu 24.04's 3.0.13.
|
|
296
|
+
const args = [
|
|
297
|
+
"ts",
|
|
298
|
+
"-verify",
|
|
299
|
+
"-token_in",
|
|
300
|
+
"-in",
|
|
301
|
+
path("token.der"),
|
|
302
|
+
"-queryfile",
|
|
303
|
+
path("query.tsq"),
|
|
304
|
+
"-CAfile",
|
|
305
|
+
path("trust.pem"),
|
|
306
|
+
"-CApath",
|
|
307
|
+
path("empty"),
|
|
308
|
+
"-auth_level",
|
|
309
|
+
"2",
|
|
310
|
+
"-x509_strict",
|
|
311
|
+
];
|
|
312
|
+
if (trust.untrustedPem)
|
|
313
|
+
args.push("-untrusted", path("intermediates.pem"));
|
|
314
|
+
yield command([
|
|
315
|
+
...args,
|
|
316
|
+
"-attime",
|
|
317
|
+
String(now),
|
|
318
|
+
...(trust.revocation === "crl" ? ["-crl_check_all"] : []),
|
|
319
|
+
]);
|
|
320
|
+
if (request) {
|
|
321
|
+
write("original.tsq", Buffer.from(request));
|
|
322
|
+
yield command([
|
|
323
|
+
...args.map((arg) => (arg === path("query.tsq") ? path("original.tsq") : arg)),
|
|
324
|
+
"-attime",
|
|
325
|
+
String(now),
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
// Historical certificate validity is checked independently of current CRL freshness.
|
|
329
|
+
yield command([...args, "-attime", String(genTime)]);
|
|
330
|
+
return { ok: true, genTime };
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return { ok: false, reason: FAILURE };
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
if (dir) {
|
|
337
|
+
try {
|
|
338
|
+
rmSync(dir, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
/* Result remains fail-closed; no secrets are written. */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuditLeaf } from "./ledger-leaf.js";
|
|
2
|
+
/** Caller-owned identity/key mapping. WebAuthn keys use base64 COSE_Key; ES256 keys use SPKI. */
|
|
3
|
+
export interface AuditSignaturePolicy {
|
|
4
|
+
trustedSigners: Record<string, string[]>;
|
|
5
|
+
expectedOrigin?: string;
|
|
6
|
+
expectedRpId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AuditSignatureCheck {
|
|
9
|
+
status: "verified" | "invalid" | "not_checked" | "not_applicable";
|
|
10
|
+
reason: string;
|
|
11
|
+
/** True only when the signature verified under a caller-provisioned key for this signer DID. */
|
|
12
|
+
trusted: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const uncheckedSignature: () => AuditSignatureCheck;
|
|
15
|
+
/** Signature over the committed payload only — never a quorum, authorization or hardware verdict. */
|
|
16
|
+
export declare function verifyAuditSignature(c: AuditLeaf, policy?: AuditSignaturePolicy): AuditSignatureCheck;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { verifyWebAuthnWitness } from "./index.js";
|
|
2
|
+
import { verifyEmbeddedSignature } from "./ledger-bundle.js";
|
|
3
|
+
export const uncheckedSignature = () => ({
|
|
4
|
+
status: "not_checked",
|
|
5
|
+
reason: "Content not verified or unavailable.",
|
|
6
|
+
trusted: false,
|
|
7
|
+
});
|
|
8
|
+
/** Signature over the committed payload only — never a quorum, authorization or hardware verdict. */
|
|
9
|
+
export function verifyAuditSignature(c, policy) {
|
|
10
|
+
const result = (status, reason, trusted = false) => ({ status, reason, trusted });
|
|
11
|
+
if (!c.sigAlg && (c.signature || c.signedPayload || c.signerPublicKey))
|
|
12
|
+
return result("not_checked", "Signature algorithm is missing.");
|
|
13
|
+
if (!c.sigAlg || c.sigAlg === "AUTO_APPROVED")
|
|
14
|
+
return result("not_applicable", "No human signature is declared.");
|
|
15
|
+
if (c.sigAlg !== "ES256" && c.sigAlg !== "WEBAUTHN")
|
|
16
|
+
return result("not_checked", "Unsupported signature algorithm.");
|
|
17
|
+
if (!c.signature || !c.signedPayload)
|
|
18
|
+
return result("not_checked", "Signature or signed payload is missing.");
|
|
19
|
+
// Preserve the legacy ES256 mathematical check, but never mistake an embedded key for trust.
|
|
20
|
+
if (!policy && c.sigAlg === "ES256") {
|
|
21
|
+
if (!c.signerPublicKey)
|
|
22
|
+
return result("not_checked", "Signer public key is missing.");
|
|
23
|
+
return verifyEmbeddedSignature(c)
|
|
24
|
+
? result("verified", "Signature valid under embedded key; signer identity is not established.")
|
|
25
|
+
: result("invalid", "Signature does not verify.");
|
|
26
|
+
}
|
|
27
|
+
const keys = c.signerDid && policy?.trustedSigners && Object.hasOwn(policy.trustedSigners, c.signerDid)
|
|
28
|
+
? policy.trustedSigners[c.signerDid]
|
|
29
|
+
: undefined;
|
|
30
|
+
if (!Array.isArray(keys) || keys.length === 0 || !keys.every((k) => typeof k === "string" && k.length > 0))
|
|
31
|
+
return result("not_checked", "No caller-trusted key for this signer.");
|
|
32
|
+
if (c.sigAlg === "WEBAUTHN" && (!policy?.expectedOrigin || !policy.expectedRpId))
|
|
33
|
+
return result("not_checked", "Caller-selected WebAuthn origin and RP ID are required.");
|
|
34
|
+
const m = c.metadata && typeof c.metadata === "object" ? c.metadata : {};
|
|
35
|
+
const w = m.webauthn && typeof m.webauthn === "object" ? m.webauthn : {};
|
|
36
|
+
if (c.sigAlg === "WEBAUTHN" &&
|
|
37
|
+
(typeof w.authenticatorData !== "string" ||
|
|
38
|
+
!w.authenticatorData ||
|
|
39
|
+
typeof w.clientDataJSON !== "string" ||
|
|
40
|
+
!w.clientDataJSON))
|
|
41
|
+
return result("not_checked", "WebAuthn authenticatorData or clientDataJSON is missing.");
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
const valid = c.sigAlg === "ES256"
|
|
44
|
+
? verifyEmbeddedSignature({ ...c, signerPublicKey: key })
|
|
45
|
+
: Buffer.from(key, "base64")[0] !== 0x30 &&
|
|
46
|
+
verifyWebAuthnWitness({
|
|
47
|
+
signedPayload: c.signedPayload,
|
|
48
|
+
signature: c.signature,
|
|
49
|
+
publicKey: key,
|
|
50
|
+
authenticatorData: w.authenticatorData,
|
|
51
|
+
clientDataJSON: w.clientDataJSON,
|
|
52
|
+
}, {
|
|
53
|
+
expectedOrigin: policy?.expectedOrigin ?? "",
|
|
54
|
+
expectedRpId: policy?.expectedRpId ?? "",
|
|
55
|
+
requireUserVerification: true,
|
|
56
|
+
}).ok;
|
|
57
|
+
if (valid)
|
|
58
|
+
return result("verified", "Signature valid under caller-trusted signer key.", true);
|
|
59
|
+
}
|
|
60
|
+
return result("invalid", "Signature or WebAuthn assertion does not verify under caller trust.");
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intyga/verify",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Independently verify INTYGA approval receipts — confirm a human cryptographically approved exactly the action about to run, with NO INTYGA secret. Zero npm runtime dependencies; optional OpenSSL 3 for timestamps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/intyga-dev/verify.git"
|
|
9
|
+
},
|
|
6
10
|
"homepage": "https://www.intyga.com",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
11
|
+
"bugs": "https://github.com/intyga-dev/verify/issues",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"verification",
|
|
14
|
+
"receipts",
|
|
15
|
+
"webauthn",
|
|
16
|
+
"es256",
|
|
17
|
+
"audit",
|
|
18
|
+
"offline",
|
|
19
|
+
"zero-dependency"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./approval-policy": {
|
|
31
|
+
"types": "./dist/approval-policy.d.ts",
|
|
32
|
+
"import": "./dist/approval-policy.js",
|
|
33
|
+
"default": "./dist/approval-policy.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"!dist/scripts",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "rm -rf dist && tsc",
|
|
51
|
+
"generate:parity-vectors": "node --import tsx src/scripts/generate-verifier-parity-vectors.ts",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "tsx --test 'src/**/*.test.ts'",
|
|
54
|
+
"test:coverage": "tsx --test --experimental-test-coverage --test-coverage-lines=94 --test-coverage-branches=80 --test-coverage-functions=98 --test-coverage-exclude='../**' --test-coverage-exclude='src/**/*.test.ts' 'src/**/*.test.ts'"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^26.4.0",
|
|
58
|
+
"ajv": "^8.20.0",
|
|
59
|
+
"tsx": "4.23.12",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
61
|
+
}
|
|
9
62
|
}
|