@nomosprotocol/sdk 2.0.0 → 2.3.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 +25 -5
- package/dist/index.cjs +1092 -109
- package/dist/index.d.cts +349 -9
- package/dist/index.d.ts +349 -9
- package/dist/index.js +1072 -108
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ACT_MEDIA_TYPE: () => ACT_MEDIA_TYPE,
|
|
24
|
+
MAX_PRESENTED: () => MAX_PRESENTED,
|
|
24
25
|
Nomos: () => Nomos,
|
|
25
26
|
NomosAPIError: () => NomosAPIError,
|
|
26
27
|
NomosActError: () => NomosActError,
|
|
@@ -35,6 +36,8 @@ __export(index_exports, {
|
|
|
35
36
|
NomosRateLimitError: () => NomosRateLimitError,
|
|
36
37
|
NomosValidationError: () => NomosValidationError,
|
|
37
38
|
NomosWebhookSignatureError: () => NomosWebhookSignatureError,
|
|
39
|
+
RegistryError: () => RegistryError,
|
|
40
|
+
RevocationUnavailable: () => RevocationUnavailable,
|
|
38
41
|
WELL_KNOWN: () => WELL_KNOWN,
|
|
39
42
|
accept: () => accept,
|
|
40
43
|
act: () => act,
|
|
@@ -42,24 +45,40 @@ __export(index_exports, {
|
|
|
42
45
|
bindingDigest: () => bindingDigest,
|
|
43
46
|
buildAct: () => buildAct,
|
|
44
47
|
can: () => can,
|
|
48
|
+
certificateRegistry: () => certificateRegistry,
|
|
45
49
|
certify: () => certify,
|
|
46
50
|
fetchDocument: () => fetchDocument,
|
|
47
51
|
generateSigner: () => generateSigner,
|
|
48
52
|
issuer: () => issuer,
|
|
49
53
|
kidOf: () => kidOf2,
|
|
50
54
|
localSigner: () => localSigner,
|
|
55
|
+
memoryPasskeyStore: () => memoryPasskeyStore,
|
|
56
|
+
memoryRegistryStore: () => memoryRegistryStore,
|
|
51
57
|
memoryStore: () => memoryStore,
|
|
52
58
|
nomos: () => nomos,
|
|
53
59
|
originOf: () => originOf,
|
|
60
|
+
passkeyApprover: () => passkeyApprover,
|
|
61
|
+
postgresPasskeyStore: () => postgresPasskeyStore,
|
|
62
|
+
postgresRegistryStore: () => postgresRegistryStore,
|
|
63
|
+
postgresStore: () => postgresStore,
|
|
64
|
+
presentedCertificates: () => presentedCertificates,
|
|
65
|
+
readAttestation: () => readAttestation,
|
|
66
|
+
reliedKids: () => reliedKids,
|
|
67
|
+
revocationListCertificates: () => revocationListCertificates,
|
|
68
|
+
revocationSource: () => revocationSource,
|
|
54
69
|
seal: () => seal,
|
|
55
70
|
seededSigner: () => seededSigner,
|
|
56
71
|
signConsent: () => signConsent,
|
|
72
|
+
signEnrolmentToken: () => signEnrolmentToken,
|
|
57
73
|
signInForce: () => signInForce,
|
|
74
|
+
signRevocation: () => signRevocation,
|
|
75
|
+
signRevocationList: () => signRevocationList,
|
|
58
76
|
signTestimony: () => signTestimony,
|
|
59
77
|
statusFor: () => statusFor,
|
|
60
78
|
verifyAct: () => verifyAct,
|
|
61
79
|
verifyArtifact: () => verifyArtifact,
|
|
62
80
|
verifyChain: () => verifyChain,
|
|
81
|
+
verifyEnrolmentToken: () => verifyEnrolmentToken,
|
|
63
82
|
verifySeal: () => verifySeal,
|
|
64
83
|
witness: () => witness
|
|
65
84
|
});
|
|
@@ -873,7 +892,7 @@ var Nomos = class {
|
|
|
873
892
|
}
|
|
874
893
|
};
|
|
875
894
|
|
|
876
|
-
//
|
|
895
|
+
// node_modules/@noble/hashes/_u64.js
|
|
877
896
|
var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
|
|
878
897
|
var _32n = /* @__PURE__ */ BigInt(32);
|
|
879
898
|
function fromBig(n, le = false) {
|
|
@@ -916,7 +935,7 @@ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0
|
|
|
916
935
|
var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
917
936
|
var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
918
937
|
|
|
919
|
-
//
|
|
938
|
+
// node_modules/@noble/hashes/utils.js
|
|
920
939
|
function isBytes(a) {
|
|
921
940
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
922
941
|
}
|
|
@@ -1081,7 +1100,7 @@ var oidNist = (suffix) => ({
|
|
|
1081
1100
|
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
1082
1101
|
});
|
|
1083
1102
|
|
|
1084
|
-
//
|
|
1103
|
+
// node_modules/@noble/hashes/_md.js
|
|
1085
1104
|
function Chi(a, b, c) {
|
|
1086
1105
|
return a & b ^ ~a & c;
|
|
1087
1106
|
}
|
|
@@ -1212,7 +1231,7 @@ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
1212
1231
|
327033209
|
|
1213
1232
|
]);
|
|
1214
1233
|
|
|
1215
|
-
//
|
|
1234
|
+
// node_modules/@noble/hashes/sha2.js
|
|
1216
1235
|
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
1217
1236
|
1116352408,
|
|
1218
1237
|
1899447441,
|
|
@@ -1609,7 +1628,7 @@ var sha512 = /* @__PURE__ */ createHasher(
|
|
|
1609
1628
|
/* @__PURE__ */ oidNist(3)
|
|
1610
1629
|
);
|
|
1611
1630
|
|
|
1612
|
-
//
|
|
1631
|
+
// node_modules/@nomosprotocol/verify/dist/bytes.js
|
|
1613
1632
|
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1614
1633
|
var REV = new Map([...B64].map((c, i) => [c, i]));
|
|
1615
1634
|
var utf8 = (s) => new TextEncoder().encode(s);
|
|
@@ -1671,7 +1690,7 @@ function fromBase64url(s) {
|
|
|
1671
1690
|
return raw && toBase64url(raw) === s ? raw : null;
|
|
1672
1691
|
}
|
|
1673
1692
|
|
|
1674
|
-
//
|
|
1693
|
+
// node_modules/@nomosprotocol/verify/dist/jcs.js
|
|
1675
1694
|
function value(v) {
|
|
1676
1695
|
if (v === null)
|
|
1677
1696
|
return "null";
|
|
@@ -1705,7 +1724,7 @@ function codePointCompare(a, b) {
|
|
|
1705
1724
|
return A.length - B.length;
|
|
1706
1725
|
}
|
|
1707
1726
|
|
|
1708
|
-
//
|
|
1727
|
+
// node_modules/@noble/curves/utils.js
|
|
1709
1728
|
function aarray(item, title, inner = () => {
|
|
1710
1729
|
}) {
|
|
1711
1730
|
if (!Array.isArray(item))
|
|
@@ -1887,7 +1906,7 @@ function validateObject(object, fields = {}, optFields = {}, title = "object") {
|
|
|
1887
1906
|
iter(optFields, true);
|
|
1888
1907
|
}
|
|
1889
1908
|
|
|
1890
|
-
//
|
|
1909
|
+
// node_modules/@noble/curves/abstract/modular.js
|
|
1891
1910
|
var _0n2 = /* @__PURE__ */ BigInt(0);
|
|
1892
1911
|
var _1n2 = /* @__PURE__ */ BigInt(1);
|
|
1893
1912
|
var _2n = /* @__PURE__ */ BigInt(2);
|
|
@@ -2350,7 +2369,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
|
|
|
2350
2369
|
return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
|
|
2351
2370
|
}
|
|
2352
2371
|
|
|
2353
|
-
//
|
|
2372
|
+
// node_modules/@noble/curves/abstract/curve.js
|
|
2354
2373
|
var _0n3 = /* @__PURE__ */ BigInt(0);
|
|
2355
2374
|
var _1n3 = /* @__PURE__ */ BigInt(1);
|
|
2356
2375
|
var _4n2 = /* @__PURE__ */ BigInt(4);
|
|
@@ -2737,7 +2756,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
|
|
|
2737
2756
|
};
|
|
2738
2757
|
}
|
|
2739
2758
|
|
|
2740
|
-
//
|
|
2759
|
+
// node_modules/@noble/curves/abstract/edwards.js
|
|
2741
2760
|
var _0n4 = /* @__PURE__ */ BigInt(0);
|
|
2742
2761
|
var _1n4 = /* @__PURE__ */ BigInt(1);
|
|
2743
2762
|
var _2n2 = /* @__PURE__ */ BigInt(2);
|
|
@@ -3188,7 +3207,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3188
3207
|
});
|
|
3189
3208
|
}
|
|
3190
3209
|
|
|
3191
|
-
//
|
|
3210
|
+
// node_modules/@noble/curves/ed25519.js
|
|
3192
3211
|
var _1n5 = /* @__PURE__ */ BigInt(1);
|
|
3193
3212
|
var _2n3 = /* @__PURE__ */ BigInt(2);
|
|
3194
3213
|
var _5n2 = /* @__PURE__ */ BigInt(5);
|
|
@@ -3263,7 +3282,7 @@ function ed(opts) {
|
|
|
3263
3282
|
}
|
|
3264
3283
|
var ed25519 = /* @__PURE__ */ ed({});
|
|
3265
3284
|
|
|
3266
|
-
//
|
|
3285
|
+
// node_modules/@noble/hashes/hmac.js
|
|
3267
3286
|
var _HMAC = class {
|
|
3268
3287
|
oHash;
|
|
3269
3288
|
iHash;
|
|
@@ -3340,7 +3359,7 @@ var hmac = /* @__PURE__ */ (() => {
|
|
|
3340
3359
|
return hmac_;
|
|
3341
3360
|
})();
|
|
3342
3361
|
|
|
3343
|
-
//
|
|
3362
|
+
// node_modules/@noble/curves/abstract/der.js
|
|
3344
3363
|
var _0n5 = /* @__PURE__ */ BigInt(0);
|
|
3345
3364
|
var DERErr = class extends Error {
|
|
3346
3365
|
constructor(m = "") {
|
|
@@ -3434,18 +3453,18 @@ var _DER = {
|
|
|
3434
3453
|
}
|
|
3435
3454
|
},
|
|
3436
3455
|
toSig(bytes, maxScalarBytes) {
|
|
3437
|
-
const { Err: E, _int: int, _tlv:
|
|
3456
|
+
const { Err: E, _int: int, _tlv: tlv2 } = _DER;
|
|
3438
3457
|
if (maxScalarBytes !== void 0) {
|
|
3439
3458
|
asafenumber(maxScalarBytes, "maxScalarBytes");
|
|
3440
3459
|
if (maxScalarBytes < 1)
|
|
3441
3460
|
throw new E("invalid signature: maxScalarBytes must be positive");
|
|
3442
3461
|
}
|
|
3443
3462
|
const data = abytes2(bytes, void 0, "signature");
|
|
3444
|
-
const { v: seqBytes, l: seqLeftBytes } =
|
|
3463
|
+
const { v: seqBytes, l: seqLeftBytes } = tlv2.decode(48, data);
|
|
3445
3464
|
if (seqLeftBytes.length)
|
|
3446
3465
|
throw new E("invalid signature: left bytes after parsing");
|
|
3447
|
-
const { v: rBytes, l: rLeftBytes } =
|
|
3448
|
-
const { v: sBytes, l: sLeftBytes } =
|
|
3466
|
+
const { v: rBytes, l: rLeftBytes } = tlv2.decode(2, seqBytes);
|
|
3467
|
+
const { v: sBytes, l: sLeftBytes } = tlv2.decode(2, rLeftBytes);
|
|
3449
3468
|
if (sLeftBytes.length)
|
|
3450
3469
|
throw new E("invalid signature: left bytes after parsing");
|
|
3451
3470
|
if (maxScalarBytes !== void 0 && (rBytes.length > maxScalarBytes || sBytes.length > maxScalarBytes))
|
|
@@ -3453,12 +3472,12 @@ var _DER = {
|
|
|
3453
3472
|
return { r: int.decode(rBytes), s: int.decode(sBytes) };
|
|
3454
3473
|
},
|
|
3455
3474
|
hexFromSig(sig) {
|
|
3456
|
-
const { _tlv:
|
|
3475
|
+
const { _tlv: tlv2, _int: int } = _DER;
|
|
3457
3476
|
validateObject(sig, { r: "bigint", s: "bigint" }, {}, "sig");
|
|
3458
|
-
const rs =
|
|
3459
|
-
const ss =
|
|
3477
|
+
const rs = tlv2.encode(2, int.encode(sig.r));
|
|
3478
|
+
const ss = tlv2.encode(2, int.encode(sig.s));
|
|
3460
3479
|
const seq = rs + ss;
|
|
3461
|
-
return
|
|
3480
|
+
return tlv2.encode(48, seq);
|
|
3462
3481
|
}
|
|
3463
3482
|
};
|
|
3464
3483
|
var DER = /* @__PURE__ */ (() => {
|
|
@@ -3467,7 +3486,7 @@ var DER = /* @__PURE__ */ (() => {
|
|
|
3467
3486
|
return Object.freeze(_DER);
|
|
3468
3487
|
})();
|
|
3469
3488
|
|
|
3470
|
-
//
|
|
3489
|
+
// node_modules/@noble/curves/abstract/weierstrass.js
|
|
3471
3490
|
var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
|
|
3472
3491
|
function _splitEndoScalar(k, basis, n) {
|
|
3473
3492
|
aInRange("scalar", k, _0n6, n);
|
|
@@ -4299,7 +4318,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
4299
4318
|
});
|
|
4300
4319
|
}
|
|
4301
4320
|
|
|
4302
|
-
//
|
|
4321
|
+
// node_modules/@noble/curves/nist.js
|
|
4303
4322
|
var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
4304
4323
|
p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
|
|
4305
4324
|
n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
|
|
@@ -4312,21 +4331,21 @@ var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
|
4312
4331
|
var p256_Point = /* @__PURE__ */ weierstrass(p256_CURVE);
|
|
4313
4332
|
var p256 = /* @__PURE__ */ ecdsa(p256_Point, sha256);
|
|
4314
4333
|
|
|
4315
|
-
//
|
|
4334
|
+
// node_modules/@nomosprotocol/verify/dist/suites.js
|
|
4316
4335
|
var SUITES = ["Ed25519", "ES256", "WebAuthn-ES256"];
|
|
4317
4336
|
var N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
4318
4337
|
var ED25519_PREFIX = Uint8Array.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]);
|
|
4319
4338
|
var P256_PREFIX = Uint8Array.from([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4]);
|
|
4320
4339
|
var SUITES_FOR = { "Ed25519": ["Ed25519"], "P-256": ["ES256", "WebAuthn-ES256"] };
|
|
4321
4340
|
var isSuite = (s) => typeof s === "string" && SUITES.includes(s);
|
|
4322
|
-
function pemDer(
|
|
4323
|
-
if (typeof
|
|
4341
|
+
function pemDer(pem2) {
|
|
4342
|
+
if (typeof pem2 !== "string")
|
|
4324
4343
|
return null;
|
|
4325
|
-
const m =
|
|
4344
|
+
const m = pem2.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
|
|
4326
4345
|
return m ? fromBase64(m[1].replace(/\s+/g, "")) : null;
|
|
4327
4346
|
}
|
|
4328
|
-
function loadKey(
|
|
4329
|
-
const der = pemDer(
|
|
4347
|
+
function loadKey(pem2) {
|
|
4348
|
+
const der = pemDer(pem2);
|
|
4330
4349
|
if (!der)
|
|
4331
4350
|
return { reason: "invalid_key" };
|
|
4332
4351
|
if (der.length === 44 && equal(der.subarray(0, 12), ED25519_PREFIX)) {
|
|
@@ -4350,16 +4369,16 @@ function loadKey(pem) {
|
|
|
4350
4369
|
}
|
|
4351
4370
|
return { reason: "invalid_key" };
|
|
4352
4371
|
}
|
|
4353
|
-
function kidOf(
|
|
4354
|
-
const k = loadKey(
|
|
4372
|
+
function kidOf(pem2) {
|
|
4373
|
+
const k = loadKey(pem2);
|
|
4355
4374
|
return "reason" in k ? null : toBase64url(sha256(k.der)).slice(0, 16);
|
|
4356
4375
|
}
|
|
4357
4376
|
var toBigInt = (b) => b.reduce((n, x) => n << 8n | BigInt(x), 0n);
|
|
4358
|
-
function verifySuite(suite,
|
|
4377
|
+
function verifySuite(suite, pem2, payload, proof) {
|
|
4359
4378
|
const fail2 = (reason) => ({ ok: false, reason });
|
|
4360
4379
|
if (!isSuite(suite))
|
|
4361
4380
|
return fail2("unknown_suite");
|
|
4362
|
-
const k = loadKey(
|
|
4381
|
+
const k = loadKey(pem2);
|
|
4363
4382
|
if ("reason" in k)
|
|
4364
4383
|
return fail2("invalid_key");
|
|
4365
4384
|
if (!SUITES_FOR[k.type].includes(suite))
|
|
@@ -4421,7 +4440,7 @@ function verifySuite(suite, pem, payload, proof) {
|
|
|
4421
4440
|
return ok ? { ok: true } : fail2("bad_signature");
|
|
4422
4441
|
}
|
|
4423
4442
|
|
|
4424
|
-
//
|
|
4443
|
+
// node_modules/@nomosprotocol/verify/dist/scope.js
|
|
4425
4444
|
var ARTIFACT_DIMENSIONS = ["artifact", "industry", "jurisdiction"];
|
|
4426
4445
|
var SET_DIMENSIONS = ["claim", "consent", "suite"];
|
|
4427
4446
|
var UNRESTRICTED = { terms: {} };
|
|
@@ -4540,7 +4559,7 @@ function formatScope(scope) {
|
|
|
4540
4559
|
return Object.entries(scope.terms).map(([d, v]) => `${d}:${v}`).join(" ");
|
|
4541
4560
|
}
|
|
4542
4561
|
|
|
4543
|
-
//
|
|
4562
|
+
// node_modules/@nomosprotocol/verify/dist/chain.js
|
|
4544
4563
|
var MAX_CHAIN = 20;
|
|
4545
4564
|
var CERT_SUITES = ["Ed25519", "ES256"];
|
|
4546
4565
|
var SEAL_SUITES = ["Ed25519", "ES256"];
|
|
@@ -4632,7 +4651,7 @@ var Memo = class {
|
|
|
4632
4651
|
}
|
|
4633
4652
|
};
|
|
4634
4653
|
function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo) {
|
|
4635
|
-
let
|
|
4654
|
+
let pem2 = rootPem, scope = UNRESTRICTED;
|
|
4636
4655
|
const hops = [];
|
|
4637
4656
|
for (const cert of path) {
|
|
4638
4657
|
const f = memo.of(cert);
|
|
@@ -4642,7 +4661,7 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4642
4661
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: "kid_mismatch" });
|
|
4643
4662
|
if (!suitePermitted(scope, cert.algorithm))
|
|
4644
4663
|
return fail("OUT_OF_SCOPE", { dimension: "suite", reason_code: "suite_not_permitted" });
|
|
4645
|
-
const sig = memo.sig(cert,
|
|
4664
|
+
const sig = memo.sig(cert, pem2);
|
|
4646
4665
|
if (!sig.ok)
|
|
4647
4666
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: sig.reason });
|
|
4648
4667
|
const issued = parseTime(cert.issued_at), expires = parseTime(cert.expires_at);
|
|
@@ -4662,14 +4681,14 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4662
4681
|
const narrowing = isNarrowerOrEqual(f.scope.scope, scope);
|
|
4663
4682
|
if (!narrowing.ok)
|
|
4664
4683
|
return fail("OUT_OF_SCOPE", { dimension: narrowing.dimension, reason_code: "widening" });
|
|
4665
|
-
hops.push({ cert, parentPem:
|
|
4666
|
-
|
|
4684
|
+
hops.push({ cert, parentPem: pem2, parentScope: scope });
|
|
4685
|
+
pem2 = cert.child_public_key_pem;
|
|
4667
4686
|
scope = mergeScopes(scope, f.scope.scope);
|
|
4668
4687
|
}
|
|
4669
4688
|
const admitted = purpose(scope);
|
|
4670
4689
|
if (!admitted.ok)
|
|
4671
4690
|
return fail("OUT_OF_SCOPE", { dimension: admitted.dimension });
|
|
4672
|
-
return { decision: "RESOLVED", pem, path: [], effective: scope, hops };
|
|
4691
|
+
return { decision: "RESOLVED", pem: pem2, path: [], effective: scope, hops };
|
|
4673
4692
|
}
|
|
4674
4693
|
function resolveKey(o) {
|
|
4675
4694
|
const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
|
|
@@ -4755,7 +4774,7 @@ function verifyArtifact(o) {
|
|
|
4755
4774
|
}
|
|
4756
4775
|
}
|
|
4757
4776
|
|
|
4758
|
-
//
|
|
4777
|
+
// node_modules/@nomosprotocol/verify/dist/evaluate.js
|
|
4759
4778
|
var jcsString = (v) => canonicalize(v);
|
|
4760
4779
|
var FIELD = /* @__PURE__ */ Symbol("field");
|
|
4761
4780
|
var ABSENT = /* @__PURE__ */ Symbol("absent");
|
|
@@ -5084,7 +5103,7 @@ function evaluate(artifact, facts) {
|
|
|
5084
5103
|
return ev;
|
|
5085
5104
|
}
|
|
5086
5105
|
|
|
5087
|
-
//
|
|
5106
|
+
// node_modules/@nomosprotocol/verify/dist/act.js
|
|
5088
5107
|
var MAX_WINDOW_SECONDS = 300;
|
|
5089
5108
|
var MAX_IN_FORCE_SECONDS = 86400;
|
|
5090
5109
|
var TESTIMONY_SUITES = ["Ed25519", "ES256"];
|
|
@@ -5122,7 +5141,7 @@ function hasAmbiguousEscalation(artifact) {
|
|
|
5122
5141
|
return [...named.values()].some((ids) => ids.size > 1);
|
|
5123
5142
|
}
|
|
5124
5143
|
var proofOf = (st) => st.algorithm === "WebAuthn-ES256" ? pick(st, WEBAUTHN_FIELDS) : { signature: st.signature };
|
|
5125
|
-
var statementSig = (
|
|
5144
|
+
var statementSig = (pem2, st, payload) => verifySuite(st.algorithm, pem2, jcs(payload), proofOf(st));
|
|
5126
5145
|
var time = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5127
5146
|
var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
5128
5147
|
function statementFailure(r, invalid, outOfScope, who) {
|
|
@@ -5287,11 +5306,11 @@ function verifyUnsafe(a, o) {
|
|
|
5287
5306
|
return { decision: "AUTHORIZED", commit: true, ...base };
|
|
5288
5307
|
if (ev.outcome === "block")
|
|
5289
5308
|
return { decision: "DENIED", commit: false, ...base };
|
|
5290
|
-
const
|
|
5291
|
-
if (!
|
|
5309
|
+
const esc3 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
|
|
5310
|
+
if (!esc3)
|
|
5292
5311
|
return { decision: "ESCALATED", commit: false, escalation_id: ev.escalation_id, consents_valid: 0, consents_required: null, ...base };
|
|
5293
|
-
const role =
|
|
5294
|
-
const needN = Math.trunc(Number(
|
|
5312
|
+
const role = esc3.role_required;
|
|
5313
|
+
const needN = Math.trunc(Number(esc3.required_consents ?? 1));
|
|
5295
5314
|
const approvers = /* @__PURE__ */ new Set();
|
|
5296
5315
|
for (const c of Array.isArray(act2.consents) ? act2.consents : []) {
|
|
5297
5316
|
if (!isObject(c) || !["binding_digest", "role", "decision", "signed_at", "kid", "signature"].every((k2) => typeof c[k2] === "string") || !CONSENT_SUITES.includes(c.algorithm) || c.algorithm === "WebAuthn-ES256" && !WEBAUTHN_FIELDS.every((k2) => typeof c[k2] === "string")) {
|
|
@@ -5317,7 +5336,7 @@ function verifyUnsafe(a, o) {
|
|
|
5317
5336
|
return { decision: "ESCALATED", commit: false, ...counted };
|
|
5318
5337
|
}
|
|
5319
5338
|
|
|
5320
|
-
//
|
|
5339
|
+
// node_modules/@nomosprotocol/verify/dist/seal.js
|
|
5321
5340
|
function verifySeal(artifact, publicKeyPem) {
|
|
5322
5341
|
const bad = checkSealShape(artifact);
|
|
5323
5342
|
if (bad)
|
|
@@ -5331,6 +5350,83 @@ function verifySeal(artifact, publicKeyPem) {
|
|
|
5331
5350
|
return sealFailure(artifact, publicKeyPem) ?? { decision: "VALID", kid };
|
|
5332
5351
|
}
|
|
5333
5352
|
|
|
5353
|
+
// node_modules/@nomosprotocol/verify/dist/revocation.js
|
|
5354
|
+
var MAX_LIST_CERTS = 1e3;
|
|
5355
|
+
var REVOCATION_SUITES = ["Ed25519", "ES256"];
|
|
5356
|
+
var CERT_FIELDS2 = ["parent_kid", "child_kid", "child_public_key_pem", "issued_at", "expires_at", "algorithm", "signature"];
|
|
5357
|
+
var FP = /^[0-9a-f]{64}$/;
|
|
5358
|
+
var revocationPayload = (st) => "revoked_fingerprint" in st ? { revoked_fingerprint: st.revoked_fingerprint, reason: st.reason, issued_at: st.issued_at } : { revoked_kid: st.revoked_kid, reason: st.reason, issued_at: st.issued_at };
|
|
5359
|
+
var revocationListPayload = (l) => ({ generated_at: l.generated_at, statements: l.statements });
|
|
5360
|
+
var forSigning = (suite) => (scope) => suitePermitted(scope, suite) ? { ok: true } : { ok: false, dimension: "suite" };
|
|
5361
|
+
var str = (v) => typeof v === "string" && v !== "";
|
|
5362
|
+
var time2 = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5363
|
+
function signerPem(kid, pool, root, at, suite, revokedKids, revokedCerts) {
|
|
5364
|
+
const r = resolveKey({ targetKid: kid, pool, rootPublicKeyPem: root, purpose: forSigning(suite), now: new Date(at), revokedKids, revokedCerts });
|
|
5365
|
+
return r.decision === "RESOLVED" ? r.pem : null;
|
|
5366
|
+
}
|
|
5367
|
+
function evaluateStatement(st, pool, rootPublicKeyPem, now) {
|
|
5368
|
+
if (st === null || typeof st !== "object" || Array.isArray(st))
|
|
5369
|
+
return "bad_statement";
|
|
5370
|
+
const hasKid = "revoked_kid" in st, hasFp = "revoked_fingerprint" in st;
|
|
5371
|
+
if (hasKid === hasFp)
|
|
5372
|
+
return "bad_statement";
|
|
5373
|
+
const target = hasKid ? st.revoked_kid : st.revoked_fingerprint;
|
|
5374
|
+
if (!str(target) || hasFp && !FP.test(target))
|
|
5375
|
+
return "bad_statement";
|
|
5376
|
+
if (typeof st.reason !== "string" || !["issued_at", "kid", "algorithm", "signature"].every((f) => str(st[f])))
|
|
5377
|
+
return "bad_statement";
|
|
5378
|
+
if (!REVOCATION_SUITES.includes(st.algorithm))
|
|
5379
|
+
return "bad_statement";
|
|
5380
|
+
const issued = time2(st.issued_at);
|
|
5381
|
+
if (Number.isNaN(issued) || issued > now.getTime())
|
|
5382
|
+
return "bad_statement";
|
|
5383
|
+
const certs = pool.filter((c) => c.parent_kid === st.kid && (hasKid ? c.child_kid === target : fingerprint(c) === target));
|
|
5384
|
+
if (certs.length === 0)
|
|
5385
|
+
return "no_certificate";
|
|
5386
|
+
const parent = signerPem(st.kid, pool, rootPublicKeyPem, issued, st.algorithm, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
5387
|
+
if (!parent)
|
|
5388
|
+
return "signer_not_recognized";
|
|
5389
|
+
if (!certs.some((c) => verifySuite(c.algorithm, parent, jcs(certPayload(c)), { signature: c.signature }).ok))
|
|
5390
|
+
return "bad_certificate_signature";
|
|
5391
|
+
return verifySuite(st.algorithm, parent, jcs(revocationPayload(st)), { signature: st.signature }).ok ? null : "bad_signature";
|
|
5392
|
+
}
|
|
5393
|
+
function verifyRevocationList(list, o) {
|
|
5394
|
+
const L = list;
|
|
5395
|
+
if (L === null || typeof L !== "object" || !["generated_at", "kid", "algorithm", "signature"].every((f) => typeof L[f] === "string") || !Array.isArray(L.statements)) {
|
|
5396
|
+
return { decision: "MALFORMED", reason_code: "bad_list" };
|
|
5397
|
+
}
|
|
5398
|
+
const extra = L.key_certs ?? [];
|
|
5399
|
+
if (!Array.isArray(extra) || extra.length > MAX_LIST_CERTS || !extra.every((c) => c && typeof c === "object" && CERT_FIELDS2.every((f) => str(c[f])) && ["Ed25519", "ES256"].includes(c.algorithm) && (c.scope === void 0 || c.scope === null || typeof c.scope === "string"))) {
|
|
5400
|
+
return { decision: "MALFORMED", reason_code: "bad_list_certs" };
|
|
5401
|
+
}
|
|
5402
|
+
const seen = /* @__PURE__ */ new Set(), pool = [];
|
|
5403
|
+
for (const c of [...extra, ...o.ownCerts ?? []]) {
|
|
5404
|
+
const fp = fingerprint(c);
|
|
5405
|
+
if (!seen.has(fp)) {
|
|
5406
|
+
seen.add(fp);
|
|
5407
|
+
pool.push(c);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set(), ignored = [];
|
|
5411
|
+
L.statements.forEach((st, index) => {
|
|
5412
|
+
const why = evaluateStatement(st, pool, o.rootPublicKeyPem, o.now);
|
|
5413
|
+
if (why)
|
|
5414
|
+
ignored.push({ index, reason: why });
|
|
5415
|
+
else if ("revoked_kid" in st)
|
|
5416
|
+
kids.add(st.revoked_kid);
|
|
5417
|
+
else
|
|
5418
|
+
certs.add(st.revoked_fingerprint);
|
|
5419
|
+
});
|
|
5420
|
+
const out = { decision: "OK", revoked_kids: [...kids].sort(), revoked_certs: [...certs].sort(), ignored };
|
|
5421
|
+
const generated = time2(L.generated_at);
|
|
5422
|
+
if (Number.isNaN(generated) || generated > o.now.getTime() || !REVOCATION_SUITES.includes(L.algorithm))
|
|
5423
|
+
return out;
|
|
5424
|
+
const signer = signerPem(L.kid, pool, o.rootPublicKeyPem, generated, L.algorithm, kids, certs);
|
|
5425
|
+
if (signer && verifySuite(L.algorithm, signer, jcs(revocationListPayload(L)), { signature: L.signature }).ok)
|
|
5426
|
+
out.as_of = L.generated_at;
|
|
5427
|
+
return out;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5334
5430
|
// src/verify-chain.ts
|
|
5335
5431
|
var CHAIN_PROTOCOL_VERSION2 = "1";
|
|
5336
5432
|
var DEFAULT_BASE = "https://www.nomosprotocol.com";
|
|
@@ -5377,43 +5473,241 @@ async function verifyChainRemote(params, opts) {
|
|
|
5377
5473
|
throw new NomosAPIError(message, res.status, requestId);
|
|
5378
5474
|
}
|
|
5379
5475
|
|
|
5476
|
+
// src/act/bytes.ts
|
|
5477
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
5478
|
+
var fromUtf82 = (b) => new TextDecoder().decode(b);
|
|
5479
|
+
function concat2(...parts) {
|
|
5480
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5481
|
+
let o = 0;
|
|
5482
|
+
for (const p of parts) {
|
|
5483
|
+
out.set(p, o);
|
|
5484
|
+
o += p.length;
|
|
5485
|
+
}
|
|
5486
|
+
return out;
|
|
5487
|
+
}
|
|
5488
|
+
function equal2(a, b) {
|
|
5489
|
+
if (a.length !== b.length) return false;
|
|
5490
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
5491
|
+
return true;
|
|
5492
|
+
}
|
|
5493
|
+
function fromHex(h) {
|
|
5494
|
+
const out = new Uint8Array(h.length / 2);
|
|
5495
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
5496
|
+
return out;
|
|
5497
|
+
}
|
|
5498
|
+
function toBase642(b) {
|
|
5499
|
+
let s = "";
|
|
5500
|
+
for (let i = 0; i < b.length; i += 32768) s += String.fromCharCode(...b.subarray(i, i + 32768));
|
|
5501
|
+
return btoa(s);
|
|
5502
|
+
}
|
|
5503
|
+
function fromBase642(s) {
|
|
5504
|
+
const bin = atob(s);
|
|
5505
|
+
const out = new Uint8Array(bin.length);
|
|
5506
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
5507
|
+
return out;
|
|
5508
|
+
}
|
|
5509
|
+
var toBase64url2 = (b) => toBase642(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5510
|
+
var fromBase64url2 = (s) => {
|
|
5511
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
5512
|
+
return fromBase642(t + "=".repeat((4 - t.length % 4) % 4));
|
|
5513
|
+
};
|
|
5514
|
+
function randomBytes3(n) {
|
|
5515
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
5516
|
+
}
|
|
5517
|
+
var randomUUID = () => globalThis.crypto.randomUUID();
|
|
5518
|
+
|
|
5380
5519
|
// src/act/signer.ts
|
|
5381
|
-
var import_node_crypto = require("crypto");
|
|
5382
|
-
var P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n;
|
|
5383
5520
|
function kidOf2(publicKeyPem) {
|
|
5384
|
-
const
|
|
5385
|
-
|
|
5521
|
+
const kid = kidOf(publicKeyPem);
|
|
5522
|
+
if (!kid) throw new Error("kidOf: not a canonical Ed25519 or P-256 public key (NOMOS-SPEC-010 \xA73.1)");
|
|
5523
|
+
return kid;
|
|
5524
|
+
}
|
|
5525
|
+
var ED25519_SPKI = fromHex("302a300506032b6570032100");
|
|
5526
|
+
var P256_SPKI = fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
|
|
5527
|
+
var ED25519_PKCS8 = fromHex("302e020100300506032b657004220420");
|
|
5528
|
+
var P256_PKCS8_HEAD = fromHex("308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420");
|
|
5529
|
+
var P256_PKCS8_PUB = fromHex("a144034200");
|
|
5530
|
+
var OID_ED25519 = fromHex("2b6570");
|
|
5531
|
+
var OID_EC = fromHex("2a8648ce3d0201");
|
|
5532
|
+
var OID_P256 = fromHex("2a8648ce3d030107");
|
|
5533
|
+
function pem(label, der) {
|
|
5534
|
+
const b64 = toBase642(der);
|
|
5535
|
+
return `-----BEGIN ${label}-----
|
|
5536
|
+
${b64.match(/.{1,64}/g).join("\n")}
|
|
5537
|
+
-----END ${label}-----
|
|
5538
|
+
`;
|
|
5539
|
+
}
|
|
5540
|
+
function pemBody(text) {
|
|
5541
|
+
const m = text.trim().match(/^-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----$/);
|
|
5542
|
+
if (!m) throw new Error("Signer: expected a PEM private key");
|
|
5543
|
+
return { label: m[1], der: fromBase642(m[2].replace(/\s+/g, "")) };
|
|
5544
|
+
}
|
|
5545
|
+
function tlv(b, at) {
|
|
5546
|
+
const tag = b[at];
|
|
5547
|
+
let len = b[at + 1], start = at + 2;
|
|
5548
|
+
if (len & 128) {
|
|
5549
|
+
const n = len & 127;
|
|
5550
|
+
if (n < 1 || n > 2) throw new Error("Signer: unsupported DER length");
|
|
5551
|
+
len = 0;
|
|
5552
|
+
for (let i = 0; i < n; i++) len = len << 8 | b[start + i];
|
|
5553
|
+
start += n;
|
|
5554
|
+
}
|
|
5555
|
+
if (start + len > b.length) throw new Error("Signer: truncated DER");
|
|
5556
|
+
return { tag, start, end: start + len };
|
|
5557
|
+
}
|
|
5558
|
+
function children(b, outer) {
|
|
5559
|
+
const out = [];
|
|
5560
|
+
for (let at = outer.start; at < outer.end; ) {
|
|
5561
|
+
const t = tlv(b, at);
|
|
5562
|
+
out.push(t);
|
|
5563
|
+
at = t.end;
|
|
5564
|
+
}
|
|
5565
|
+
return out;
|
|
5386
5566
|
}
|
|
5387
|
-
function
|
|
5388
|
-
const
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5567
|
+
function sec1Scalar(b, seq) {
|
|
5568
|
+
const [version, d, ...rest] = children(b, seq);
|
|
5569
|
+
if (version.tag !== 2 || d.tag !== 4 || d.end - d.start !== 32) throw new Error("Signer: malformed EC private key");
|
|
5570
|
+
for (const r of rest) {
|
|
5571
|
+
if (r.tag === 160) {
|
|
5572
|
+
const oid = tlv(b, r.start);
|
|
5573
|
+
if (!equal2(b.subarray(oid.start, oid.end), OID_P256)) throw new Error("Signer: only P-256 EC keys are a NOMOS suite (NOMOS-SPEC-010)");
|
|
5574
|
+
}
|
|
5392
5575
|
}
|
|
5576
|
+
return b.slice(d.start, d.end);
|
|
5577
|
+
}
|
|
5578
|
+
function readPrivateKey(text) {
|
|
5579
|
+
const { label, der } = pemBody(text);
|
|
5580
|
+
const top = tlv(der, 0);
|
|
5581
|
+
if (label === "EC PRIVATE KEY") return { type: "ES256", key: sec1Scalar(der, top) };
|
|
5582
|
+
if (label !== "PRIVATE KEY") throw new Error(`Signer: unsupported key type "${label}" (PKCS#8 "PRIVATE KEY" or SEC1 "EC PRIVATE KEY")`);
|
|
5583
|
+
const [, alg, key] = children(der, top);
|
|
5584
|
+
const [oid, params] = children(der, alg);
|
|
5585
|
+
const algOid = der.subarray(oid.start, oid.end);
|
|
5586
|
+
if (equal2(algOid, OID_ED25519)) {
|
|
5587
|
+
const inner = tlv(der, key.start);
|
|
5588
|
+
if (inner.tag !== 4 || inner.end - inner.start !== 32) throw new Error("Signer: malformed Ed25519 private key");
|
|
5589
|
+
return { type: "Ed25519", key: der.slice(inner.start, inner.end) };
|
|
5590
|
+
}
|
|
5591
|
+
if (equal2(algOid, OID_EC)) {
|
|
5592
|
+
if (!params || !equal2(der.subarray(params.start, params.end), OID_P256)) throw new Error("Signer: only P-256 EC keys are a NOMOS suite (NOMOS-SPEC-010)");
|
|
5593
|
+
return { type: "ES256", key: sec1Scalar(der, tlv(der, key.start)) };
|
|
5594
|
+
}
|
|
5595
|
+
throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
|
|
5596
|
+
}
|
|
5597
|
+
function publicPem(s) {
|
|
5598
|
+
return s.type === "Ed25519" ? pem("PUBLIC KEY", concat2(ED25519_SPKI, ed25519.getPublicKey(s.key))) : pem("PUBLIC KEY", concat2(P256_SPKI, p256.getPublicKey(s.key, false)));
|
|
5599
|
+
}
|
|
5600
|
+
function privatePem(s) {
|
|
5601
|
+
return s.type === "Ed25519" ? pem("PRIVATE KEY", concat2(ED25519_PKCS8, s.key)) : pem("PRIVATE KEY", concat2(P256_PKCS8_HEAD, s.key, P256_PKCS8_PUB, p256.getPublicKey(s.key, false)));
|
|
5602
|
+
}
|
|
5603
|
+
function fromSecret(s) {
|
|
5604
|
+
const publicKeyPem = publicPem(s);
|
|
5393
5605
|
return {
|
|
5394
5606
|
kid: kidOf2(publicKeyPem),
|
|
5395
5607
|
publicKeyPem,
|
|
5396
|
-
algorithm:
|
|
5608
|
+
algorithm: s.type,
|
|
5397
5609
|
async sign(payload) {
|
|
5398
|
-
|
|
5399
|
-
const raw = (0, import_node_crypto.sign)("sha256", Buffer.from(payload), { key: privateKey, dsaEncoding: "ieee-p1363" });
|
|
5400
|
-
let s = BigInt("0x" + raw.subarray(32).toString("hex"));
|
|
5401
|
-
if (s > P256_N / 2n) s = P256_N - s;
|
|
5402
|
-
return Buffer.concat([raw.subarray(0, 32), Buffer.from(s.toString(16).padStart(64, "0"), "hex")]).toString("base64");
|
|
5610
|
+
return toBase642(s.type === "Ed25519" ? ed25519.sign(payload, s.key) : p256.sign(payload, s.key, { prehash: true, lowS: true, format: "compact" }));
|
|
5403
5611
|
}
|
|
5404
5612
|
};
|
|
5405
5613
|
}
|
|
5406
5614
|
function localSigner(privateKeyPem) {
|
|
5407
|
-
return
|
|
5615
|
+
return fromSecret(readPrivateKey(privateKeyPem));
|
|
5408
5616
|
}
|
|
5409
5617
|
function generateSigner(algorithm = "Ed25519") {
|
|
5410
|
-
const
|
|
5411
|
-
return { ...
|
|
5618
|
+
const s = algorithm === "Ed25519" ? { type: "Ed25519", key: ed25519.utils.randomSecretKey() } : { type: "ES256", key: p256.utils.randomSecretKey() };
|
|
5619
|
+
return { ...fromSecret(s), privateKeyPem: privatePem(s) };
|
|
5412
5620
|
}
|
|
5413
5621
|
function seededSigner(seed) {
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5622
|
+
return fromSecret({ type: "Ed25519", key: sha2562(seed) });
|
|
5623
|
+
}
|
|
5624
|
+
var sha2562 = (s) => sha256(new TextEncoder().encode(s));
|
|
5625
|
+
|
|
5626
|
+
// src/act/certificates.ts
|
|
5627
|
+
var MAX_PRESENTED = 20;
|
|
5628
|
+
function reliedKids(act2) {
|
|
5629
|
+
const kids = [act2?.authority?.artifact?.seal?.kid, ...(act2?.testimony ?? []).map((t) => t?.kid), ...(act2?.consents ?? []).map((c) => c?.kid)];
|
|
5630
|
+
return [...new Set(kids.filter((k) => typeof k === "string" && k !== ""))];
|
|
5631
|
+
}
|
|
5632
|
+
function presentedCertificates(available, act2) {
|
|
5633
|
+
const unique = /* @__PURE__ */ new Map();
|
|
5634
|
+
for (const c of available) unique.set(fingerprint(c), c);
|
|
5635
|
+
const kids = new Set(reliedKids(act2)), chosen = /* @__PURE__ */ new Map();
|
|
5636
|
+
for (let grew = true; grew; ) {
|
|
5637
|
+
grew = false;
|
|
5638
|
+
for (const [fp, c] of unique) {
|
|
5639
|
+
if (!chosen.has(fp) && kids.has(c.child_kid)) {
|
|
5640
|
+
chosen.set(fp, c);
|
|
5641
|
+
kids.add(c.parent_kid);
|
|
5642
|
+
grew = true;
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
return [...chosen.values()];
|
|
5647
|
+
}
|
|
5648
|
+
async function signRevocation(parent, target, reason, issuedAt = /* @__PURE__ */ new Date()) {
|
|
5649
|
+
const st = "kid" in target ? { revoked_kid: target.kid, reason, issued_at: issuedAt.toISOString() } : { revoked_fingerprint: fingerprint(target.certificate), reason, issued_at: issuedAt.toISOString() };
|
|
5650
|
+
if ("certificate" in target && target.certificate.parent_kid !== parent.kid) throw new Error("signRevocation: only the parent that issued a certificate can revoke it");
|
|
5651
|
+
return { ...st, kid: parent.kid, algorithm: parent.algorithm, signature: await parent.sign(jcs(revocationPayload(st))) };
|
|
5652
|
+
}
|
|
5653
|
+
async function signRevocationList(signer, statements, keyCerts = [], generatedAt = /* @__PURE__ */ new Date()) {
|
|
5654
|
+
const L = { generated_at: generatedAt.toISOString(), kid: signer.kid, algorithm: signer.algorithm, statements, ...keyCerts.length ? { key_certs: keyCerts } : {} };
|
|
5655
|
+
L.signature = await signer.sign(jcs(revocationListPayload(L)));
|
|
5656
|
+
return L;
|
|
5657
|
+
}
|
|
5658
|
+
function revocationListCertificates(available, statements, listSignerKid) {
|
|
5659
|
+
const byFp = new Map(available.map((c) => [fingerprint(c), c]));
|
|
5660
|
+
const revokedCerts = statements.map((s) => s.revoked_fingerprint && byFp.get(s.revoked_fingerprint)).filter((c) => !!c);
|
|
5661
|
+
const kids = [listSignerKid, ...statements.flatMap((s) => [s.kid, s.revoked_kid].filter((k) => !!k)), ...revokedCerts.map((c) => c.parent_kid)];
|
|
5662
|
+
const closure = presentedCertificates(available, { authority: { artifact: { seal: {} } }, testimony: kids.map((kid) => ({ kid })) });
|
|
5663
|
+
const out = new Map(closure.map((c) => [fingerprint(c), c]));
|
|
5664
|
+
for (const c of revokedCerts) out.set(fingerprint(c), c);
|
|
5665
|
+
return [...out.values()];
|
|
5666
|
+
}
|
|
5667
|
+
var RevocationUnavailable = class extends Error {
|
|
5668
|
+
constructor(message, retryAfterSeconds) {
|
|
5669
|
+
super(message);
|
|
5670
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
5671
|
+
this.name = "RevocationUnavailable";
|
|
5672
|
+
}
|
|
5673
|
+
};
|
|
5674
|
+
function revocationSource(o) {
|
|
5675
|
+
const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
5676
|
+
const maxAge = (o.maxAgeSeconds ?? 3600) * 1e3, refresh = (o.refreshSeconds ?? 60) * 1e3;
|
|
5677
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
5678
|
+
let asOf = 0, fetchedAt = 0, inFlight = null, lastError = "";
|
|
5679
|
+
async function load() {
|
|
5680
|
+
fetchedAt = now().getTime();
|
|
5681
|
+
try {
|
|
5682
|
+
const res = await f(o.url, { headers: { accept: "application/json" } });
|
|
5683
|
+
if (!res.ok) {
|
|
5684
|
+
lastError = `${o.url} answered ${res.status}`;
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
const r = verifyRevocationList(await res.json(), { rootPublicKeyPem: o.root, now: now(), ownCerts: o.ownCerts });
|
|
5688
|
+
if (r.decision !== "OK") {
|
|
5689
|
+
lastError = `${o.url} served a malformed list (${r.reason_code})`;
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
for (const k of r.revoked_kids) kids.add(k);
|
|
5693
|
+
for (const c of r.revoked_certs) certs.add(c);
|
|
5694
|
+
if (r.as_of) asOf = Math.max(asOf, Date.parse(r.as_of));
|
|
5695
|
+
else lastError = `${o.url}'s list is not signed by a key your root reaches, so it does not show it is current`;
|
|
5696
|
+
} catch (e) {
|
|
5697
|
+
lastError = `${o.url}: ${e?.message ?? e}`;
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
return async () => {
|
|
5701
|
+
const t = now().getTime();
|
|
5702
|
+
if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
|
|
5703
|
+
inFlight = null;
|
|
5704
|
+
});
|
|
5705
|
+
if (inFlight) await inFlight;
|
|
5706
|
+
if (!asOf || t - asOf > maxAge) {
|
|
5707
|
+
throw new RevocationUnavailable(`No current revocation list${asOf ? `: the newest is from ${new Date(asOf).toISOString()}` : ""}${lastError ? ` (${lastError})` : ""}. Not committing until revocations can be checked.`, Math.ceil(refresh / 1e3));
|
|
5708
|
+
}
|
|
5709
|
+
return { kids: new Set(kids), certs: new Set(certs) };
|
|
5710
|
+
};
|
|
5417
5711
|
}
|
|
5418
5712
|
|
|
5419
5713
|
// src/act/documents.ts
|
|
@@ -5518,6 +5812,11 @@ var Rollback = class extends Error {
|
|
|
5518
5812
|
this.answer = answer2;
|
|
5519
5813
|
}
|
|
5520
5814
|
};
|
|
5815
|
+
var LostRace = class extends Error {
|
|
5816
|
+
constructor() {
|
|
5817
|
+
super("replayed");
|
|
5818
|
+
}
|
|
5819
|
+
};
|
|
5521
5820
|
function accept(o) {
|
|
5522
5821
|
const kid = kidOf2(o.root);
|
|
5523
5822
|
for (const a of o.authorities) {
|
|
@@ -5525,7 +5824,7 @@ function accept(o) {
|
|
|
5525
5824
|
throw new Error(`accept(): authority ${a.artifactId} uses currencyPolicy "record" but has no inForceSealHash`);
|
|
5526
5825
|
}
|
|
5527
5826
|
}
|
|
5528
|
-
if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only.");
|
|
5827
|
+
if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only; use postgresStore() in production.");
|
|
5529
5828
|
const store = o.store ?? memoryStore();
|
|
5530
5829
|
const maxWindowSeconds = o.maxWindowSeconds ?? 900;
|
|
5531
5830
|
const maxActBytes = o.maxActBytes ?? 1024 * 1024;
|
|
@@ -5551,8 +5850,7 @@ function accept(o) {
|
|
|
5551
5850
|
valid_until: new Date(now().getTime() + (o.documentTtlSeconds ?? 86400) * 1e3).toISOString()
|
|
5552
5851
|
};
|
|
5553
5852
|
}
|
|
5554
|
-
|
|
5555
|
-
const rev = await o.revocations?.() ?? {};
|
|
5853
|
+
function verifyWith(act2, authority, facts, at, seen, rev) {
|
|
5556
5854
|
return verifyAct(act2, {
|
|
5557
5855
|
relyingPartyId: o.relyingPartyId,
|
|
5558
5856
|
rootPublicKeyPem: o.root,
|
|
@@ -5584,9 +5882,20 @@ function accept(o) {
|
|
|
5584
5882
|
const authority = o.authorities.find((a) => a.artifactId === artifactId);
|
|
5585
5883
|
if (!authority) return answer({ decision: "NOT_ACCEPTED", reason_code: "authority_not_accepted", artifact_id: artifactId ?? null });
|
|
5586
5884
|
if (!authority.actions.includes(act2?.action?.type)) return answer({ decision: "NOT_ACCEPTED", reason_code: "action_not_accepted", action_type: act2?.action?.type ?? null });
|
|
5885
|
+
let rev;
|
|
5886
|
+
try {
|
|
5887
|
+
rev = await o.revocations?.() ?? {};
|
|
5888
|
+
} catch (e) {
|
|
5889
|
+
const retry = e instanceof RevocationUnavailable ? e.retryAfterSeconds : 60;
|
|
5890
|
+
return {
|
|
5891
|
+
status: 503,
|
|
5892
|
+
headers: { "content-type": "application/json", "retry-after": String(retry) },
|
|
5893
|
+
body: { error: "Revocation status is not current, so nothing is committed. Resubmit this same Act later.", detail: String(e?.message ?? e), retry_after: retry }
|
|
5894
|
+
};
|
|
5895
|
+
}
|
|
5587
5896
|
const verifiedAt = now();
|
|
5588
5897
|
const facts = await o.localFacts?.(act2) ?? {};
|
|
5589
|
-
const verdict =
|
|
5898
|
+
const verdict = verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce), rev);
|
|
5590
5899
|
const receiptOf = (v, f, committedAt) => ({
|
|
5591
5900
|
act_id: act2.act_id,
|
|
5592
5901
|
act_digest: String(v.act_digest ?? ""),
|
|
@@ -5606,35 +5915,34 @@ function accept(o) {
|
|
|
5606
5915
|
if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
|
|
5607
5916
|
await o.onVerdict?.(verdict, receiptOf(verdict, facts));
|
|
5608
5917
|
if (verdict.decision === "ESCALATED") {
|
|
5609
|
-
const
|
|
5610
|
-
return answer(verdict, { role:
|
|
5918
|
+
const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
|
|
5919
|
+
return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
|
|
5611
5920
|
}
|
|
5612
5921
|
if (o.mode === "shadow") return { ...answer(verdict), status: 200, body: { ...answer(verdict).body, shadow: true } };
|
|
5613
5922
|
return answer(verdict);
|
|
5614
5923
|
}
|
|
5615
5924
|
try {
|
|
5616
5925
|
return await store.transaction(async (t) => {
|
|
5617
|
-
if (!await t.claimNonce(act2.nonce))
|
|
5618
|
-
throw new Rollback(answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) }));
|
|
5619
|
-
}
|
|
5926
|
+
if (!await t.claimNonce(act2.nonce)) throw new LostRace();
|
|
5620
5927
|
let final = verdict, finalFacts = facts;
|
|
5621
5928
|
if (o.localFacts) {
|
|
5622
5929
|
const fresh = await o.localFacts(act2, t.tx);
|
|
5623
5930
|
if (!sameFacts(fresh, facts)) {
|
|
5624
5931
|
finalFacts = fresh;
|
|
5625
|
-
final =
|
|
5932
|
+
final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
|
|
5626
5933
|
if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
|
|
5627
5934
|
}
|
|
5628
5935
|
}
|
|
5629
5936
|
const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
|
|
5630
5937
|
const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
|
|
5631
5938
|
const receipt = receiptOf(final, finalFacts, now());
|
|
5632
|
-
await t.putReceipt(act2.nonce, receipt);
|
|
5939
|
+
await t.putReceipt(act2.nonce, receipt, act2);
|
|
5633
5940
|
await o.onVerdict?.(final, receipt);
|
|
5634
5941
|
return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
|
|
5635
5942
|
});
|
|
5636
5943
|
} catch (e) {
|
|
5637
5944
|
if (e instanceof Rollback) return e.answer;
|
|
5945
|
+
if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
|
|
5638
5946
|
return { status: 500, headers: { "content-type": "application/json" }, body: { error: "The commit failed and was rolled back; nothing was applied.", detail: String(e?.message ?? e) } };
|
|
5639
5947
|
}
|
|
5640
5948
|
}
|
|
@@ -5673,7 +5981,7 @@ function accept(o) {
|
|
|
5673
5981
|
if (size > maxActBytes) break;
|
|
5674
5982
|
chunks.push(c);
|
|
5675
5983
|
}
|
|
5676
|
-
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) :
|
|
5984
|
+
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : concat2(...chunks);
|
|
5677
5985
|
}
|
|
5678
5986
|
const a = await handle({ method: req.method, headers: req.headers, body });
|
|
5679
5987
|
send(a.status, a.headers, JSON.stringify(a.body));
|
|
@@ -5684,7 +5992,7 @@ function accept(o) {
|
|
|
5684
5992
|
function consentOrigins(act2) {
|
|
5685
5993
|
const origins = (Array.isArray(act2?.consents) ? act2.consents : []).filter((c) => c?.algorithm === "WebAuthn-ES256" && typeof c.client_data_json === "string").map((c) => {
|
|
5686
5994
|
try {
|
|
5687
|
-
return JSON.parse(
|
|
5995
|
+
return JSON.parse(fromUtf82(fromBase64url2(c.client_data_json))).origin;
|
|
5688
5996
|
} catch {
|
|
5689
5997
|
return null;
|
|
5690
5998
|
}
|
|
@@ -5692,8 +6000,74 @@ function consentOrigins(act2) {
|
|
|
5692
6000
|
return origins.length ? { consent_origins: [...new Set(origins)] } : {};
|
|
5693
6001
|
}
|
|
5694
6002
|
|
|
6003
|
+
// src/act/postgres.ts
|
|
6004
|
+
var IDENT = /^[a-z_][a-z0-9_]{0,62}(\.[a-z_][a-z0-9_]{0,62})?$/;
|
|
6005
|
+
function postgresStore(pool, opts = {}) {
|
|
6006
|
+
const table = opts.table ?? "nomos_acts";
|
|
6007
|
+
if (!IDENT.test(table)) throw new Error(`postgresStore: "${table}" is not a plain lower-case table name`);
|
|
6008
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${table} (
|
|
6009
|
+
nonce text PRIMARY KEY, -- SPEC-008 \xA75.2: the nonce ledger
|
|
6010
|
+
act_id text NOT NULL,
|
|
6011
|
+
receipt json NOT NULL, -- SPEC-008 \xA79; json, not jsonb: kept byte for byte
|
|
6012
|
+
act json NOT NULL, -- the exact Act verified, for re-verification
|
|
6013
|
+
committed_at timestamptz NOT NULL DEFAULT now()
|
|
6014
|
+
);`;
|
|
6015
|
+
return {
|
|
6016
|
+
schema,
|
|
6017
|
+
async migrate() {
|
|
6018
|
+
await pool.query(schema);
|
|
6019
|
+
},
|
|
6020
|
+
async hasNonce(nonce) {
|
|
6021
|
+
const r = await pool.query(`SELECT 1 FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6022
|
+
return (r.rowCount ?? r.rows.length) > 0;
|
|
6023
|
+
},
|
|
6024
|
+
async receiptFor(nonce) {
|
|
6025
|
+
const r = await pool.query(`SELECT receipt FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
6026
|
+
return r.rows[0]?.receipt ?? null;
|
|
6027
|
+
},
|
|
6028
|
+
async transaction(fn) {
|
|
6029
|
+
const client = await pool.connect();
|
|
6030
|
+
let claimed = null, completed = false;
|
|
6031
|
+
try {
|
|
6032
|
+
await client.query("BEGIN");
|
|
6033
|
+
const out = await fn({
|
|
6034
|
+
tx: client,
|
|
6035
|
+
async claimNonce(nonce) {
|
|
6036
|
+
const r = await client.query(
|
|
6037
|
+
`INSERT INTO ${table} (nonce, act_id, receipt, act) VALUES ($1, '', '{}'::json, '{}'::json)
|
|
6038
|
+
ON CONFLICT (nonce) DO NOTHING RETURNING nonce`,
|
|
6039
|
+
[nonce]
|
|
6040
|
+
);
|
|
6041
|
+
const won = (r.rowCount ?? r.rows.length) === 1;
|
|
6042
|
+
if (won) claimed = nonce;
|
|
6043
|
+
return won;
|
|
6044
|
+
},
|
|
6045
|
+
async putReceipt(nonce, receipt, act2) {
|
|
6046
|
+
if (nonce !== claimed) throw new Error("postgresStore: putReceipt for a nonce this transaction did not claim");
|
|
6047
|
+
await client.query(
|
|
6048
|
+
`UPDATE ${table} SET act_id = $2, receipt = $3::json, act = $4::json WHERE nonce = $1`,
|
|
6049
|
+
[nonce, receipt.act_id, JSON.stringify(receipt), JSON.stringify(act2 ?? {})]
|
|
6050
|
+
);
|
|
6051
|
+
completed = true;
|
|
6052
|
+
}
|
|
6053
|
+
});
|
|
6054
|
+
if (claimed && !completed) throw new Error("postgresStore: the transaction claimed a nonce but stored no receipt");
|
|
6055
|
+
await client.query("COMMIT");
|
|
6056
|
+
client.release();
|
|
6057
|
+
return out;
|
|
6058
|
+
} catch (e) {
|
|
6059
|
+
let broken;
|
|
6060
|
+
await client.query("ROLLBACK").catch((err) => {
|
|
6061
|
+
broken = err;
|
|
6062
|
+
});
|
|
6063
|
+
client.release(broken);
|
|
6064
|
+
throw e;
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
};
|
|
6068
|
+
}
|
|
6069
|
+
|
|
5695
6070
|
// src/act/statements.ts
|
|
5696
|
-
var import_node_crypto2 = require("crypto");
|
|
5697
6071
|
async function certify(parent, child, opts) {
|
|
5698
6072
|
const unsigned = {
|
|
5699
6073
|
parent_kid: parent.kid,
|
|
@@ -5747,8 +6121,8 @@ function buildAct(o) {
|
|
|
5747
6121
|
const window = o.windowSeconds ?? 300;
|
|
5748
6122
|
const act2 = {
|
|
5749
6123
|
act_version: "1",
|
|
5750
|
-
act_id: `act-${
|
|
5751
|
-
nonce: (
|
|
6124
|
+
act_id: `act-${randomUUID()}`,
|
|
6125
|
+
nonce: toBase642(randomBytes3(16)),
|
|
5752
6126
|
// Issued a second in the past so a relying party's clock a moment behind ours still accepts it.
|
|
5753
6127
|
issued_at: new Date(now.getTime() - 1e3).toISOString(),
|
|
5754
6128
|
expires_at: new Date(now.getTime() - 1e3 + window * 1e3).toISOString(),
|
|
@@ -5762,6 +6136,575 @@ function buildAct(o) {
|
|
|
5762
6136
|
return act2;
|
|
5763
6137
|
}
|
|
5764
6138
|
|
|
6139
|
+
// src/act/registry.ts
|
|
6140
|
+
function memoryRegistryStore() {
|
|
6141
|
+
const certs = [], sts = [];
|
|
6142
|
+
return {
|
|
6143
|
+
async certificates() {
|
|
6144
|
+
return [...certs];
|
|
6145
|
+
},
|
|
6146
|
+
async addCertificate(c) {
|
|
6147
|
+
if (!certs.some((x) => fingerprint(x) === fingerprint(c))) certs.push(c);
|
|
6148
|
+
},
|
|
6149
|
+
async statements() {
|
|
6150
|
+
return [...sts];
|
|
6151
|
+
},
|
|
6152
|
+
async addStatement(s) {
|
|
6153
|
+
sts.push(s);
|
|
6154
|
+
}
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
var RegistryError = class extends Error {
|
|
6158
|
+
constructor(message, code) {
|
|
6159
|
+
super(message);
|
|
6160
|
+
this.code = code;
|
|
6161
|
+
this.name = "RegistryError";
|
|
6162
|
+
}
|
|
6163
|
+
};
|
|
6164
|
+
var any = () => ({ ok: true });
|
|
6165
|
+
function certificateRegistry(o) {
|
|
6166
|
+
const rootKid = kidOf(o.root);
|
|
6167
|
+
if (!rootKid) throw new RegistryError("certificateRegistry: the root is not a canonical Ed25519 or P-256 public key", "bad_root");
|
|
6168
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6169
|
+
async function revokedSets() {
|
|
6170
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
6171
|
+
for (const s of await o.store.statements()) s.revoked_kid ? kids.add(s.revoked_kid) : s.revoked_fingerprint && certs.add(s.revoked_fingerprint);
|
|
6172
|
+
return { kids, certs };
|
|
6173
|
+
}
|
|
6174
|
+
async function scopesOf(kid) {
|
|
6175
|
+
if (kid === rootKid) return { ok: true, scopes: [{ terms: {} }] };
|
|
6176
|
+
const { kids, certs } = await revokedSets();
|
|
6177
|
+
const r = resolveKey({ targetKid: kid, pool: await o.store.certificates(), rootPublicKeyPem: o.root, purpose: any, now: now(), revokedKids: kids, revokedCerts: certs, allPaths: true });
|
|
6178
|
+
if (r.decision !== "RESOLVED") return { ok: false, reason: `${r.decision}${r.reason_code ? ` (${r.reason_code})` : ""}` };
|
|
6179
|
+
return { ok: true, scopes: r.admitted ?? [r.effective] };
|
|
6180
|
+
}
|
|
6181
|
+
async function pool(filter) {
|
|
6182
|
+
const { kids, certs } = await revokedSets(), t = now().getTime();
|
|
6183
|
+
return (await o.store.certificates()).filter((c) => !kids.has(c.child_kid) && !certs.has(fingerprint(c)) && Date.parse(c.expires_at) > t && (!filter || filter(c)));
|
|
6184
|
+
}
|
|
6185
|
+
return {
|
|
6186
|
+
rootKid,
|
|
6187
|
+
pool,
|
|
6188
|
+
/** NOMOS-SPEC-007 §3: certify `child` under `parent`. Refuses anything a verifier would refuse. */
|
|
6189
|
+
async issue(io) {
|
|
6190
|
+
if (!kidOf(io.child.publicKeyPem)) throw new RegistryError("The child key is not a canonical Ed25519 or P-256 key (NOMOS-SPEC-010 \xA73.1)", "invalid_key");
|
|
6191
|
+
const scope = parseScope(io.scope ?? null);
|
|
6192
|
+
if (!scope.ok) throw new RegistryError(`Malformed scope: ${scope.detail}`, "malformed_scope");
|
|
6193
|
+
const parentScopes = await scopesOf(io.parent.kid);
|
|
6194
|
+
if (!parentScopes.ok) throw new RegistryError(`The parent ${io.parent.kid} is not reached from the root: ${parentScopes.reason}`, "parent_not_recognized");
|
|
6195
|
+
const fits = parentScopes.scopes.filter((p) => suitePermitted(p, io.parent.algorithm) && isNarrowerOrEqual(scope.scope, p).ok);
|
|
6196
|
+
if (!fits.length) {
|
|
6197
|
+
const widened = parentScopes.scopes.map((p) => isNarrowerOrEqual(scope.scope, p)).find((x) => !x.ok);
|
|
6198
|
+
throw new RegistryError(widened ? `The scope widens the parent's: ${widened.detail}` : `The parent may not sign under ${io.parent.algorithm}`, widened ? "widening" : "suite_not_permitted");
|
|
6199
|
+
}
|
|
6200
|
+
const expiresAt = io.expiresAt ?? (io.days ? new Date(now().getTime() + io.days * 864e5) : void 0);
|
|
6201
|
+
if (!expiresAt) throw new RegistryError("Give expiresAt or days: a certificate always expires", "no_expiry");
|
|
6202
|
+
const c = await certify(io.parent, io.child, { scope: io.scope, issuedAt: io.issuedAt ?? now(), expiresAt });
|
|
6203
|
+
await o.store.addCertificate(c);
|
|
6204
|
+
return c;
|
|
6205
|
+
},
|
|
6206
|
+
/** rev. 1.10 §5.1: `parent` revokes a key it certified. The key is dead on every path. */
|
|
6207
|
+
async revokeKey(io) {
|
|
6208
|
+
const certs = (await o.store.certificates()).filter((c) => c.child_kid === io.kid && c.parent_kid === io.parent.kid);
|
|
6209
|
+
if (!certs.length) throw new RegistryError(`${io.parent.kid} never certified ${io.kid}: only a key's parent can revoke it`, "not_the_parent");
|
|
6210
|
+
const st = await signRevocation(io.parent, { kid: io.kid }, io.reason, now());
|
|
6211
|
+
await o.store.addStatement(st);
|
|
6212
|
+
return st;
|
|
6213
|
+
},
|
|
6214
|
+
/** rev. 1.10 §5.1: withdraw one certificate; the key stays valid under any other. */
|
|
6215
|
+
async revokeCertificate(io) {
|
|
6216
|
+
const c = (await o.store.certificates()).find((x) => fingerprint(x) === io.fingerprint);
|
|
6217
|
+
if (!c) throw new RegistryError(`No certificate ${io.fingerprint} in this registry`, "unknown_certificate");
|
|
6218
|
+
const st = await signRevocation(io.parent, { certificate: c }, io.reason, now());
|
|
6219
|
+
await o.store.addStatement(st);
|
|
6220
|
+
return st;
|
|
6221
|
+
},
|
|
6222
|
+
/** The certificates that connect `kid` to the root: what a witness or approver returns with
|
|
6223
|
+
* what it signs (SPEC-009 0.2.0 §4, §5). */
|
|
6224
|
+
async chainFor(kid) {
|
|
6225
|
+
return presentedCertificates(await pool(), { authority: { artifact: { seal: { kid } } } });
|
|
6226
|
+
},
|
|
6227
|
+
/** How `kid` is reached from the root, and with which effective scopes, or why it is not. */
|
|
6228
|
+
async check(kid) {
|
|
6229
|
+
return scopesOf(kid);
|
|
6230
|
+
},
|
|
6231
|
+
certificates: () => o.store.certificates(),
|
|
6232
|
+
statements: () => o.store.statements(),
|
|
6233
|
+
/** rev. 1.10 §5.3: the signed list, carrying the certificates that make it checkable. The
|
|
6234
|
+
* signer should be reached from the root, or the list establishes no date (§5.3). */
|
|
6235
|
+
async revocationList(signer) {
|
|
6236
|
+
const statements = await o.store.statements();
|
|
6237
|
+
return signRevocationList(signer, statements, revocationListCertificates(await o.store.certificates(), statements, signer.kid), now());
|
|
6238
|
+
}
|
|
6239
|
+
};
|
|
6240
|
+
}
|
|
6241
|
+
function postgresRegistryStore(pool, opts = {}) {
|
|
6242
|
+
const p = opts.prefix ?? "nomos_registry";
|
|
6243
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new RegistryError(`"${p}" is not a plain lower-case prefix`, "bad_prefix");
|
|
6244
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_certificates (
|
|
6245
|
+
fingerprint text PRIMARY KEY, child_kid text NOT NULL, parent_kid text NOT NULL, certificate json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
|
|
6246
|
+
);
|
|
6247
|
+
CREATE TABLE IF NOT EXISTS ${p}_statements (
|
|
6248
|
+
id bigserial PRIMARY KEY, target text NOT NULL, kid text NOT NULL, statement json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
|
|
6249
|
+
);`;
|
|
6250
|
+
return {
|
|
6251
|
+
schema,
|
|
6252
|
+
async migrate() {
|
|
6253
|
+
await pool.query(schema);
|
|
6254
|
+
},
|
|
6255
|
+
async certificates() {
|
|
6256
|
+
return (await pool.query(`SELECT certificate FROM ${p}_certificates ORDER BY added_at, fingerprint`)).rows.map((r) => r.certificate);
|
|
6257
|
+
},
|
|
6258
|
+
async addCertificate(c) {
|
|
6259
|
+
await pool.query(
|
|
6260
|
+
`INSERT INTO ${p}_certificates (fingerprint, child_kid, parent_kid, certificate) VALUES ($1, $2, $3, $4::json) ON CONFLICT (fingerprint) DO NOTHING`,
|
|
6261
|
+
[fingerprint(c), c.child_kid, c.parent_kid, JSON.stringify(c)]
|
|
6262
|
+
);
|
|
6263
|
+
},
|
|
6264
|
+
async statements() {
|
|
6265
|
+
return (await pool.query(`SELECT statement FROM ${p}_statements ORDER BY id`)).rows.map((r) => r.statement);
|
|
6266
|
+
},
|
|
6267
|
+
async addStatement(s) {
|
|
6268
|
+
await pool.query(`INSERT INTO ${p}_statements (target, kid, statement) VALUES ($1, $2, $3::json)`, [s.revoked_kid ?? s.revoked_fingerprint, s.kid, JSON.stringify(s)]);
|
|
6269
|
+
}
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
// src/act/passkeys.ts
|
|
6274
|
+
function cbor(b) {
|
|
6275
|
+
let i = 0;
|
|
6276
|
+
const len = (info) => {
|
|
6277
|
+
if (info < 24) return info;
|
|
6278
|
+
const n = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : -1;
|
|
6279
|
+
if (n < 0) throw new Error("unsupported CBOR length");
|
|
6280
|
+
let v2 = 0;
|
|
6281
|
+
for (let k = 0; k < n; k++) v2 = v2 * 256 + b[i++];
|
|
6282
|
+
return v2;
|
|
6283
|
+
};
|
|
6284
|
+
const item = (depth) => {
|
|
6285
|
+
if (depth > 16 || i >= b.length) throw new Error("malformed CBOR");
|
|
6286
|
+
const head = b[i++], major = head >> 5, info = head & 31;
|
|
6287
|
+
switch (major) {
|
|
6288
|
+
case 0:
|
|
6289
|
+
return len(info);
|
|
6290
|
+
case 1:
|
|
6291
|
+
return -1 - len(info);
|
|
6292
|
+
case 2: {
|
|
6293
|
+
const n = len(info);
|
|
6294
|
+
const v2 = b.slice(i, i + n);
|
|
6295
|
+
i += n;
|
|
6296
|
+
if (v2.length !== n) throw new Error("truncated CBOR");
|
|
6297
|
+
return v2;
|
|
6298
|
+
}
|
|
6299
|
+
case 3: {
|
|
6300
|
+
const n = len(info);
|
|
6301
|
+
const v2 = fromUtf82(b.slice(i, i + n));
|
|
6302
|
+
i += n;
|
|
6303
|
+
return v2;
|
|
6304
|
+
}
|
|
6305
|
+
case 4: {
|
|
6306
|
+
const n = len(info), a = [];
|
|
6307
|
+
for (let k = 0; k < n; k++) a.push(item(depth + 1));
|
|
6308
|
+
return a;
|
|
6309
|
+
}
|
|
6310
|
+
case 5: {
|
|
6311
|
+
const n = len(info), m = /* @__PURE__ */ new Map();
|
|
6312
|
+
for (let k = 0; k < n; k++) m.set(item(depth + 1), item(depth + 1));
|
|
6313
|
+
return m;
|
|
6314
|
+
}
|
|
6315
|
+
case 7:
|
|
6316
|
+
if (info === 20) return false;
|
|
6317
|
+
if (info === 21) return true;
|
|
6318
|
+
if (info === 22) return null;
|
|
6319
|
+
throw new Error("unsupported CBOR simple value");
|
|
6320
|
+
default:
|
|
6321
|
+
throw new Error("unsupported CBOR type");
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
const v = item(0);
|
|
6325
|
+
return { value: v, end: i };
|
|
6326
|
+
}
|
|
6327
|
+
function readAttestation(attestationObject) {
|
|
6328
|
+
const att = cbor(attestationObject).value;
|
|
6329
|
+
if (!(att instanceof Map) || !(att.get("authData") instanceof Uint8Array)) throw new Error("not an attestation object");
|
|
6330
|
+
const a = att.get("authData");
|
|
6331
|
+
if (a.length < 55 || !(a[32] & 64)) throw new Error("no attested credential data");
|
|
6332
|
+
const idLen = a[53] << 8 | a[54];
|
|
6333
|
+
const credentialId = a.slice(55, 55 + idLen);
|
|
6334
|
+
const cose = cbor(a.slice(55 + idLen)).value;
|
|
6335
|
+
if (!(cose instanceof Map) || cose.get(1) !== 2 || cose.get(3) !== -7 || cose.get(-1) !== 1) throw new Error("the passkey is not an ES256 P-256 key (COSE kty 2, alg -7, crv 1)");
|
|
6336
|
+
const x = cose.get(-2), y = cose.get(-3);
|
|
6337
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) throw new Error("malformed COSE key");
|
|
6338
|
+
const spki = concat2(Uint8Array.from([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4]), x, y);
|
|
6339
|
+
const publicKeyPem = pem("PUBLIC KEY", spki);
|
|
6340
|
+
if (!kidOf(publicKeyPem)) throw new Error("the passkey's key is not a valid P-256 point");
|
|
6341
|
+
return { authData: a, flags: a[32], credentialId, publicKeyPem };
|
|
6342
|
+
}
|
|
6343
|
+
async function signEnrolmentToken(authority, c) {
|
|
6344
|
+
if (!/^[^,\s]+$/.test(c.approver)) throw new Error("An approver id has no commas or spaces (NOMOS-SPEC-007 rev. 1.9 \xA73.7)");
|
|
6345
|
+
const claims = { approver: c.approver, roles: [...c.roles], origin: c.origin.replace(/\/$/, ""), expires_at: c.expiresAt.toISOString(), nonce: toBase64url2(randomBytes3(16)) };
|
|
6346
|
+
const token = { ...claims, kid: authority.kid, algorithm: authority.algorithm, signature: await authority.sign(jcs(claims)) };
|
|
6347
|
+
return toBase64url2(utf82(JSON.stringify(token)));
|
|
6348
|
+
}
|
|
6349
|
+
function verifyEnrolmentToken(token, authorityPem, origin, now) {
|
|
6350
|
+
let t;
|
|
6351
|
+
try {
|
|
6352
|
+
t = JSON.parse(fromUtf82(fromBase64url2(token)));
|
|
6353
|
+
} catch {
|
|
6354
|
+
return { error: "not an enrolment link" };
|
|
6355
|
+
}
|
|
6356
|
+
const claims = { approver: t?.approver, roles: t?.roles, origin: t?.origin, expires_at: t?.expires_at, nonce: t?.nonce };
|
|
6357
|
+
if (typeof claims.approver !== "string" || !Array.isArray(claims.roles) || !claims.roles.every((r) => typeof r === "string") || typeof claims.nonce !== "string") return { error: "malformed enrolment link" };
|
|
6358
|
+
if (t.kid !== kidOf(authorityPem) || !verifySuite(t.algorithm, authorityPem, jcs(claims), { signature: t.signature }).ok) return { error: "this enrolment link was not signed by the enrolment authority" };
|
|
6359
|
+
if (claims.origin !== origin) return { error: "this enrolment link is for another service" };
|
|
6360
|
+
if (!(Date.parse(claims.expires_at) > now.getTime())) return { error: "this enrolment link has expired" };
|
|
6361
|
+
return claims;
|
|
6362
|
+
}
|
|
6363
|
+
function memoryPasskeyStore() {
|
|
6364
|
+
const used = /* @__PURE__ */ new Set(), challenges = /* @__PURE__ */ new Map(), creds = [], pend = /* @__PURE__ */ new Map();
|
|
6365
|
+
return {
|
|
6366
|
+
async claimToken(n) {
|
|
6367
|
+
if (used.has(n)) return false;
|
|
6368
|
+
used.add(n);
|
|
6369
|
+
return true;
|
|
6370
|
+
},
|
|
6371
|
+
async tokenUsed(n) {
|
|
6372
|
+
return used.has(n);
|
|
6373
|
+
},
|
|
6374
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6375
|
+
if (!used.has(n)) challenges.set(n, c);
|
|
6376
|
+
},
|
|
6377
|
+
async enrolmentChallenge(n) {
|
|
6378
|
+
return used.has(n) ? null : challenges.get(n) ?? null;
|
|
6379
|
+
},
|
|
6380
|
+
async addCredential(c) {
|
|
6381
|
+
creds.push(structuredClone(c));
|
|
6382
|
+
},
|
|
6383
|
+
async credentials() {
|
|
6384
|
+
return structuredClone(creds);
|
|
6385
|
+
},
|
|
6386
|
+
async createPending(p) {
|
|
6387
|
+
pend.set(p.id, structuredClone(p));
|
|
6388
|
+
},
|
|
6389
|
+
async pending(id) {
|
|
6390
|
+
const p = pend.get(id);
|
|
6391
|
+
return p ? structuredClone(p) : null;
|
|
6392
|
+
},
|
|
6393
|
+
async addAttempt(id, c, a) {
|
|
6394
|
+
const p = pend.get(id);
|
|
6395
|
+
if (!p || p.status !== "pending") return false;
|
|
6396
|
+
p.attempts[c] = { ...a };
|
|
6397
|
+
return true;
|
|
6398
|
+
},
|
|
6399
|
+
async resolvePending(id, out) {
|
|
6400
|
+
const p = pend.get(id);
|
|
6401
|
+
if (!p || p.status !== "pending") return false;
|
|
6402
|
+
Object.assign(p, structuredClone(out));
|
|
6403
|
+
return true;
|
|
6404
|
+
}
|
|
6405
|
+
};
|
|
6406
|
+
}
|
|
6407
|
+
function postgresPasskeyStore(pool, opts = {}) {
|
|
6408
|
+
const p = opts.prefix ?? "nomos_passkeys";
|
|
6409
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new Error(`"${p}" is not a plain lower-case prefix`);
|
|
6410
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_tokens (nonce text PRIMARY KEY, challenge text, used boolean NOT NULL DEFAULT false);
|
|
6411
|
+
CREATE TABLE IF NOT EXISTS ${p}_credentials (credential_id text PRIMARY KEY, credential json NOT NULL, added_at timestamptz NOT NULL DEFAULT now());
|
|
6412
|
+
CREATE TABLE IF NOT EXISTS ${p}_pending (id text PRIMARY KEY, approval jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());`;
|
|
6413
|
+
return {
|
|
6414
|
+
schema,
|
|
6415
|
+
async migrate() {
|
|
6416
|
+
await pool.query(schema);
|
|
6417
|
+
},
|
|
6418
|
+
async claimToken(n) {
|
|
6419
|
+
const r = await pool.query(`INSERT INTO ${p}_tokens (nonce, used) VALUES ($1, true) ON CONFLICT (nonce) DO UPDATE SET used = true WHERE ${p}_tokens.used = false RETURNING nonce`, [n]);
|
|
6420
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6421
|
+
},
|
|
6422
|
+
async tokenUsed(n) {
|
|
6423
|
+
return (await pool.query(`SELECT 1 FROM ${p}_tokens WHERE nonce = $1 AND used = true`, [n])).rows.length > 0;
|
|
6424
|
+
},
|
|
6425
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6426
|
+
await pool.query(`INSERT INTO ${p}_tokens (nonce, challenge) VALUES ($1, $2) ON CONFLICT (nonce) DO UPDATE SET challenge = $2 WHERE ${p}_tokens.used = false`, [n, c]);
|
|
6427
|
+
},
|
|
6428
|
+
async enrolmentChallenge(n) {
|
|
6429
|
+
return (await pool.query(`SELECT challenge FROM ${p}_tokens WHERE nonce = $1 AND used = false`, [n])).rows[0]?.challenge ?? null;
|
|
6430
|
+
},
|
|
6431
|
+
async addCredential(c) {
|
|
6432
|
+
await pool.query(`INSERT INTO ${p}_credentials (credential_id, credential) VALUES ($1, $2::json)`, [c.credential_id, JSON.stringify(c)]);
|
|
6433
|
+
},
|
|
6434
|
+
async credentials() {
|
|
6435
|
+
return (await pool.query(`SELECT credential FROM ${p}_credentials ORDER BY added_at`)).rows.map((r) => r.credential);
|
|
6436
|
+
},
|
|
6437
|
+
async createPending(a) {
|
|
6438
|
+
await pool.query(`INSERT INTO ${p}_pending (id, approval) VALUES ($1, $2::jsonb)`, [a.id, JSON.stringify(a)]);
|
|
6439
|
+
},
|
|
6440
|
+
async pending(id) {
|
|
6441
|
+
return (await pool.query(`SELECT approval FROM ${p}_pending WHERE id = $1`, [id])).rows[0]?.approval ?? null;
|
|
6442
|
+
},
|
|
6443
|
+
// Each change is one conditional statement: Postgres serializes them on the row, and none
|
|
6444
|
+
// rewrites the whole record, so a stale reader cannot write an old status back.
|
|
6445
|
+
async addAttempt(id, c, a) {
|
|
6446
|
+
const r = await pool.query(`UPDATE ${p}_pending SET approval = jsonb_set(approval, ARRAY['attempts', $2::text], $3::jsonb), updated_at = now() WHERE id = $1 AND approval->>'status' = 'pending' RETURNING id`, [id, c, JSON.stringify(a)]);
|
|
6447
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6448
|
+
},
|
|
6449
|
+
async resolvePending(id, out) {
|
|
6450
|
+
const r = await pool.query(`UPDATE ${p}_pending SET approval = approval || $2::jsonb, updated_at = now() WHERE id = $1 AND approval->>'status' = 'pending' RETURNING id`, [id, JSON.stringify(out)]);
|
|
6451
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6452
|
+
}
|
|
6453
|
+
};
|
|
6454
|
+
}
|
|
6455
|
+
var MAX_ATTEMPTS = 50;
|
|
6456
|
+
var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6457
|
+
function passkeyApprover(o) {
|
|
6458
|
+
const base = o.origin.replace(/\/$/, "");
|
|
6459
|
+
const rpId = o.rpId ?? new URL(base).hostname;
|
|
6460
|
+
const store = o.store ?? memoryPasskeyStore();
|
|
6461
|
+
if (!o.store) console.warn("[nomos] passkeyApprover(): using the in-memory store. Enrolled passkeys are lost on restart. Development only; use postgresPasskeyStore() in production.");
|
|
6462
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6463
|
+
const rpIdHash = sha256(utf82(rpId));
|
|
6464
|
+
const expired = (p) => p.status === "pending" && Date.parse(p.act.expires_at) <= now().getTime();
|
|
6465
|
+
const statusOf = (p) => expired(p) ? "expired" : p.status;
|
|
6466
|
+
function clientData(b64, type, challenge) {
|
|
6467
|
+
let c;
|
|
6468
|
+
try {
|
|
6469
|
+
c = JSON.parse(fromUtf82(fromBase64url2(b64)));
|
|
6470
|
+
} catch {
|
|
6471
|
+
return "unreadable client data";
|
|
6472
|
+
}
|
|
6473
|
+
if (c?.type !== type) return `expected a ${type} ceremony`;
|
|
6474
|
+
if (c?.challenge !== challenge) return "the challenge does not match";
|
|
6475
|
+
if (c?.origin !== base) return `the ceremony ran on ${c?.origin}, not ${base}`;
|
|
6476
|
+
return null;
|
|
6477
|
+
}
|
|
6478
|
+
async function route(method, path, json) {
|
|
6479
|
+
if (method === "POST" && path === "/nomos/approvals") {
|
|
6480
|
+
const { role, act: act2 } = await json();
|
|
6481
|
+
if (!o.roles.includes(role)) return { status: 404, body: { error: `This service does not approve as ${role}.` } };
|
|
6482
|
+
if (typeof act2?.expires_at !== "string" || !act2?.action) return { status: 400, body: { error: "The request needs the Act." } };
|
|
6483
|
+
const id = randomUUID();
|
|
6484
|
+
await store.createPending({ id, role, act: act2, status: "pending", created_at: now().toISOString(), attempts: {} });
|
|
6485
|
+
await o.onPending?.({ id, role, act: act2, approvalUrl: `${base}/approve/${id}` });
|
|
6486
|
+
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6487
|
+
}
|
|
6488
|
+
let m = path.match(/^\/nomos\/approvals\/([0-9a-f-]{36})$/);
|
|
6489
|
+
if (method === "GET" && m) {
|
|
6490
|
+
const p = await store.pending(m[1]);
|
|
6491
|
+
if (!p) return null;
|
|
6492
|
+
const s = statusOf(p);
|
|
6493
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...p.key_certs?.length ? { key_certs: p.key_certs } : {} } : {} } };
|
|
6494
|
+
}
|
|
6495
|
+
m = path.match(/^\/approve\/([0-9a-f-]{36})(?:\/(devices|options|assertion|refuse))?$/);
|
|
6496
|
+
if (m) {
|
|
6497
|
+
const p = await store.pending(m[1]);
|
|
6498
|
+
if (!p) return null;
|
|
6499
|
+
if (method === "GET" && !m[2]) return { status: 200, type: "text/html", body: approvalPage(p, statusOf(p), o.listApprovers === true ? (await store.credentials()).filter((c) => c.roles.includes(p.role)) : null) };
|
|
6500
|
+
if (method !== "POST" || !m[2]) return null;
|
|
6501
|
+
if (statusOf(p) !== "pending") return { status: 409, body: { error: `This request is ${statusOf(p)}.` } };
|
|
6502
|
+
if (m[2] === "refuse") return await store.resolvePending(p.id, { status: "refused" }) ? { status: 200, body: { status: "refused" } } : { status: 409, body: { error: "This request is no longer pending." } };
|
|
6503
|
+
const req = await json();
|
|
6504
|
+
if (m[2] === "devices") {
|
|
6505
|
+
const mine = (await store.credentials()).filter((c) => c.approver === String(req?.approver ?? "") && c.roles.includes(p.role));
|
|
6506
|
+
return { status: 200, body: { devices: mine.map((c) => ({ credential_id: c.credential_id, label: c.label })) } };
|
|
6507
|
+
}
|
|
6508
|
+
const cred = (await store.credentials()).find((c) => c.credential_id === req?.credential_id);
|
|
6509
|
+
if (!cred || !cred.roles.includes(p.role)) return { status: 403, body: { error: `That passkey does not approve as ${p.role}.` } };
|
|
6510
|
+
if (m[2] === "options") {
|
|
6511
|
+
const signed_at = now().toISOString();
|
|
6512
|
+
const payload2 = consentPayload({ binding_digest: bindingDigest(p.act), role: p.role, decision: "approve", signed_at, kid: cred.kid });
|
|
6513
|
+
const challenge = toBase64url2(sha256(jcs(payload2)));
|
|
6514
|
+
if (Object.keys(p.attempts).length >= MAX_ATTEMPTS) return { status: 429, body: { error: "Too many approval ceremonies for this request." } };
|
|
6515
|
+
const expires_at = new Date(Math.min(Date.parse(p.act.expires_at), now().getTime() + 5 * 6e4)).toISOString();
|
|
6516
|
+
if (!await store.addAttempt(p.id, challenge, { credential_id: cred.credential_id, signed_at, expires_at })) return { status: 409, body: { error: "This request is no longer pending." } };
|
|
6517
|
+
return { status: 200, body: { challenge, rpId, allowCredentials: [{ type: "public-key", id: cred.credential_id }], userVerification: "required", timeout: 12e4 } };
|
|
6518
|
+
}
|
|
6519
|
+
let offered;
|
|
6520
|
+
try {
|
|
6521
|
+
offered = JSON.parse(fromUtf82(fromBase64url2(String(req.client_data_json ?? "")))).challenge;
|
|
6522
|
+
} catch {
|
|
6523
|
+
return { status: 400, body: { error: "unreadable client data" } };
|
|
6524
|
+
}
|
|
6525
|
+
const attempt = p.attempts[offered];
|
|
6526
|
+
if (!attempt || attempt.credential_id !== cred.credential_id) return { status: 400, body: { error: "This ceremony was not offered for this passkey." } };
|
|
6527
|
+
if (Date.parse(attempt.expires_at) <= now().getTime()) return { status: 400, body: { error: "This ceremony has expired; start again." } };
|
|
6528
|
+
const consent = {
|
|
6529
|
+
binding_digest: bindingDigest(p.act),
|
|
6530
|
+
role: p.role,
|
|
6531
|
+
decision: "approve",
|
|
6532
|
+
signed_at: attempt.signed_at,
|
|
6533
|
+
kid: cred.kid,
|
|
6534
|
+
algorithm: "WebAuthn-ES256",
|
|
6535
|
+
authenticator_data: String(req.authenticator_data ?? ""),
|
|
6536
|
+
client_data_json: String(req.client_data_json ?? ""),
|
|
6537
|
+
signature: String(req.signature ?? "")
|
|
6538
|
+
};
|
|
6539
|
+
const payload = jcs(consentPayload(consent));
|
|
6540
|
+
const cd = clientData(consent.client_data_json, "webauthn.get", toBase64url2(sha256(payload)));
|
|
6541
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6542
|
+
const ad = fromBase64url2(consent.authenticator_data);
|
|
6543
|
+
if (ad.length < 37 || !equal2(ad.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The assertion is for another relying-party id." } };
|
|
6544
|
+
const v = verifySuite("WebAuthn-ES256", cred.public_key_pem, payload, consent);
|
|
6545
|
+
if (!v.ok) return { status: 400, body: { error: `The passkey's signature does not verify (${v.reason}).` } };
|
|
6546
|
+
const key_certs = await o.registry.chainFor(cred.kid);
|
|
6547
|
+
if (!key_certs.length) return { status: 409, body: { error: "This passkey is no longer certified (revoked or expired)." } };
|
|
6548
|
+
if (!await store.resolvePending(p.id, { status: "approved", consent, key_certs })) return { status: 409, body: { error: "This request is no longer pending." } };
|
|
6549
|
+
return { status: 200, body: { status: "approved" } };
|
|
6550
|
+
}
|
|
6551
|
+
if (method === "GET" && path === "/enroll") return { status: 200, type: "text/html", body: enrolPage(o.rpName ?? rpId) };
|
|
6552
|
+
if (method === "POST" && (path === "/nomos/enrol/begin" || path === "/nomos/enrol/finish")) {
|
|
6553
|
+
const req = await json();
|
|
6554
|
+
const claims = verifyEnrolmentToken(String(req?.token ?? ""), o.enrolmentAuthority, base, now());
|
|
6555
|
+
if ("error" in claims) return { status: 403, body: { error: claims.error } };
|
|
6556
|
+
if (!claims.roles.every((r) => o.roles.includes(r))) return { status: 403, body: { error: `This service does not enrol for ${claims.roles.join(", ")}.` } };
|
|
6557
|
+
if (path === "/nomos/enrol/begin") {
|
|
6558
|
+
if (await store.tokenUsed(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6559
|
+
const challenge2 = toBase64url2(randomBytes3(32));
|
|
6560
|
+
await store.saveEnrolmentChallenge(claims.nonce, challenge2);
|
|
6561
|
+
const existing = (await store.credentials()).filter((c) => c.approver === claims.approver);
|
|
6562
|
+
return { status: 200, body: {
|
|
6563
|
+
challenge: challenge2,
|
|
6564
|
+
rp: { id: rpId, name: o.rpName ?? rpId },
|
|
6565
|
+
user: { id: toBase64url2(utf82(claims.approver)), name: claims.approver, displayName: claims.approver },
|
|
6566
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
6567
|
+
authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
|
|
6568
|
+
attestation: "none",
|
|
6569
|
+
timeout: 12e4,
|
|
6570
|
+
excludeCredentials: existing.map((c) => ({ type: "public-key", id: c.credential_id })),
|
|
6571
|
+
approver: claims.approver,
|
|
6572
|
+
roles: claims.roles
|
|
6573
|
+
} };
|
|
6574
|
+
}
|
|
6575
|
+
const challenge = await store.enrolmentChallenge(claims.nonce);
|
|
6576
|
+
if (!challenge) return { status: 403, body: { error: "Start enrolment again: this link has been used or has no ceremony in progress." } };
|
|
6577
|
+
const cd = clientData(String(req.client_data_json ?? ""), "webauthn.create", challenge);
|
|
6578
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6579
|
+
let att;
|
|
6580
|
+
try {
|
|
6581
|
+
att = readAttestation(fromBase64url2(String(req.attestation_object ?? "")));
|
|
6582
|
+
} catch (e) {
|
|
6583
|
+
return { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6584
|
+
}
|
|
6585
|
+
if (!equal2(att.authData.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The passkey was created for another relying-party id." } };
|
|
6586
|
+
if (!(att.flags & 1) || !(att.flags & 4)) return { status: 400, body: { error: "The device did not verify the person (Face ID, Touch ID or PIN is required)." } };
|
|
6587
|
+
if (toBase64url2(att.credentialId) !== req.credential_id) return { status: 400, body: { error: "The credential id does not match the attestation." } };
|
|
6588
|
+
if (!await store.claimToken(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6589
|
+
const kid = kidOf(att.publicKeyPem);
|
|
6590
|
+
const certificate = await o.registry.issue({
|
|
6591
|
+
parent: o.enroller,
|
|
6592
|
+
child: { kid, publicKeyPem: att.publicKeyPem },
|
|
6593
|
+
scope: `consent:${claims.roles.join(",")} approver:${claims.approver} suite:WebAuthn-ES256`,
|
|
6594
|
+
days: o.certificateDays ?? 365
|
|
6595
|
+
});
|
|
6596
|
+
const label = String(req.label ?? "").slice(0, 60) || "Passkey";
|
|
6597
|
+
await store.addCredential({ credential_id: req.credential_id, approver: claims.approver, roles: claims.roles, kid, public_key_pem: att.publicKeyPem, label, created_at: now().toISOString() });
|
|
6598
|
+
return { status: 200, body: { enrolled: true, approver: claims.approver, roles: claims.roles, kid, certificate } };
|
|
6599
|
+
}
|
|
6600
|
+
return null;
|
|
6601
|
+
}
|
|
6602
|
+
async function fetchHandler(request) {
|
|
6603
|
+
const out = await route(request.method, new URL(request.url).pathname, () => request.json()).catch((e) => ({ status: 400, body: { error: String(e?.message ?? e) } }));
|
|
6604
|
+
if (!out) return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
|
|
6605
|
+
const html = out.type === "text/html";
|
|
6606
|
+
return new Response(html ? String(out.body) : JSON.stringify(out.body), { status: out.status, headers: { "content-type": html ? "text/html; charset=utf-8" : "application/json", ...html ? { "content-security-policy": CSP() } : {} } });
|
|
6607
|
+
}
|
|
6608
|
+
function node() {
|
|
6609
|
+
return async (req, res, next) => {
|
|
6610
|
+
const path = String(req.originalUrl ?? req.url ?? "").split("?")[0];
|
|
6611
|
+
const json = async () => {
|
|
6612
|
+
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
6613
|
+
const chunks = [];
|
|
6614
|
+
for await (const c of req) chunks.push(c);
|
|
6615
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
6616
|
+
};
|
|
6617
|
+
let out;
|
|
6618
|
+
try {
|
|
6619
|
+
out = await route(req.method, path, json);
|
|
6620
|
+
} catch (e) {
|
|
6621
|
+
out = { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6622
|
+
}
|
|
6623
|
+
if (!out) {
|
|
6624
|
+
if (next) return next();
|
|
6625
|
+
res.statusCode = 404;
|
|
6626
|
+
return res.end(JSON.stringify({ error: "Not found" }));
|
|
6627
|
+
}
|
|
6628
|
+
res.statusCode = out.status;
|
|
6629
|
+
const html = out.type === "text/html";
|
|
6630
|
+
res.setHeader("content-type", html ? "text/html; charset=utf-8" : "application/json");
|
|
6631
|
+
if (html) res.setHeader("content-security-policy", CSP());
|
|
6632
|
+
res.end(html ? String(out.body) : JSON.stringify(out.body));
|
|
6633
|
+
};
|
|
6634
|
+
}
|
|
6635
|
+
return { store, fetch: fetchHandler, express: node, node };
|
|
6636
|
+
}
|
|
6637
|
+
var STYLE = `body{font:15px/1.55 system-ui,-apple-system,sans-serif;max-width:560px;margin:40px auto;padding:0 16px;color:#111;background:#fff}
|
|
6638
|
+
@media (prefers-color-scheme:dark){body{color:#eee;background:#111}table td{border-color:#333}}
|
|
6639
|
+
h2{margin:0 0 12px}table{border-collapse:collapse;margin:12px 0}td{padding:6px 10px;border-bottom:1px solid #ddd}
|
|
6640
|
+
button{font:inherit;padding:10px 18px;border-radius:8px;border:1px solid #888;background:transparent;color:inherit;cursor:pointer;margin:4px 8px 4px 0}
|
|
6641
|
+
button.primary{background:#2563eb;border-color:#2563eb;color:#fff}.muted{color:#777;font-size:13px}#msg{margin-top:16px;font-weight:600}`;
|
|
6642
|
+
var B642 = `const b64u={enc:b=>btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,''),dec:s=>Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')+'='.repeat((4-s.length%4)%4)),c=>c.charCodeAt(0))};
|
|
6643
|
+
const post=(u,b)=>fetch(u,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b)}).then(async r=>({ok:r.ok,body:await r.json().catch(()=>({}))}));
|
|
6644
|
+
const say=t=>{document.getElementById('msg').textContent=t};`;
|
|
6645
|
+
function approvalPage(p, status, devices) {
|
|
6646
|
+
const a = p.act.action ?? {};
|
|
6647
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
6648
|
+
const device = (d) => `<button class="primary" data-cred="${esc(d.credential_id)}">Approve${d.approver ? ` as ${esc(d.approver)}` : ""} \xB7 ${esc(d.label)}</button>`;
|
|
6649
|
+
const choose = devices === null ? `<p><label>Your approver id <input id="who" autocomplete="username"></label> <button id="find">Continue</button></p><div id="devices"></div>` : devices.length ? `<div id="devices">${devices.map(device).join("")}</div>` : `<p class="muted">No passkey is enrolled for ${esc(p.role)} yet.</p>`;
|
|
6650
|
+
const body = status === "pending" ? `${choose}<button id="refuse">Refuse</button>` : `<p><b>${esc(status)}</b></p>`;
|
|
6651
|
+
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Approve: ${esc(a.type)}</title><style>${STYLE}</style>
|
|
6652
|
+
<h2>Approve as ${esc(p.role)}?</h2>
|
|
6653
|
+
<p><b>${esc(a.type)}</b>${a.subject ? ` for <b>${esc(a.subject)}</b>` : ""}, at <b>${esc(p.act.relying_party)}</b>, under ${esc(p.act.authority?.artifact?.meta?.artifact_id)} v${esc(p.act.authority?.artifact?.meta?.version)}.</p>
|
|
6654
|
+
<table>${params}</table>
|
|
6655
|
+
<p class="muted">Your passkey signs this exact change and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
|
|
6656
|
+
${body}<div id="msg" data-id="${esc(p.id)}"></div>
|
|
6657
|
+
<script>${APPROVE_JS}</script>`;
|
|
6658
|
+
}
|
|
6659
|
+
var APPROVE_JS = `${B642}
|
|
6660
|
+
const id=document.getElementById('msg').dataset.id;
|
|
6661
|
+
async function approve(credential_id){
|
|
6662
|
+
try{
|
|
6663
|
+
const o=await post('/approve/'+id+'/options',{credential_id});
|
|
6664
|
+
if(!o.ok) return say(o.body.error||'Not allowed.');
|
|
6665
|
+
const a=await navigator.credentials.get({publicKey:{challenge:b64u.dec(o.body.challenge),rpId:o.body.rpId,allowCredentials:o.body.allowCredentials.map(c=>({type:c.type,id:b64u.dec(c.id)})),userVerification:'required',timeout:o.body.timeout}});
|
|
6666
|
+
const r=await post('/approve/'+id+'/assertion',{credential_id,authenticator_data:b64u.enc(a.response.authenticatorData),client_data_json:b64u.enc(a.response.clientDataJSON),signature:b64u.enc(a.response.signature)});
|
|
6667
|
+
say(r.ok?'Approved. You can close this page.':(r.body.error||'Not approved.'));
|
|
6668
|
+
if(r.ok) document.querySelectorAll('button').forEach(b=>b.disabled=true);
|
|
6669
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6670
|
+
}
|
|
6671
|
+
async function refuse(){const r=await post('/approve/'+id+'/refuse',{});say(r.ok?'Refused.':(r.body.error||''));if(r.ok)document.querySelectorAll('button').forEach(b=>b.disabled=true)}
|
|
6672
|
+
function wire(root){root.querySelectorAll('[data-cred]').forEach(b=>b.addEventListener('click',()=>approve(b.dataset.cred)))}
|
|
6673
|
+
wire(document);
|
|
6674
|
+
document.getElementById('refuse')?.addEventListener('click',refuse);
|
|
6675
|
+
document.getElementById('find')?.addEventListener('click',async()=>{
|
|
6676
|
+
const r=await post('/approve/'+id+'/devices',{approver:document.getElementById('who').value});
|
|
6677
|
+
const box=document.getElementById('devices');box.textContent='';
|
|
6678
|
+
if(!r.body.devices?.length) return say('No passkey of yours approves this.');
|
|
6679
|
+
for(const d of r.body.devices){const b=document.createElement('button');b.className='primary';b.dataset.cred=d.credential_id;b.textContent='Approve \xB7 '+d.label;box.appendChild(b)}
|
|
6680
|
+
wire(box);
|
|
6681
|
+
});`;
|
|
6682
|
+
function enrolPage(rpName) {
|
|
6683
|
+
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Enrol a passkey</title><style>${STYLE}</style>
|
|
6684
|
+
<h2>Enrol a passkey for ${esc(rpName)}</h2>
|
|
6685
|
+
<p id="who" class="muted">Checking your enrolment link\u2026</p>
|
|
6686
|
+
<p><label>Name this device <input id="label" value="" placeholder="e.g. iPhone"></label></p>
|
|
6687
|
+
<button class="primary" id="go" disabled>Create passkey</button>
|
|
6688
|
+
<div id="msg"></div>
|
|
6689
|
+
<p class="muted">The passkey stays on your device. It can approve requests in your role and nothing else, and this link works once.</p>
|
|
6690
|
+
<script>${ENROL_JS}</script>`;
|
|
6691
|
+
}
|
|
6692
|
+
var ENROL_JS = `${B642}
|
|
6693
|
+
const token=location.hash.slice(1);let opts=null;
|
|
6694
|
+
(async()=>{const r=await post('/nomos/enrol/begin',{token});if(!r.ok){document.getElementById('who').textContent=r.body.error||'This link is not valid.';return}
|
|
6695
|
+
opts=r.body;document.getElementById('who').textContent='Enrolling '+opts.approver+' to approve as '+opts.roles.join(', ')+'.';document.getElementById('go').disabled=false})();
|
|
6696
|
+
async function enrol(){
|
|
6697
|
+
try{
|
|
6698
|
+
const c=await navigator.credentials.create({publicKey:{challenge:b64u.dec(opts.challenge),rp:opts.rp,user:{id:b64u.dec(opts.user.id),name:opts.user.name,displayName:opts.user.displayName},pubKeyCredParams:opts.pubKeyCredParams,authenticatorSelection:opts.authenticatorSelection,attestation:opts.attestation,timeout:opts.timeout,excludeCredentials:opts.excludeCredentials.map(x=>({type:x.type,id:b64u.dec(x.id)}))}});
|
|
6699
|
+
const r=await post('/nomos/enrol/finish',{token,label:document.getElementById('label').value,credential_id:b64u.enc(c.rawId),client_data_json:b64u.enc(c.response.clientDataJSON),attestation_object:b64u.enc(c.response.attestationObject)});
|
|
6700
|
+
say(r.ok?'Enrolled. This device can now approve as '+r.body.roles.join(', ')+'.':(r.body.error||'Not enrolled.'));
|
|
6701
|
+
if(r.ok) document.getElementById('go').disabled=true;
|
|
6702
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6703
|
+
}
|
|
6704
|
+
document.getElementById('go').addEventListener('click',enrol);`;
|
|
6705
|
+
var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js))))}'`;
|
|
6706
|
+
var CSP = () => `default-src 'none'; script-src ${scriptHash(APPROVE_JS)} ${scriptHash(ENROL_JS)}; style-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'`;
|
|
6707
|
+
|
|
5765
6708
|
// src/act/carrier.ts
|
|
5766
6709
|
var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
|
|
5767
6710
|
var NomosActError = class extends Error {
|
|
@@ -5798,6 +6741,7 @@ async function act(o) {
|
|
|
5798
6741
|
}
|
|
5799
6742
|
}
|
|
5800
6743
|
const testimony = [];
|
|
6744
|
+
const available = [...issuer2.key_certs ?? []];
|
|
5801
6745
|
const unavailable = [];
|
|
5802
6746
|
for (const claim of witnessed) {
|
|
5803
6747
|
const w = witnesses.find((x) => x.claims.includes(claim));
|
|
@@ -5821,8 +6765,10 @@ async function act(o) {
|
|
|
5821
6765
|
continue;
|
|
5822
6766
|
}
|
|
5823
6767
|
testimony.push(t);
|
|
6768
|
+
if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
|
|
5824
6769
|
}
|
|
5825
|
-
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts:
|
|
6770
|
+
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
|
|
6771
|
+
present(theAct, available);
|
|
5826
6772
|
let { status, body } = await submit(f, rp.act_endpoint, theAct);
|
|
5827
6773
|
let approvalUrl;
|
|
5828
6774
|
while (status === 202 && body.decision === "ESCALATED") {
|
|
@@ -5830,16 +6776,18 @@ async function act(o) {
|
|
|
5830
6776
|
const endpoint = (issuer2.approvals ?? []).find((a) => a.role === role)?.endpoint;
|
|
5831
6777
|
if (!endpoint) return done("escalated", body, theAct, { summary: `Needs approval from ${role}, and ${issuer2.issuer_id} publishes no approval endpoint for that role.` });
|
|
5832
6778
|
const deadline = Math.min(Date.parse(theAct.expires_at), Date.now() + (o.approvalTimeoutMs ?? Infinity));
|
|
5833
|
-
const
|
|
6779
|
+
const answer2 = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
|
|
5834
6780
|
approvalUrl = url;
|
|
5835
6781
|
o.onApproval?.({ role, url, actDigest: body.act_digest });
|
|
5836
6782
|
});
|
|
5837
|
-
if (
|
|
5838
|
-
if (!
|
|
6783
|
+
if (answer2 === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
|
|
6784
|
+
if (!answer2) {
|
|
5839
6785
|
const expired = Date.now() >= Date.parse(theAct.expires_at);
|
|
5840
6786
|
return done("escalated", body, theAct, { approvalUrl, summary: expired ? `Not approved by ${role} before the Act expired at ${theAct.expires_at}; build a new Act to ask again.` : `Waiting for approval from ${role}${approvalUrl ? ` at ${approvalUrl}` : ""}. Not approved yet; this Act stays valid until ${theAct.expires_at}.` });
|
|
5841
6787
|
}
|
|
5842
|
-
theAct.consents.push(consent);
|
|
6788
|
+
theAct.consents.push(answer2.consent);
|
|
6789
|
+
if (Array.isArray(answer2.keyCerts)) available.push(...answer2.keyCerts);
|
|
6790
|
+
present(theAct, available);
|
|
5843
6791
|
const before = Number(body.consents_valid ?? 0);
|
|
5844
6792
|
({ status, body } = await submit(f, rp.act_endpoint, theAct));
|
|
5845
6793
|
if (status === 202 && Number(body.consents_valid ?? 0) <= before) {
|
|
@@ -5860,6 +6808,13 @@ async function act(o) {
|
|
|
5860
6808
|
summary: kind === "committed" ? `Committed by ${rp.relying_party_id}${body.by_consent ? ", with approval" : ""}.` : kind === "denied" ? `Denied by rule ${body.rule ?? "(default)"}.` : kind === "replayed" ? "Already committed; this is the original receipt." : `Refused: ${body.decision}${body.reason_code ? ` (${body.reason_code})` : ""}.${body.hint ? " " + body.hint : ""}`
|
|
5861
6809
|
});
|
|
5862
6810
|
}
|
|
6811
|
+
function present(theAct, available) {
|
|
6812
|
+
const set = presentedCertificates(available, theAct);
|
|
6813
|
+
if (set.length > MAX_PRESENTED) {
|
|
6814
|
+
throw new NomosActError(`This Act relies on ${set.length} certificates; at most ${MAX_PRESENTED} can be presented (NOMOS-SPEC-008 \xA76 step 1). The issuer's hierarchy is too deep for one Act.`);
|
|
6815
|
+
}
|
|
6816
|
+
theAct.authority.key_certs = set;
|
|
6817
|
+
}
|
|
5863
6818
|
function done(status, verdict, theAct, extra) {
|
|
5864
6819
|
return { status, verdict, act: theAct, ...extra };
|
|
5865
6820
|
}
|
|
@@ -5877,13 +6832,13 @@ async function submit(f, endpoint, theAct) {
|
|
|
5877
6832
|
async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPending) {
|
|
5878
6833
|
const res = await f(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, act: { ...theAct, consents: [] } }) });
|
|
5879
6834
|
const body = await res.json().catch(() => ({}));
|
|
5880
|
-
if (res.status === 200 && body.status === "approved") return body.consent;
|
|
6835
|
+
if (res.status === 200 && body.status === "approved") return { consent: body.consent, keyCerts: body.key_certs };
|
|
5881
6836
|
if (res.status !== 202 || !body.poll_url) return null;
|
|
5882
6837
|
onPending(body.approval_url);
|
|
5883
6838
|
while (Date.now() < deadline) {
|
|
5884
6839
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
5885
6840
|
const p = await (await f(body.poll_url)).json().catch(() => ({}));
|
|
5886
|
-
if (p.status === "approved") return p.consent;
|
|
6841
|
+
if (p.status === "approved") return { consent: p.consent, keyCerts: p.key_certs };
|
|
5887
6842
|
if (p.status === "refused") return "refused";
|
|
5888
6843
|
if (p.status === "expired") return null;
|
|
5889
6844
|
}
|
|
@@ -5891,7 +6846,6 @@ async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPen
|
|
|
5891
6846
|
}
|
|
5892
6847
|
|
|
5893
6848
|
// src/act/roles.ts
|
|
5894
|
-
var import_node_crypto3 = require("crypto");
|
|
5895
6849
|
function serve(route) {
|
|
5896
6850
|
async function fetchHandler(request) {
|
|
5897
6851
|
const out = await route({ method: request.method, path: new URL(request.url).pathname, json: () => request.json() });
|
|
@@ -5906,7 +6860,7 @@ function serve(route) {
|
|
|
5906
6860
|
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
5907
6861
|
const chunks = [];
|
|
5908
6862
|
for await (const c of req) chunks.push(c);
|
|
5909
|
-
return JSON.parse(
|
|
6863
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
5910
6864
|
};
|
|
5911
6865
|
let out;
|
|
5912
6866
|
try {
|
|
@@ -5930,7 +6884,9 @@ var day = 864e5;
|
|
|
5930
6884
|
function issuer(o) {
|
|
5931
6885
|
const byId = new Map(o.authorities.map((a) => [a.meta.artifact_id, a]));
|
|
5932
6886
|
const base = o.origin.replace(/\/$/, "");
|
|
5933
|
-
const
|
|
6887
|
+
const pool = async () => typeof o.keyCerts === "function" ? await o.keyCerts() : o.keyCerts;
|
|
6888
|
+
const revocationsUrl = o.revocations ? `${base}/nomos/revocations` : o.revocationsUrl;
|
|
6889
|
+
const build = (keyCerts) => ({
|
|
5934
6890
|
nomos_document: "issuer",
|
|
5935
6891
|
version: "1",
|
|
5936
6892
|
issuer_id: o.issuerId,
|
|
@@ -5941,17 +6897,24 @@ function issuer(o) {
|
|
|
5941
6897
|
artifact_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}.nomos`,
|
|
5942
6898
|
in_force_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}/in-force`
|
|
5943
6899
|
})),
|
|
5944
|
-
key_certs:
|
|
6900
|
+
key_certs: keyCerts,
|
|
5945
6901
|
...o.witnesses ? { witnesses: o.witnesses } : {},
|
|
5946
6902
|
...o.approvals ? { approvals: o.approvals } : {},
|
|
5947
|
-
...
|
|
6903
|
+
...revocationsUrl ? { revocations_url: revocationsUrl } : {},
|
|
5948
6904
|
valid_until: new Date(Date.now() + day).toISOString()
|
|
5949
6905
|
});
|
|
6906
|
+
const document = () => {
|
|
6907
|
+
if (typeof o.keyCerts === "function") throw new Error("issuer(): keyCerts is a function, so the document must be read with `await resolveDocument()`");
|
|
6908
|
+
return build(o.keyCerts);
|
|
6909
|
+
};
|
|
6910
|
+
const resolveDocument = async () => build(await pool());
|
|
5950
6911
|
return {
|
|
5951
6912
|
document,
|
|
6913
|
+
resolveDocument,
|
|
5952
6914
|
...serve(async ({ method, path }) => {
|
|
5953
6915
|
if (method !== "GET") return null;
|
|
5954
|
-
if (path === WELL_KNOWN.issuer) return { status: 200, body:
|
|
6916
|
+
if (path === WELL_KNOWN.issuer) return { status: 200, body: await resolveDocument() };
|
|
6917
|
+
if (path === "/nomos/revocations" && o.revocations) return { status: 200, body: await o.revocations() };
|
|
5955
6918
|
const m = path.match(/^\/nomos\/authorities\/([^/]+?)(\.nomos|\/in-force)$/);
|
|
5956
6919
|
const art = m && byId.get(decodeURIComponent(m[1]));
|
|
5957
6920
|
if (!art) return null;
|
|
@@ -5986,13 +6949,14 @@ function witness(o) {
|
|
|
5986
6949
|
if (value2 === void 0) return { status: 404, body: { error: `${o.witnessId} knows nothing about ${about}.` } };
|
|
5987
6950
|
const max = o.maxValiditySeconds ?? 900;
|
|
5988
6951
|
const seconds = Math.max(1, Math.min(max, Number(req?.valid_for_seconds) || max));
|
|
5989
|
-
return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }) } };
|
|
6952
|
+
return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }), ...o.keyCerts?.length ? { key_certs: o.keyCerts } : {} } };
|
|
5990
6953
|
})
|
|
5991
6954
|
};
|
|
5992
6955
|
}
|
|
5993
6956
|
function approver(o) {
|
|
5994
6957
|
const base = o.origin.replace(/\/$/, "");
|
|
5995
6958
|
const pending = /* @__PURE__ */ new Map();
|
|
6959
|
+
const chain = o.keyCerts?.length ? { key_certs: o.keyCerts } : {};
|
|
5996
6960
|
const statusOf = (p) => p.status === "pending" && Date.parse(p.act.expires_at) < Date.now() ? "expired" : p.status;
|
|
5997
6961
|
return {
|
|
5998
6962
|
pending,
|
|
@@ -6002,9 +6966,9 @@ function approver(o) {
|
|
|
6002
6966
|
if (!o.roles.includes(role)) return { status: 404, body: { error: `This approver does not consent as ${role}.` } };
|
|
6003
6967
|
if (o.decide === "auto" || typeof o.decide === "function") {
|
|
6004
6968
|
const ok = o.decide === "auto" ? true : await o.decide({ role, act: act2 });
|
|
6005
|
-
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role) } } : { status: 200, body: { status: "refused" } };
|
|
6969
|
+
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role), ...chain } } : { status: 200, body: { status: "refused" } };
|
|
6006
6970
|
}
|
|
6007
|
-
const id =
|
|
6971
|
+
const id = randomUUID();
|
|
6008
6972
|
pending.set(id, { role, act: act2, status: "pending" });
|
|
6009
6973
|
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6010
6974
|
}
|
|
@@ -6013,7 +6977,7 @@ function approver(o) {
|
|
|
6013
6977
|
const p = pending.get(m[1]);
|
|
6014
6978
|
if (!p) return null;
|
|
6015
6979
|
const s = statusOf(p);
|
|
6016
|
-
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent } : {} } };
|
|
6980
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...chain } : {} } };
|
|
6017
6981
|
}
|
|
6018
6982
|
m = path.match(/^\/approve\/([0-9a-f-]+)(\/(approve|refuse))?$/);
|
|
6019
6983
|
if (m) {
|
|
@@ -6033,23 +6997,24 @@ function approver(o) {
|
|
|
6033
6997
|
})
|
|
6034
6998
|
};
|
|
6035
6999
|
}
|
|
6036
|
-
var
|
|
7000
|
+
var esc2 = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6037
7001
|
function page(p, status, id) {
|
|
6038
7002
|
const a = p.act.action ?? {};
|
|
6039
|
-
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${
|
|
7003
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc2(k)}</td><td><b>${esc2(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
6040
7004
|
const buttons = status === "pending" ? `<form method="post" action="/approve/${id}/approve" style="display:inline"><button style="padding:10px 18px">Approve</button></form>
|
|
6041
|
-
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${
|
|
6042
|
-
return `<!doctype html><meta charset="utf-8"><title>Approve: ${
|
|
7005
|
+
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc2(status)}</b></p>`;
|
|
7006
|
+
return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc2(a.type)}</title>
|
|
6043
7007
|
<body style="font:15px system-ui;max-width:560px;margin:40px auto;padding:0 16px">
|
|
6044
|
-
<h2>Approve as ${
|
|
6045
|
-
<p><b>${
|
|
7008
|
+
<h2>Approve as ${esc2(p.role)}?</h2>
|
|
7009
|
+
<p><b>${esc2(a.type)}</b>${a.subject ? ` for <b>${esc2(a.subject)}</b>` : ""}, at <b>${esc2(p.act.relying_party)}</b>, under ${esc2(p.act.authority?.artifact?.meta?.artifact_id)} v${esc2(p.act.authority?.artifact?.meta?.version)}.</p>
|
|
6046
7010
|
<table style="border-collapse:collapse" cellpadding="4">${params}</table>
|
|
6047
|
-
<p style="color:#666">Your approval signs this exact change (digest ${
|
|
7011
|
+
<p style="color:#666">Your approval signs this exact change (digest ${esc2(bindingDigest(p.act).slice(0, 16))}\u2026) and nothing else. It expires at ${esc2(p.act.expires_at)}.</p>
|
|
6048
7012
|
${buttons}</body>`;
|
|
6049
7013
|
}
|
|
6050
7014
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6051
7015
|
0 && (module.exports = {
|
|
6052
7016
|
ACT_MEDIA_TYPE,
|
|
7017
|
+
MAX_PRESENTED,
|
|
6053
7018
|
Nomos,
|
|
6054
7019
|
NomosAPIError,
|
|
6055
7020
|
NomosActError,
|
|
@@ -6064,6 +7029,8 @@ ${buttons}</body>`;
|
|
|
6064
7029
|
NomosRateLimitError,
|
|
6065
7030
|
NomosValidationError,
|
|
6066
7031
|
NomosWebhookSignatureError,
|
|
7032
|
+
RegistryError,
|
|
7033
|
+
RevocationUnavailable,
|
|
6067
7034
|
WELL_KNOWN,
|
|
6068
7035
|
accept,
|
|
6069
7036
|
act,
|
|
@@ -6071,24 +7038,40 @@ ${buttons}</body>`;
|
|
|
6071
7038
|
bindingDigest,
|
|
6072
7039
|
buildAct,
|
|
6073
7040
|
can,
|
|
7041
|
+
certificateRegistry,
|
|
6074
7042
|
certify,
|
|
6075
7043
|
fetchDocument,
|
|
6076
7044
|
generateSigner,
|
|
6077
7045
|
issuer,
|
|
6078
7046
|
kidOf,
|
|
6079
7047
|
localSigner,
|
|
7048
|
+
memoryPasskeyStore,
|
|
7049
|
+
memoryRegistryStore,
|
|
6080
7050
|
memoryStore,
|
|
6081
7051
|
nomos,
|
|
6082
7052
|
originOf,
|
|
7053
|
+
passkeyApprover,
|
|
7054
|
+
postgresPasskeyStore,
|
|
7055
|
+
postgresRegistryStore,
|
|
7056
|
+
postgresStore,
|
|
7057
|
+
presentedCertificates,
|
|
7058
|
+
readAttestation,
|
|
7059
|
+
reliedKids,
|
|
7060
|
+
revocationListCertificates,
|
|
7061
|
+
revocationSource,
|
|
6083
7062
|
seal,
|
|
6084
7063
|
seededSigner,
|
|
6085
7064
|
signConsent,
|
|
7065
|
+
signEnrolmentToken,
|
|
6086
7066
|
signInForce,
|
|
7067
|
+
signRevocation,
|
|
7068
|
+
signRevocationList,
|
|
6087
7069
|
signTestimony,
|
|
6088
7070
|
statusFor,
|
|
6089
7071
|
verifyAct,
|
|
6090
7072
|
verifyArtifact,
|
|
6091
7073
|
verifyChain,
|
|
7074
|
+
verifyEnrolmentToken,
|
|
6092
7075
|
verifySeal,
|
|
6093
7076
|
witness
|
|
6094
7077
|
});
|