@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.js
CHANGED
|
@@ -812,7 +812,7 @@ var Nomos = class {
|
|
|
812
812
|
}
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
-
//
|
|
815
|
+
// node_modules/@noble/hashes/_u64.js
|
|
816
816
|
var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
|
|
817
817
|
var _32n = /* @__PURE__ */ BigInt(32);
|
|
818
818
|
function fromBig(n, le = false) {
|
|
@@ -855,7 +855,7 @@ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0
|
|
|
855
855
|
var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
856
856
|
var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
857
857
|
|
|
858
|
-
//
|
|
858
|
+
// node_modules/@noble/hashes/utils.js
|
|
859
859
|
function isBytes(a) {
|
|
860
860
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
861
861
|
}
|
|
@@ -1020,7 +1020,7 @@ var oidNist = (suffix) => ({
|
|
|
1020
1020
|
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
1021
1021
|
});
|
|
1022
1022
|
|
|
1023
|
-
//
|
|
1023
|
+
// node_modules/@noble/hashes/_md.js
|
|
1024
1024
|
function Chi(a, b, c) {
|
|
1025
1025
|
return a & b ^ ~a & c;
|
|
1026
1026
|
}
|
|
@@ -1151,7 +1151,7 @@ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
1151
1151
|
327033209
|
|
1152
1152
|
]);
|
|
1153
1153
|
|
|
1154
|
-
//
|
|
1154
|
+
// node_modules/@noble/hashes/sha2.js
|
|
1155
1155
|
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
1156
1156
|
1116352408,
|
|
1157
1157
|
1899447441,
|
|
@@ -1548,7 +1548,7 @@ var sha512 = /* @__PURE__ */ createHasher(
|
|
|
1548
1548
|
/* @__PURE__ */ oidNist(3)
|
|
1549
1549
|
);
|
|
1550
1550
|
|
|
1551
|
-
//
|
|
1551
|
+
// node_modules/@nomosprotocol/verify/dist/bytes.js
|
|
1552
1552
|
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1553
1553
|
var REV = new Map([...B64].map((c, i) => [c, i]));
|
|
1554
1554
|
var utf8 = (s) => new TextEncoder().encode(s);
|
|
@@ -1610,7 +1610,7 @@ function fromBase64url(s) {
|
|
|
1610
1610
|
return raw && toBase64url(raw) === s ? raw : null;
|
|
1611
1611
|
}
|
|
1612
1612
|
|
|
1613
|
-
//
|
|
1613
|
+
// node_modules/@nomosprotocol/verify/dist/jcs.js
|
|
1614
1614
|
function value(v) {
|
|
1615
1615
|
if (v === null)
|
|
1616
1616
|
return "null";
|
|
@@ -1644,7 +1644,7 @@ function codePointCompare(a, b) {
|
|
|
1644
1644
|
return A.length - B.length;
|
|
1645
1645
|
}
|
|
1646
1646
|
|
|
1647
|
-
//
|
|
1647
|
+
// node_modules/@noble/curves/utils.js
|
|
1648
1648
|
function aarray(item, title, inner = () => {
|
|
1649
1649
|
}) {
|
|
1650
1650
|
if (!Array.isArray(item))
|
|
@@ -1826,7 +1826,7 @@ function validateObject(object, fields = {}, optFields = {}, title = "object") {
|
|
|
1826
1826
|
iter(optFields, true);
|
|
1827
1827
|
}
|
|
1828
1828
|
|
|
1829
|
-
//
|
|
1829
|
+
// node_modules/@noble/curves/abstract/modular.js
|
|
1830
1830
|
var _0n2 = /* @__PURE__ */ BigInt(0);
|
|
1831
1831
|
var _1n2 = /* @__PURE__ */ BigInt(1);
|
|
1832
1832
|
var _2n = /* @__PURE__ */ BigInt(2);
|
|
@@ -2289,7 +2289,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
|
|
|
2289
2289
|
return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
|
|
2290
2290
|
}
|
|
2291
2291
|
|
|
2292
|
-
//
|
|
2292
|
+
// node_modules/@noble/curves/abstract/curve.js
|
|
2293
2293
|
var _0n3 = /* @__PURE__ */ BigInt(0);
|
|
2294
2294
|
var _1n3 = /* @__PURE__ */ BigInt(1);
|
|
2295
2295
|
var _4n2 = /* @__PURE__ */ BigInt(4);
|
|
@@ -2676,7 +2676,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
|
|
|
2676
2676
|
};
|
|
2677
2677
|
}
|
|
2678
2678
|
|
|
2679
|
-
//
|
|
2679
|
+
// node_modules/@noble/curves/abstract/edwards.js
|
|
2680
2680
|
var _0n4 = /* @__PURE__ */ BigInt(0);
|
|
2681
2681
|
var _1n4 = /* @__PURE__ */ BigInt(1);
|
|
2682
2682
|
var _2n2 = /* @__PURE__ */ BigInt(2);
|
|
@@ -3127,7 +3127,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3127
3127
|
});
|
|
3128
3128
|
}
|
|
3129
3129
|
|
|
3130
|
-
//
|
|
3130
|
+
// node_modules/@noble/curves/ed25519.js
|
|
3131
3131
|
var _1n5 = /* @__PURE__ */ BigInt(1);
|
|
3132
3132
|
var _2n3 = /* @__PURE__ */ BigInt(2);
|
|
3133
3133
|
var _5n2 = /* @__PURE__ */ BigInt(5);
|
|
@@ -3202,7 +3202,7 @@ function ed(opts) {
|
|
|
3202
3202
|
}
|
|
3203
3203
|
var ed25519 = /* @__PURE__ */ ed({});
|
|
3204
3204
|
|
|
3205
|
-
//
|
|
3205
|
+
// node_modules/@noble/hashes/hmac.js
|
|
3206
3206
|
var _HMAC = class {
|
|
3207
3207
|
oHash;
|
|
3208
3208
|
iHash;
|
|
@@ -3279,7 +3279,7 @@ var hmac = /* @__PURE__ */ (() => {
|
|
|
3279
3279
|
return hmac_;
|
|
3280
3280
|
})();
|
|
3281
3281
|
|
|
3282
|
-
//
|
|
3282
|
+
// node_modules/@noble/curves/abstract/der.js
|
|
3283
3283
|
var _0n5 = /* @__PURE__ */ BigInt(0);
|
|
3284
3284
|
var DERErr = class extends Error {
|
|
3285
3285
|
constructor(m = "") {
|
|
@@ -3373,18 +3373,18 @@ var _DER = {
|
|
|
3373
3373
|
}
|
|
3374
3374
|
},
|
|
3375
3375
|
toSig(bytes, maxScalarBytes) {
|
|
3376
|
-
const { Err: E, _int: int, _tlv:
|
|
3376
|
+
const { Err: E, _int: int, _tlv: tlv2 } = _DER;
|
|
3377
3377
|
if (maxScalarBytes !== void 0) {
|
|
3378
3378
|
asafenumber(maxScalarBytes, "maxScalarBytes");
|
|
3379
3379
|
if (maxScalarBytes < 1)
|
|
3380
3380
|
throw new E("invalid signature: maxScalarBytes must be positive");
|
|
3381
3381
|
}
|
|
3382
3382
|
const data = abytes2(bytes, void 0, "signature");
|
|
3383
|
-
const { v: seqBytes, l: seqLeftBytes } =
|
|
3383
|
+
const { v: seqBytes, l: seqLeftBytes } = tlv2.decode(48, data);
|
|
3384
3384
|
if (seqLeftBytes.length)
|
|
3385
3385
|
throw new E("invalid signature: left bytes after parsing");
|
|
3386
|
-
const { v: rBytes, l: rLeftBytes } =
|
|
3387
|
-
const { v: sBytes, l: sLeftBytes } =
|
|
3386
|
+
const { v: rBytes, l: rLeftBytes } = tlv2.decode(2, seqBytes);
|
|
3387
|
+
const { v: sBytes, l: sLeftBytes } = tlv2.decode(2, rLeftBytes);
|
|
3388
3388
|
if (sLeftBytes.length)
|
|
3389
3389
|
throw new E("invalid signature: left bytes after parsing");
|
|
3390
3390
|
if (maxScalarBytes !== void 0 && (rBytes.length > maxScalarBytes || sBytes.length > maxScalarBytes))
|
|
@@ -3392,12 +3392,12 @@ var _DER = {
|
|
|
3392
3392
|
return { r: int.decode(rBytes), s: int.decode(sBytes) };
|
|
3393
3393
|
},
|
|
3394
3394
|
hexFromSig(sig) {
|
|
3395
|
-
const { _tlv:
|
|
3395
|
+
const { _tlv: tlv2, _int: int } = _DER;
|
|
3396
3396
|
validateObject(sig, { r: "bigint", s: "bigint" }, {}, "sig");
|
|
3397
|
-
const rs =
|
|
3398
|
-
const ss =
|
|
3397
|
+
const rs = tlv2.encode(2, int.encode(sig.r));
|
|
3398
|
+
const ss = tlv2.encode(2, int.encode(sig.s));
|
|
3399
3399
|
const seq = rs + ss;
|
|
3400
|
-
return
|
|
3400
|
+
return tlv2.encode(48, seq);
|
|
3401
3401
|
}
|
|
3402
3402
|
};
|
|
3403
3403
|
var DER = /* @__PURE__ */ (() => {
|
|
@@ -3406,7 +3406,7 @@ var DER = /* @__PURE__ */ (() => {
|
|
|
3406
3406
|
return Object.freeze(_DER);
|
|
3407
3407
|
})();
|
|
3408
3408
|
|
|
3409
|
-
//
|
|
3409
|
+
// node_modules/@noble/curves/abstract/weierstrass.js
|
|
3410
3410
|
var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
|
|
3411
3411
|
function _splitEndoScalar(k, basis, n) {
|
|
3412
3412
|
aInRange("scalar", k, _0n6, n);
|
|
@@ -4238,7 +4238,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
4238
4238
|
});
|
|
4239
4239
|
}
|
|
4240
4240
|
|
|
4241
|
-
//
|
|
4241
|
+
// node_modules/@noble/curves/nist.js
|
|
4242
4242
|
var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
4243
4243
|
p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
|
|
4244
4244
|
n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
|
|
@@ -4251,21 +4251,21 @@ var p256_CURVE = /* @__PURE__ */ (() => ({
|
|
|
4251
4251
|
var p256_Point = /* @__PURE__ */ weierstrass(p256_CURVE);
|
|
4252
4252
|
var p256 = /* @__PURE__ */ ecdsa(p256_Point, sha256);
|
|
4253
4253
|
|
|
4254
|
-
//
|
|
4254
|
+
// node_modules/@nomosprotocol/verify/dist/suites.js
|
|
4255
4255
|
var SUITES = ["Ed25519", "ES256", "WebAuthn-ES256"];
|
|
4256
4256
|
var N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
|
|
4257
4257
|
var ED25519_PREFIX = Uint8Array.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]);
|
|
4258
4258
|
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]);
|
|
4259
4259
|
var SUITES_FOR = { "Ed25519": ["Ed25519"], "P-256": ["ES256", "WebAuthn-ES256"] };
|
|
4260
4260
|
var isSuite = (s) => typeof s === "string" && SUITES.includes(s);
|
|
4261
|
-
function pemDer(
|
|
4262
|
-
if (typeof
|
|
4261
|
+
function pemDer(pem2) {
|
|
4262
|
+
if (typeof pem2 !== "string")
|
|
4263
4263
|
return null;
|
|
4264
|
-
const m =
|
|
4264
|
+
const m = pem2.match(/^\s*-----BEGIN PUBLIC KEY-----\s*([A-Za-z0-9+/=\s]+?)\s*-----END PUBLIC KEY-----\s*$/);
|
|
4265
4265
|
return m ? fromBase64(m[1].replace(/\s+/g, "")) : null;
|
|
4266
4266
|
}
|
|
4267
|
-
function loadKey(
|
|
4268
|
-
const der = pemDer(
|
|
4267
|
+
function loadKey(pem2) {
|
|
4268
|
+
const der = pemDer(pem2);
|
|
4269
4269
|
if (!der)
|
|
4270
4270
|
return { reason: "invalid_key" };
|
|
4271
4271
|
if (der.length === 44 && equal(der.subarray(0, 12), ED25519_PREFIX)) {
|
|
@@ -4289,16 +4289,16 @@ function loadKey(pem) {
|
|
|
4289
4289
|
}
|
|
4290
4290
|
return { reason: "invalid_key" };
|
|
4291
4291
|
}
|
|
4292
|
-
function kidOf(
|
|
4293
|
-
const k = loadKey(
|
|
4292
|
+
function kidOf(pem2) {
|
|
4293
|
+
const k = loadKey(pem2);
|
|
4294
4294
|
return "reason" in k ? null : toBase64url(sha256(k.der)).slice(0, 16);
|
|
4295
4295
|
}
|
|
4296
4296
|
var toBigInt = (b) => b.reduce((n, x) => n << 8n | BigInt(x), 0n);
|
|
4297
|
-
function verifySuite(suite,
|
|
4297
|
+
function verifySuite(suite, pem2, payload, proof) {
|
|
4298
4298
|
const fail2 = (reason) => ({ ok: false, reason });
|
|
4299
4299
|
if (!isSuite(suite))
|
|
4300
4300
|
return fail2("unknown_suite");
|
|
4301
|
-
const k = loadKey(
|
|
4301
|
+
const k = loadKey(pem2);
|
|
4302
4302
|
if ("reason" in k)
|
|
4303
4303
|
return fail2("invalid_key");
|
|
4304
4304
|
if (!SUITES_FOR[k.type].includes(suite))
|
|
@@ -4360,7 +4360,7 @@ function verifySuite(suite, pem, payload, proof) {
|
|
|
4360
4360
|
return ok ? { ok: true } : fail2("bad_signature");
|
|
4361
4361
|
}
|
|
4362
4362
|
|
|
4363
|
-
//
|
|
4363
|
+
// node_modules/@nomosprotocol/verify/dist/scope.js
|
|
4364
4364
|
var ARTIFACT_DIMENSIONS = ["artifact", "industry", "jurisdiction"];
|
|
4365
4365
|
var SET_DIMENSIONS = ["claim", "consent", "suite"];
|
|
4366
4366
|
var UNRESTRICTED = { terms: {} };
|
|
@@ -4479,7 +4479,7 @@ function formatScope(scope) {
|
|
|
4479
4479
|
return Object.entries(scope.terms).map(([d, v]) => `${d}:${v}`).join(" ");
|
|
4480
4480
|
}
|
|
4481
4481
|
|
|
4482
|
-
//
|
|
4482
|
+
// node_modules/@nomosprotocol/verify/dist/chain.js
|
|
4483
4483
|
var MAX_CHAIN = 20;
|
|
4484
4484
|
var CERT_SUITES = ["Ed25519", "ES256"];
|
|
4485
4485
|
var SEAL_SUITES = ["Ed25519", "ES256"];
|
|
@@ -4571,7 +4571,7 @@ var Memo = class {
|
|
|
4571
4571
|
}
|
|
4572
4572
|
};
|
|
4573
4573
|
function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo) {
|
|
4574
|
-
let
|
|
4574
|
+
let pem2 = rootPem, scope = UNRESTRICTED;
|
|
4575
4575
|
const hops = [];
|
|
4576
4576
|
for (const cert of path) {
|
|
4577
4577
|
const f = memo.of(cert);
|
|
@@ -4581,7 +4581,7 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4581
4581
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: "kid_mismatch" });
|
|
4582
4582
|
if (!suitePermitted(scope, cert.algorithm))
|
|
4583
4583
|
return fail("OUT_OF_SCOPE", { dimension: "suite", reason_code: "suite_not_permitted" });
|
|
4584
|
-
const sig = memo.sig(cert,
|
|
4584
|
+
const sig = memo.sig(cert, pem2);
|
|
4585
4585
|
if (!sig.ok)
|
|
4586
4586
|
return fail("ISSUER_NOT_RECOGNIZED", { reason_code: sig.reason });
|
|
4587
4587
|
const issued = parseTime(cert.issued_at), expires = parseTime(cert.expires_at);
|
|
@@ -4601,14 +4601,14 @@ function validate(path, rootPem, now, purpose, revokedKids, revokedCerts, memo)
|
|
|
4601
4601
|
const narrowing = isNarrowerOrEqual(f.scope.scope, scope);
|
|
4602
4602
|
if (!narrowing.ok)
|
|
4603
4603
|
return fail("OUT_OF_SCOPE", { dimension: narrowing.dimension, reason_code: "widening" });
|
|
4604
|
-
hops.push({ cert, parentPem:
|
|
4605
|
-
|
|
4604
|
+
hops.push({ cert, parentPem: pem2, parentScope: scope });
|
|
4605
|
+
pem2 = cert.child_public_key_pem;
|
|
4606
4606
|
scope = mergeScopes(scope, f.scope.scope);
|
|
4607
4607
|
}
|
|
4608
4608
|
const admitted = purpose(scope);
|
|
4609
4609
|
if (!admitted.ok)
|
|
4610
4610
|
return fail("OUT_OF_SCOPE", { dimension: admitted.dimension });
|
|
4611
|
-
return { decision: "RESOLVED", pem, path: [], effective: scope, hops };
|
|
4611
|
+
return { decision: "RESOLVED", pem: pem2, path: [], effective: scope, hops };
|
|
4612
4612
|
}
|
|
4613
4613
|
function resolveKey(o) {
|
|
4614
4614
|
const revokedKids = o.revokedKids ?? /* @__PURE__ */ new Set(), revokedCerts = o.revokedCerts ?? /* @__PURE__ */ new Set();
|
|
@@ -4694,7 +4694,7 @@ function verifyArtifact(o) {
|
|
|
4694
4694
|
}
|
|
4695
4695
|
}
|
|
4696
4696
|
|
|
4697
|
-
//
|
|
4697
|
+
// node_modules/@nomosprotocol/verify/dist/evaluate.js
|
|
4698
4698
|
var jcsString = (v) => canonicalize(v);
|
|
4699
4699
|
var FIELD = /* @__PURE__ */ Symbol("field");
|
|
4700
4700
|
var ABSENT = /* @__PURE__ */ Symbol("absent");
|
|
@@ -5023,7 +5023,7 @@ function evaluate(artifact, facts) {
|
|
|
5023
5023
|
return ev;
|
|
5024
5024
|
}
|
|
5025
5025
|
|
|
5026
|
-
//
|
|
5026
|
+
// node_modules/@nomosprotocol/verify/dist/act.js
|
|
5027
5027
|
var MAX_WINDOW_SECONDS = 300;
|
|
5028
5028
|
var MAX_IN_FORCE_SECONDS = 86400;
|
|
5029
5029
|
var TESTIMONY_SUITES = ["Ed25519", "ES256"];
|
|
@@ -5061,7 +5061,7 @@ function hasAmbiguousEscalation(artifact) {
|
|
|
5061
5061
|
return [...named.values()].some((ids) => ids.size > 1);
|
|
5062
5062
|
}
|
|
5063
5063
|
var proofOf = (st) => st.algorithm === "WebAuthn-ES256" ? pick(st, WEBAUTHN_FIELDS) : { signature: st.signature };
|
|
5064
|
-
var statementSig = (
|
|
5064
|
+
var statementSig = (pem2, st, payload) => verifySuite(st.algorithm, pem2, jcs(payload), proofOf(st));
|
|
5065
5065
|
var time = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5066
5066
|
var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
5067
5067
|
function statementFailure(r, invalid, outOfScope, who) {
|
|
@@ -5226,11 +5226,11 @@ function verifyUnsafe(a, o) {
|
|
|
5226
5226
|
return { decision: "AUTHORIZED", commit: true, ...base };
|
|
5227
5227
|
if (ev.outcome === "block")
|
|
5228
5228
|
return { decision: "DENIED", commit: false, ...base };
|
|
5229
|
-
const
|
|
5230
|
-
if (!
|
|
5229
|
+
const esc3 = (art.governance?.escalations ?? []).find((e) => e?.id === ev.escalation_id);
|
|
5230
|
+
if (!esc3)
|
|
5231
5231
|
return { decision: "ESCALATED", commit: false, escalation_id: ev.escalation_id, consents_valid: 0, consents_required: null, ...base };
|
|
5232
|
-
const role =
|
|
5233
|
-
const needN = Math.trunc(Number(
|
|
5232
|
+
const role = esc3.role_required;
|
|
5233
|
+
const needN = Math.trunc(Number(esc3.required_consents ?? 1));
|
|
5234
5234
|
const approvers = /* @__PURE__ */ new Set();
|
|
5235
5235
|
for (const c of Array.isArray(act2.consents) ? act2.consents : []) {
|
|
5236
5236
|
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")) {
|
|
@@ -5256,7 +5256,7 @@ function verifyUnsafe(a, o) {
|
|
|
5256
5256
|
return { decision: "ESCALATED", commit: false, ...counted };
|
|
5257
5257
|
}
|
|
5258
5258
|
|
|
5259
|
-
//
|
|
5259
|
+
// node_modules/@nomosprotocol/verify/dist/seal.js
|
|
5260
5260
|
function verifySeal(artifact, publicKeyPem) {
|
|
5261
5261
|
const bad = checkSealShape(artifact);
|
|
5262
5262
|
if (bad)
|
|
@@ -5270,6 +5270,83 @@ function verifySeal(artifact, publicKeyPem) {
|
|
|
5270
5270
|
return sealFailure(artifact, publicKeyPem) ?? { decision: "VALID", kid };
|
|
5271
5271
|
}
|
|
5272
5272
|
|
|
5273
|
+
// node_modules/@nomosprotocol/verify/dist/revocation.js
|
|
5274
|
+
var MAX_LIST_CERTS = 1e3;
|
|
5275
|
+
var REVOCATION_SUITES = ["Ed25519", "ES256"];
|
|
5276
|
+
var CERT_FIELDS2 = ["parent_kid", "child_kid", "child_public_key_pem", "issued_at", "expires_at", "algorithm", "signature"];
|
|
5277
|
+
var FP = /^[0-9a-f]{64}$/;
|
|
5278
|
+
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 };
|
|
5279
|
+
var revocationListPayload = (l) => ({ generated_at: l.generated_at, statements: l.statements });
|
|
5280
|
+
var forSigning = (suite) => (scope) => suitePermitted(scope, suite) ? { ok: true } : { ok: false, dimension: "suite" };
|
|
5281
|
+
var str = (v) => typeof v === "string" && v !== "";
|
|
5282
|
+
var time2 = (s) => typeof s === "string" ? Date.parse(s) : NaN;
|
|
5283
|
+
function signerPem(kid, pool, root, at, suite, revokedKids, revokedCerts) {
|
|
5284
|
+
const r = resolveKey({ targetKid: kid, pool, rootPublicKeyPem: root, purpose: forSigning(suite), now: new Date(at), revokedKids, revokedCerts });
|
|
5285
|
+
return r.decision === "RESOLVED" ? r.pem : null;
|
|
5286
|
+
}
|
|
5287
|
+
function evaluateStatement(st, pool, rootPublicKeyPem, now) {
|
|
5288
|
+
if (st === null || typeof st !== "object" || Array.isArray(st))
|
|
5289
|
+
return "bad_statement";
|
|
5290
|
+
const hasKid = "revoked_kid" in st, hasFp = "revoked_fingerprint" in st;
|
|
5291
|
+
if (hasKid === hasFp)
|
|
5292
|
+
return "bad_statement";
|
|
5293
|
+
const target = hasKid ? st.revoked_kid : st.revoked_fingerprint;
|
|
5294
|
+
if (!str(target) || hasFp && !FP.test(target))
|
|
5295
|
+
return "bad_statement";
|
|
5296
|
+
if (typeof st.reason !== "string" || !["issued_at", "kid", "algorithm", "signature"].every((f) => str(st[f])))
|
|
5297
|
+
return "bad_statement";
|
|
5298
|
+
if (!REVOCATION_SUITES.includes(st.algorithm))
|
|
5299
|
+
return "bad_statement";
|
|
5300
|
+
const issued = time2(st.issued_at);
|
|
5301
|
+
if (Number.isNaN(issued) || issued > now.getTime())
|
|
5302
|
+
return "bad_statement";
|
|
5303
|
+
const certs = pool.filter((c) => c.parent_kid === st.kid && (hasKid ? c.child_kid === target : fingerprint(c) === target));
|
|
5304
|
+
if (certs.length === 0)
|
|
5305
|
+
return "no_certificate";
|
|
5306
|
+
const parent = signerPem(st.kid, pool, rootPublicKeyPem, issued, st.algorithm, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
5307
|
+
if (!parent)
|
|
5308
|
+
return "signer_not_recognized";
|
|
5309
|
+
if (!certs.some((c) => verifySuite(c.algorithm, parent, jcs(certPayload(c)), { signature: c.signature }).ok))
|
|
5310
|
+
return "bad_certificate_signature";
|
|
5311
|
+
return verifySuite(st.algorithm, parent, jcs(revocationPayload(st)), { signature: st.signature }).ok ? null : "bad_signature";
|
|
5312
|
+
}
|
|
5313
|
+
function verifyRevocationList(list, o) {
|
|
5314
|
+
const L = list;
|
|
5315
|
+
if (L === null || typeof L !== "object" || !["generated_at", "kid", "algorithm", "signature"].every((f) => typeof L[f] === "string") || !Array.isArray(L.statements)) {
|
|
5316
|
+
return { decision: "MALFORMED", reason_code: "bad_list" };
|
|
5317
|
+
}
|
|
5318
|
+
const extra = L.key_certs ?? [];
|
|
5319
|
+
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"))) {
|
|
5320
|
+
return { decision: "MALFORMED", reason_code: "bad_list_certs" };
|
|
5321
|
+
}
|
|
5322
|
+
const seen = /* @__PURE__ */ new Set(), pool = [];
|
|
5323
|
+
for (const c of [...extra, ...o.ownCerts ?? []]) {
|
|
5324
|
+
const fp = fingerprint(c);
|
|
5325
|
+
if (!seen.has(fp)) {
|
|
5326
|
+
seen.add(fp);
|
|
5327
|
+
pool.push(c);
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set(), ignored = [];
|
|
5331
|
+
L.statements.forEach((st, index) => {
|
|
5332
|
+
const why = evaluateStatement(st, pool, o.rootPublicKeyPem, o.now);
|
|
5333
|
+
if (why)
|
|
5334
|
+
ignored.push({ index, reason: why });
|
|
5335
|
+
else if ("revoked_kid" in st)
|
|
5336
|
+
kids.add(st.revoked_kid);
|
|
5337
|
+
else
|
|
5338
|
+
certs.add(st.revoked_fingerprint);
|
|
5339
|
+
});
|
|
5340
|
+
const out = { decision: "OK", revoked_kids: [...kids].sort(), revoked_certs: [...certs].sort(), ignored };
|
|
5341
|
+
const generated = time2(L.generated_at);
|
|
5342
|
+
if (Number.isNaN(generated) || generated > o.now.getTime() || !REVOCATION_SUITES.includes(L.algorithm))
|
|
5343
|
+
return out;
|
|
5344
|
+
const signer = signerPem(L.kid, pool, o.rootPublicKeyPem, generated, L.algorithm, kids, certs);
|
|
5345
|
+
if (signer && verifySuite(L.algorithm, signer, jcs(revocationListPayload(L)), { signature: L.signature }).ok)
|
|
5346
|
+
out.as_of = L.generated_at;
|
|
5347
|
+
return out;
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5273
5350
|
// src/verify-chain.ts
|
|
5274
5351
|
var CHAIN_PROTOCOL_VERSION2 = "1";
|
|
5275
5352
|
var DEFAULT_BASE = "https://www.nomosprotocol.com";
|
|
@@ -5316,43 +5393,241 @@ async function verifyChainRemote(params, opts) {
|
|
|
5316
5393
|
throw new NomosAPIError(message, res.status, requestId);
|
|
5317
5394
|
}
|
|
5318
5395
|
|
|
5396
|
+
// src/act/bytes.ts
|
|
5397
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
5398
|
+
var fromUtf82 = (b) => new TextDecoder().decode(b);
|
|
5399
|
+
function concat2(...parts) {
|
|
5400
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
5401
|
+
let o = 0;
|
|
5402
|
+
for (const p of parts) {
|
|
5403
|
+
out.set(p, o);
|
|
5404
|
+
o += p.length;
|
|
5405
|
+
}
|
|
5406
|
+
return out;
|
|
5407
|
+
}
|
|
5408
|
+
function equal2(a, b) {
|
|
5409
|
+
if (a.length !== b.length) return false;
|
|
5410
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
5411
|
+
return true;
|
|
5412
|
+
}
|
|
5413
|
+
function fromHex(h) {
|
|
5414
|
+
const out = new Uint8Array(h.length / 2);
|
|
5415
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
5416
|
+
return out;
|
|
5417
|
+
}
|
|
5418
|
+
function toBase642(b) {
|
|
5419
|
+
let s = "";
|
|
5420
|
+
for (let i = 0; i < b.length; i += 32768) s += String.fromCharCode(...b.subarray(i, i + 32768));
|
|
5421
|
+
return btoa(s);
|
|
5422
|
+
}
|
|
5423
|
+
function fromBase642(s) {
|
|
5424
|
+
const bin = atob(s);
|
|
5425
|
+
const out = new Uint8Array(bin.length);
|
|
5426
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
5427
|
+
return out;
|
|
5428
|
+
}
|
|
5429
|
+
var toBase64url2 = (b) => toBase642(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5430
|
+
var fromBase64url2 = (s) => {
|
|
5431
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
5432
|
+
return fromBase642(t + "=".repeat((4 - t.length % 4) % 4));
|
|
5433
|
+
};
|
|
5434
|
+
function randomBytes3(n) {
|
|
5435
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
5436
|
+
}
|
|
5437
|
+
var randomUUID = () => globalThis.crypto.randomUUID();
|
|
5438
|
+
|
|
5319
5439
|
// src/act/signer.ts
|
|
5320
|
-
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign as nodeSign } from "crypto";
|
|
5321
|
-
var P256_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n;
|
|
5322
5440
|
function kidOf2(publicKeyPem) {
|
|
5323
|
-
const
|
|
5324
|
-
|
|
5441
|
+
const kid = kidOf(publicKeyPem);
|
|
5442
|
+
if (!kid) throw new Error("kidOf: not a canonical Ed25519 or P-256 public key (NOMOS-SPEC-010 \xA73.1)");
|
|
5443
|
+
return kid;
|
|
5444
|
+
}
|
|
5445
|
+
var ED25519_SPKI = fromHex("302a300506032b6570032100");
|
|
5446
|
+
var P256_SPKI = fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");
|
|
5447
|
+
var ED25519_PKCS8 = fromHex("302e020100300506032b657004220420");
|
|
5448
|
+
var P256_PKCS8_HEAD = fromHex("308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420");
|
|
5449
|
+
var P256_PKCS8_PUB = fromHex("a144034200");
|
|
5450
|
+
var OID_ED25519 = fromHex("2b6570");
|
|
5451
|
+
var OID_EC = fromHex("2a8648ce3d0201");
|
|
5452
|
+
var OID_P256 = fromHex("2a8648ce3d030107");
|
|
5453
|
+
function pem(label, der) {
|
|
5454
|
+
const b64 = toBase642(der);
|
|
5455
|
+
return `-----BEGIN ${label}-----
|
|
5456
|
+
${b64.match(/.{1,64}/g).join("\n")}
|
|
5457
|
+
-----END ${label}-----
|
|
5458
|
+
`;
|
|
5459
|
+
}
|
|
5460
|
+
function pemBody(text) {
|
|
5461
|
+
const m = text.trim().match(/^-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----$/);
|
|
5462
|
+
if (!m) throw new Error("Signer: expected a PEM private key");
|
|
5463
|
+
return { label: m[1], der: fromBase642(m[2].replace(/\s+/g, "")) };
|
|
5464
|
+
}
|
|
5465
|
+
function tlv(b, at) {
|
|
5466
|
+
const tag = b[at];
|
|
5467
|
+
let len = b[at + 1], start = at + 2;
|
|
5468
|
+
if (len & 128) {
|
|
5469
|
+
const n = len & 127;
|
|
5470
|
+
if (n < 1 || n > 2) throw new Error("Signer: unsupported DER length");
|
|
5471
|
+
len = 0;
|
|
5472
|
+
for (let i = 0; i < n; i++) len = len << 8 | b[start + i];
|
|
5473
|
+
start += n;
|
|
5474
|
+
}
|
|
5475
|
+
if (start + len > b.length) throw new Error("Signer: truncated DER");
|
|
5476
|
+
return { tag, start, end: start + len };
|
|
5477
|
+
}
|
|
5478
|
+
function children(b, outer) {
|
|
5479
|
+
const out = [];
|
|
5480
|
+
for (let at = outer.start; at < outer.end; ) {
|
|
5481
|
+
const t = tlv(b, at);
|
|
5482
|
+
out.push(t);
|
|
5483
|
+
at = t.end;
|
|
5484
|
+
}
|
|
5485
|
+
return out;
|
|
5325
5486
|
}
|
|
5326
|
-
function
|
|
5327
|
-
const
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5487
|
+
function sec1Scalar(b, seq) {
|
|
5488
|
+
const [version, d, ...rest] = children(b, seq);
|
|
5489
|
+
if (version.tag !== 2 || d.tag !== 4 || d.end - d.start !== 32) throw new Error("Signer: malformed EC private key");
|
|
5490
|
+
for (const r of rest) {
|
|
5491
|
+
if (r.tag === 160) {
|
|
5492
|
+
const oid = tlv(b, r.start);
|
|
5493
|
+
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)");
|
|
5494
|
+
}
|
|
5331
5495
|
}
|
|
5496
|
+
return b.slice(d.start, d.end);
|
|
5497
|
+
}
|
|
5498
|
+
function readPrivateKey(text) {
|
|
5499
|
+
const { label, der } = pemBody(text);
|
|
5500
|
+
const top = tlv(der, 0);
|
|
5501
|
+
if (label === "EC PRIVATE KEY") return { type: "ES256", key: sec1Scalar(der, top) };
|
|
5502
|
+
if (label !== "PRIVATE KEY") throw new Error(`Signer: unsupported key type "${label}" (PKCS#8 "PRIVATE KEY" or SEC1 "EC PRIVATE KEY")`);
|
|
5503
|
+
const [, alg, key] = children(der, top);
|
|
5504
|
+
const [oid, params] = children(der, alg);
|
|
5505
|
+
const algOid = der.subarray(oid.start, oid.end);
|
|
5506
|
+
if (equal2(algOid, OID_ED25519)) {
|
|
5507
|
+
const inner = tlv(der, key.start);
|
|
5508
|
+
if (inner.tag !== 4 || inner.end - inner.start !== 32) throw new Error("Signer: malformed Ed25519 private key");
|
|
5509
|
+
return { type: "Ed25519", key: der.slice(inner.start, inner.end) };
|
|
5510
|
+
}
|
|
5511
|
+
if (equal2(algOid, OID_EC)) {
|
|
5512
|
+
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)");
|
|
5513
|
+
return { type: "ES256", key: sec1Scalar(der, tlv(der, key.start)) };
|
|
5514
|
+
}
|
|
5515
|
+
throw new Error("Signer: only Ed25519 and P-256 keys are NOMOS signature suites (NOMOS-SPEC-010)");
|
|
5516
|
+
}
|
|
5517
|
+
function publicPem(s) {
|
|
5518
|
+
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)));
|
|
5519
|
+
}
|
|
5520
|
+
function privatePem(s) {
|
|
5521
|
+
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)));
|
|
5522
|
+
}
|
|
5523
|
+
function fromSecret(s) {
|
|
5524
|
+
const publicKeyPem = publicPem(s);
|
|
5332
5525
|
return {
|
|
5333
5526
|
kid: kidOf2(publicKeyPem),
|
|
5334
5527
|
publicKeyPem,
|
|
5335
|
-
algorithm:
|
|
5528
|
+
algorithm: s.type,
|
|
5336
5529
|
async sign(payload) {
|
|
5337
|
-
|
|
5338
|
-
const raw = nodeSign("sha256", Buffer.from(payload), { key: privateKey, dsaEncoding: "ieee-p1363" });
|
|
5339
|
-
let s = BigInt("0x" + raw.subarray(32).toString("hex"));
|
|
5340
|
-
if (s > P256_N / 2n) s = P256_N - s;
|
|
5341
|
-
return Buffer.concat([raw.subarray(0, 32), Buffer.from(s.toString(16).padStart(64, "0"), "hex")]).toString("base64");
|
|
5530
|
+
return toBase642(s.type === "Ed25519" ? ed25519.sign(payload, s.key) : p256.sign(payload, s.key, { prehash: true, lowS: true, format: "compact" }));
|
|
5342
5531
|
}
|
|
5343
5532
|
};
|
|
5344
5533
|
}
|
|
5345
5534
|
function localSigner(privateKeyPem) {
|
|
5346
|
-
return
|
|
5535
|
+
return fromSecret(readPrivateKey(privateKeyPem));
|
|
5347
5536
|
}
|
|
5348
5537
|
function generateSigner(algorithm = "Ed25519") {
|
|
5349
|
-
const
|
|
5350
|
-
return { ...
|
|
5538
|
+
const s = algorithm === "Ed25519" ? { type: "Ed25519", key: ed25519.utils.randomSecretKey() } : { type: "ES256", key: p256.utils.randomSecretKey() };
|
|
5539
|
+
return { ...fromSecret(s), privateKeyPem: privatePem(s) };
|
|
5351
5540
|
}
|
|
5352
5541
|
function seededSigner(seed) {
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5542
|
+
return fromSecret({ type: "Ed25519", key: sha2562(seed) });
|
|
5543
|
+
}
|
|
5544
|
+
var sha2562 = (s) => sha256(new TextEncoder().encode(s));
|
|
5545
|
+
|
|
5546
|
+
// src/act/certificates.ts
|
|
5547
|
+
var MAX_PRESENTED = 20;
|
|
5548
|
+
function reliedKids(act2) {
|
|
5549
|
+
const kids = [act2?.authority?.artifact?.seal?.kid, ...(act2?.testimony ?? []).map((t) => t?.kid), ...(act2?.consents ?? []).map((c) => c?.kid)];
|
|
5550
|
+
return [...new Set(kids.filter((k) => typeof k === "string" && k !== ""))];
|
|
5551
|
+
}
|
|
5552
|
+
function presentedCertificates(available, act2) {
|
|
5553
|
+
const unique = /* @__PURE__ */ new Map();
|
|
5554
|
+
for (const c of available) unique.set(fingerprint(c), c);
|
|
5555
|
+
const kids = new Set(reliedKids(act2)), chosen = /* @__PURE__ */ new Map();
|
|
5556
|
+
for (let grew = true; grew; ) {
|
|
5557
|
+
grew = false;
|
|
5558
|
+
for (const [fp, c] of unique) {
|
|
5559
|
+
if (!chosen.has(fp) && kids.has(c.child_kid)) {
|
|
5560
|
+
chosen.set(fp, c);
|
|
5561
|
+
kids.add(c.parent_kid);
|
|
5562
|
+
grew = true;
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
return [...chosen.values()];
|
|
5567
|
+
}
|
|
5568
|
+
async function signRevocation(parent, target, reason, issuedAt = /* @__PURE__ */ new Date()) {
|
|
5569
|
+
const st = "kid" in target ? { revoked_kid: target.kid, reason, issued_at: issuedAt.toISOString() } : { revoked_fingerprint: fingerprint(target.certificate), reason, issued_at: issuedAt.toISOString() };
|
|
5570
|
+
if ("certificate" in target && target.certificate.parent_kid !== parent.kid) throw new Error("signRevocation: only the parent that issued a certificate can revoke it");
|
|
5571
|
+
return { ...st, kid: parent.kid, algorithm: parent.algorithm, signature: await parent.sign(jcs(revocationPayload(st))) };
|
|
5572
|
+
}
|
|
5573
|
+
async function signRevocationList(signer, statements, keyCerts = [], generatedAt = /* @__PURE__ */ new Date()) {
|
|
5574
|
+
const L = { generated_at: generatedAt.toISOString(), kid: signer.kid, algorithm: signer.algorithm, statements, ...keyCerts.length ? { key_certs: keyCerts } : {} };
|
|
5575
|
+
L.signature = await signer.sign(jcs(revocationListPayload(L)));
|
|
5576
|
+
return L;
|
|
5577
|
+
}
|
|
5578
|
+
function revocationListCertificates(available, statements, listSignerKid) {
|
|
5579
|
+
const byFp = new Map(available.map((c) => [fingerprint(c), c]));
|
|
5580
|
+
const revokedCerts = statements.map((s) => s.revoked_fingerprint && byFp.get(s.revoked_fingerprint)).filter((c) => !!c);
|
|
5581
|
+
const kids = [listSignerKid, ...statements.flatMap((s) => [s.kid, s.revoked_kid].filter((k) => !!k)), ...revokedCerts.map((c) => c.parent_kid)];
|
|
5582
|
+
const closure = presentedCertificates(available, { authority: { artifact: { seal: {} } }, testimony: kids.map((kid) => ({ kid })) });
|
|
5583
|
+
const out = new Map(closure.map((c) => [fingerprint(c), c]));
|
|
5584
|
+
for (const c of revokedCerts) out.set(fingerprint(c), c);
|
|
5585
|
+
return [...out.values()];
|
|
5586
|
+
}
|
|
5587
|
+
var RevocationUnavailable = class extends Error {
|
|
5588
|
+
constructor(message, retryAfterSeconds) {
|
|
5589
|
+
super(message);
|
|
5590
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
5591
|
+
this.name = "RevocationUnavailable";
|
|
5592
|
+
}
|
|
5593
|
+
};
|
|
5594
|
+
function revocationSource(o) {
|
|
5595
|
+
const f = o.fetch ?? fetch, now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
5596
|
+
const maxAge = (o.maxAgeSeconds ?? 3600) * 1e3, refresh = (o.refreshSeconds ?? 60) * 1e3;
|
|
5597
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
5598
|
+
let asOf = 0, fetchedAt = 0, inFlight = null, lastError = "";
|
|
5599
|
+
async function load() {
|
|
5600
|
+
fetchedAt = now().getTime();
|
|
5601
|
+
try {
|
|
5602
|
+
const res = await f(o.url, { headers: { accept: "application/json" } });
|
|
5603
|
+
if (!res.ok) {
|
|
5604
|
+
lastError = `${o.url} answered ${res.status}`;
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5607
|
+
const r = verifyRevocationList(await res.json(), { rootPublicKeyPem: o.root, now: now(), ownCerts: o.ownCerts });
|
|
5608
|
+
if (r.decision !== "OK") {
|
|
5609
|
+
lastError = `${o.url} served a malformed list (${r.reason_code})`;
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
for (const k of r.revoked_kids) kids.add(k);
|
|
5613
|
+
for (const c of r.revoked_certs) certs.add(c);
|
|
5614
|
+
if (r.as_of) asOf = Math.max(asOf, Date.parse(r.as_of));
|
|
5615
|
+
else lastError = `${o.url}'s list is not signed by a key your root reaches, so it does not show it is current`;
|
|
5616
|
+
} catch (e) {
|
|
5617
|
+
lastError = `${o.url}: ${e?.message ?? e}`;
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
return async () => {
|
|
5621
|
+
const t = now().getTime();
|
|
5622
|
+
if (!inFlight && t - fetchedAt >= refresh) inFlight = load().finally(() => {
|
|
5623
|
+
inFlight = null;
|
|
5624
|
+
});
|
|
5625
|
+
if (inFlight) await inFlight;
|
|
5626
|
+
if (!asOf || t - asOf > maxAge) {
|
|
5627
|
+
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));
|
|
5628
|
+
}
|
|
5629
|
+
return { kids: new Set(kids), certs: new Set(certs) };
|
|
5630
|
+
};
|
|
5356
5631
|
}
|
|
5357
5632
|
|
|
5358
5633
|
// src/act/documents.ts
|
|
@@ -5457,6 +5732,11 @@ var Rollback = class extends Error {
|
|
|
5457
5732
|
this.answer = answer2;
|
|
5458
5733
|
}
|
|
5459
5734
|
};
|
|
5735
|
+
var LostRace = class extends Error {
|
|
5736
|
+
constructor() {
|
|
5737
|
+
super("replayed");
|
|
5738
|
+
}
|
|
5739
|
+
};
|
|
5460
5740
|
function accept(o) {
|
|
5461
5741
|
const kid = kidOf2(o.root);
|
|
5462
5742
|
for (const a of o.authorities) {
|
|
@@ -5464,7 +5744,7 @@ function accept(o) {
|
|
|
5464
5744
|
throw new Error(`accept(): authority ${a.artifactId} uses currencyPolicy "record" but has no inForceSealHash`);
|
|
5465
5745
|
}
|
|
5466
5746
|
}
|
|
5467
|
-
if (!o.store) console.warn("[nomos] accept(): using the in-memory store. Nonces and receipts are lost on restart. Development only.");
|
|
5747
|
+
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.");
|
|
5468
5748
|
const store = o.store ?? memoryStore();
|
|
5469
5749
|
const maxWindowSeconds = o.maxWindowSeconds ?? 900;
|
|
5470
5750
|
const maxActBytes = o.maxActBytes ?? 1024 * 1024;
|
|
@@ -5490,8 +5770,7 @@ function accept(o) {
|
|
|
5490
5770
|
valid_until: new Date(now().getTime() + (o.documentTtlSeconds ?? 86400) * 1e3).toISOString()
|
|
5491
5771
|
};
|
|
5492
5772
|
}
|
|
5493
|
-
|
|
5494
|
-
const rev = await o.revocations?.() ?? {};
|
|
5773
|
+
function verifyWith(act2, authority, facts, at, seen, rev) {
|
|
5495
5774
|
return verifyAct(act2, {
|
|
5496
5775
|
relyingPartyId: o.relyingPartyId,
|
|
5497
5776
|
rootPublicKeyPem: o.root,
|
|
@@ -5523,9 +5802,20 @@ function accept(o) {
|
|
|
5523
5802
|
const authority = o.authorities.find((a) => a.artifactId === artifactId);
|
|
5524
5803
|
if (!authority) return answer({ decision: "NOT_ACCEPTED", reason_code: "authority_not_accepted", artifact_id: artifactId ?? null });
|
|
5525
5804
|
if (!authority.actions.includes(act2?.action?.type)) return answer({ decision: "NOT_ACCEPTED", reason_code: "action_not_accepted", action_type: act2?.action?.type ?? null });
|
|
5805
|
+
let rev;
|
|
5806
|
+
try {
|
|
5807
|
+
rev = await o.revocations?.() ?? {};
|
|
5808
|
+
} catch (e) {
|
|
5809
|
+
const retry = e instanceof RevocationUnavailable ? e.retryAfterSeconds : 60;
|
|
5810
|
+
return {
|
|
5811
|
+
status: 503,
|
|
5812
|
+
headers: { "content-type": "application/json", "retry-after": String(retry) },
|
|
5813
|
+
body: { error: "Revocation status is not current, so nothing is committed. Resubmit this same Act later.", detail: String(e?.message ?? e), retry_after: retry }
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5526
5816
|
const verifiedAt = now();
|
|
5527
5817
|
const facts = await o.localFacts?.(act2) ?? {};
|
|
5528
|
-
const verdict =
|
|
5818
|
+
const verdict = verifyWith(act2, authority, facts, verifiedAt, await store.hasNonce(act2.nonce), rev);
|
|
5529
5819
|
const receiptOf = (v, f, committedAt) => ({
|
|
5530
5820
|
act_id: act2.act_id,
|
|
5531
5821
|
act_digest: String(v.act_digest ?? ""),
|
|
@@ -5545,35 +5835,34 @@ function accept(o) {
|
|
|
5545
5835
|
if (verdict.decision !== "AUTHORIZED" || o.mode === "shadow") {
|
|
5546
5836
|
await o.onVerdict?.(verdict, receiptOf(verdict, facts));
|
|
5547
5837
|
if (verdict.decision === "ESCALATED") {
|
|
5548
|
-
const
|
|
5549
|
-
return answer(verdict, { role:
|
|
5838
|
+
const esc3 = (act2.authority.artifact.governance?.escalations ?? []).find((e) => e?.id === verdict.escalation_id);
|
|
5839
|
+
return answer(verdict, { role: esc3?.role_required ?? null, retry_until: act2.expires_at });
|
|
5550
5840
|
}
|
|
5551
5841
|
if (o.mode === "shadow") return { ...answer(verdict), status: 200, body: { ...answer(verdict).body, shadow: true } };
|
|
5552
5842
|
return answer(verdict);
|
|
5553
5843
|
}
|
|
5554
5844
|
try {
|
|
5555
5845
|
return await store.transaction(async (t) => {
|
|
5556
|
-
if (!await t.claimNonce(act2.nonce))
|
|
5557
|
-
throw new Rollback(answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) }));
|
|
5558
|
-
}
|
|
5846
|
+
if (!await t.claimNonce(act2.nonce)) throw new LostRace();
|
|
5559
5847
|
let final = verdict, finalFacts = facts;
|
|
5560
5848
|
if (o.localFacts) {
|
|
5561
5849
|
const fresh = await o.localFacts(act2, t.tx);
|
|
5562
5850
|
if (!sameFacts(fresh, facts)) {
|
|
5563
5851
|
finalFacts = fresh;
|
|
5564
|
-
final =
|
|
5852
|
+
final = verifyWith(act2, authority, fresh, verifiedAt, false, rev);
|
|
5565
5853
|
if (final.decision !== "AUTHORIZED") throw new Rollback(answer(final));
|
|
5566
5854
|
}
|
|
5567
5855
|
}
|
|
5568
5856
|
const action = structuredClone({ type: act2.action.type, ...act2.action.subject !== void 0 ? { subject: act2.action.subject } : {}, params: act2.action.params ?? {} });
|
|
5569
5857
|
const result = await o.commit(action, { tx: t.tx, actId: act2.act_id });
|
|
5570
5858
|
const receipt = receiptOf(final, finalFacts, now());
|
|
5571
|
-
await t.putReceipt(act2.nonce, receipt);
|
|
5859
|
+
await t.putReceipt(act2.nonce, receipt, act2);
|
|
5572
5860
|
await o.onVerdict?.(final, receipt);
|
|
5573
5861
|
return answer(final, { receipt, ...result !== void 0 ? { result } : {} });
|
|
5574
5862
|
});
|
|
5575
5863
|
} catch (e) {
|
|
5576
5864
|
if (e instanceof Rollback) return e.answer;
|
|
5865
|
+
if (e instanceof LostRace) return answer({ decision: "REPLAYED" }, { act_id: act2.act_id, receipt: await store.receiptFor(act2.nonce) });
|
|
5577
5866
|
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) } };
|
|
5578
5867
|
}
|
|
5579
5868
|
}
|
|
@@ -5612,7 +5901,7 @@ function accept(o) {
|
|
|
5612
5901
|
if (size > maxActBytes) break;
|
|
5613
5902
|
chunks.push(c);
|
|
5614
5903
|
}
|
|
5615
|
-
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) :
|
|
5904
|
+
body = size > maxActBytes ? new Uint8Array(maxActBytes + 1) : concat2(...chunks);
|
|
5616
5905
|
}
|
|
5617
5906
|
const a = await handle({ method: req.method, headers: req.headers, body });
|
|
5618
5907
|
send(a.status, a.headers, JSON.stringify(a.body));
|
|
@@ -5623,7 +5912,7 @@ function accept(o) {
|
|
|
5623
5912
|
function consentOrigins(act2) {
|
|
5624
5913
|
const origins = (Array.isArray(act2?.consents) ? act2.consents : []).filter((c) => c?.algorithm === "WebAuthn-ES256" && typeof c.client_data_json === "string").map((c) => {
|
|
5625
5914
|
try {
|
|
5626
|
-
return JSON.parse(
|
|
5915
|
+
return JSON.parse(fromUtf82(fromBase64url2(c.client_data_json))).origin;
|
|
5627
5916
|
} catch {
|
|
5628
5917
|
return null;
|
|
5629
5918
|
}
|
|
@@ -5631,8 +5920,74 @@ function consentOrigins(act2) {
|
|
|
5631
5920
|
return origins.length ? { consent_origins: [...new Set(origins)] } : {};
|
|
5632
5921
|
}
|
|
5633
5922
|
|
|
5923
|
+
// src/act/postgres.ts
|
|
5924
|
+
var IDENT = /^[a-z_][a-z0-9_]{0,62}(\.[a-z_][a-z0-9_]{0,62})?$/;
|
|
5925
|
+
function postgresStore(pool, opts = {}) {
|
|
5926
|
+
const table = opts.table ?? "nomos_acts";
|
|
5927
|
+
if (!IDENT.test(table)) throw new Error(`postgresStore: "${table}" is not a plain lower-case table name`);
|
|
5928
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${table} (
|
|
5929
|
+
nonce text PRIMARY KEY, -- SPEC-008 \xA75.2: the nonce ledger
|
|
5930
|
+
act_id text NOT NULL,
|
|
5931
|
+
receipt json NOT NULL, -- SPEC-008 \xA79; json, not jsonb: kept byte for byte
|
|
5932
|
+
act json NOT NULL, -- the exact Act verified, for re-verification
|
|
5933
|
+
committed_at timestamptz NOT NULL DEFAULT now()
|
|
5934
|
+
);`;
|
|
5935
|
+
return {
|
|
5936
|
+
schema,
|
|
5937
|
+
async migrate() {
|
|
5938
|
+
await pool.query(schema);
|
|
5939
|
+
},
|
|
5940
|
+
async hasNonce(nonce) {
|
|
5941
|
+
const r = await pool.query(`SELECT 1 FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
5942
|
+
return (r.rowCount ?? r.rows.length) > 0;
|
|
5943
|
+
},
|
|
5944
|
+
async receiptFor(nonce) {
|
|
5945
|
+
const r = await pool.query(`SELECT receipt FROM ${table} WHERE nonce = $1`, [nonce]);
|
|
5946
|
+
return r.rows[0]?.receipt ?? null;
|
|
5947
|
+
},
|
|
5948
|
+
async transaction(fn) {
|
|
5949
|
+
const client = await pool.connect();
|
|
5950
|
+
let claimed = null, completed = false;
|
|
5951
|
+
try {
|
|
5952
|
+
await client.query("BEGIN");
|
|
5953
|
+
const out = await fn({
|
|
5954
|
+
tx: client,
|
|
5955
|
+
async claimNonce(nonce) {
|
|
5956
|
+
const r = await client.query(
|
|
5957
|
+
`INSERT INTO ${table} (nonce, act_id, receipt, act) VALUES ($1, '', '{}'::json, '{}'::json)
|
|
5958
|
+
ON CONFLICT (nonce) DO NOTHING RETURNING nonce`,
|
|
5959
|
+
[nonce]
|
|
5960
|
+
);
|
|
5961
|
+
const won = (r.rowCount ?? r.rows.length) === 1;
|
|
5962
|
+
if (won) claimed = nonce;
|
|
5963
|
+
return won;
|
|
5964
|
+
},
|
|
5965
|
+
async putReceipt(nonce, receipt, act2) {
|
|
5966
|
+
if (nonce !== claimed) throw new Error("postgresStore: putReceipt for a nonce this transaction did not claim");
|
|
5967
|
+
await client.query(
|
|
5968
|
+
`UPDATE ${table} SET act_id = $2, receipt = $3::json, act = $4::json WHERE nonce = $1`,
|
|
5969
|
+
[nonce, receipt.act_id, JSON.stringify(receipt), JSON.stringify(act2 ?? {})]
|
|
5970
|
+
);
|
|
5971
|
+
completed = true;
|
|
5972
|
+
}
|
|
5973
|
+
});
|
|
5974
|
+
if (claimed && !completed) throw new Error("postgresStore: the transaction claimed a nonce but stored no receipt");
|
|
5975
|
+
await client.query("COMMIT");
|
|
5976
|
+
client.release();
|
|
5977
|
+
return out;
|
|
5978
|
+
} catch (e) {
|
|
5979
|
+
let broken;
|
|
5980
|
+
await client.query("ROLLBACK").catch((err) => {
|
|
5981
|
+
broken = err;
|
|
5982
|
+
});
|
|
5983
|
+
client.release(broken);
|
|
5984
|
+
throw e;
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5634
5990
|
// src/act/statements.ts
|
|
5635
|
-
import { randomBytes as randomBytes3, randomUUID } from "crypto";
|
|
5636
5991
|
async function certify(parent, child, opts) {
|
|
5637
5992
|
const unsigned = {
|
|
5638
5993
|
parent_kid: parent.kid,
|
|
@@ -5687,7 +6042,7 @@ function buildAct(o) {
|
|
|
5687
6042
|
const act2 = {
|
|
5688
6043
|
act_version: "1",
|
|
5689
6044
|
act_id: `act-${randomUUID()}`,
|
|
5690
|
-
nonce: randomBytes3(16)
|
|
6045
|
+
nonce: toBase642(randomBytes3(16)),
|
|
5691
6046
|
// Issued a second in the past so a relying party's clock a moment behind ours still accepts it.
|
|
5692
6047
|
issued_at: new Date(now.getTime() - 1e3).toISOString(),
|
|
5693
6048
|
expires_at: new Date(now.getTime() - 1e3 + window * 1e3).toISOString(),
|
|
@@ -5701,6 +6056,575 @@ function buildAct(o) {
|
|
|
5701
6056
|
return act2;
|
|
5702
6057
|
}
|
|
5703
6058
|
|
|
6059
|
+
// src/act/registry.ts
|
|
6060
|
+
function memoryRegistryStore() {
|
|
6061
|
+
const certs = [], sts = [];
|
|
6062
|
+
return {
|
|
6063
|
+
async certificates() {
|
|
6064
|
+
return [...certs];
|
|
6065
|
+
},
|
|
6066
|
+
async addCertificate(c) {
|
|
6067
|
+
if (!certs.some((x) => fingerprint(x) === fingerprint(c))) certs.push(c);
|
|
6068
|
+
},
|
|
6069
|
+
async statements() {
|
|
6070
|
+
return [...sts];
|
|
6071
|
+
},
|
|
6072
|
+
async addStatement(s) {
|
|
6073
|
+
sts.push(s);
|
|
6074
|
+
}
|
|
6075
|
+
};
|
|
6076
|
+
}
|
|
6077
|
+
var RegistryError = class extends Error {
|
|
6078
|
+
constructor(message, code) {
|
|
6079
|
+
super(message);
|
|
6080
|
+
this.code = code;
|
|
6081
|
+
this.name = "RegistryError";
|
|
6082
|
+
}
|
|
6083
|
+
};
|
|
6084
|
+
var any = () => ({ ok: true });
|
|
6085
|
+
function certificateRegistry(o) {
|
|
6086
|
+
const rootKid = kidOf(o.root);
|
|
6087
|
+
if (!rootKid) throw new RegistryError("certificateRegistry: the root is not a canonical Ed25519 or P-256 public key", "bad_root");
|
|
6088
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6089
|
+
async function revokedSets() {
|
|
6090
|
+
const kids = /* @__PURE__ */ new Set(), certs = /* @__PURE__ */ new Set();
|
|
6091
|
+
for (const s of await o.store.statements()) s.revoked_kid ? kids.add(s.revoked_kid) : s.revoked_fingerprint && certs.add(s.revoked_fingerprint);
|
|
6092
|
+
return { kids, certs };
|
|
6093
|
+
}
|
|
6094
|
+
async function scopesOf(kid) {
|
|
6095
|
+
if (kid === rootKid) return { ok: true, scopes: [{ terms: {} }] };
|
|
6096
|
+
const { kids, certs } = await revokedSets();
|
|
6097
|
+
const r = resolveKey({ targetKid: kid, pool: await o.store.certificates(), rootPublicKeyPem: o.root, purpose: any, now: now(), revokedKids: kids, revokedCerts: certs, allPaths: true });
|
|
6098
|
+
if (r.decision !== "RESOLVED") return { ok: false, reason: `${r.decision}${r.reason_code ? ` (${r.reason_code})` : ""}` };
|
|
6099
|
+
return { ok: true, scopes: r.admitted ?? [r.effective] };
|
|
6100
|
+
}
|
|
6101
|
+
async function pool(filter) {
|
|
6102
|
+
const { kids, certs } = await revokedSets(), t = now().getTime();
|
|
6103
|
+
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)));
|
|
6104
|
+
}
|
|
6105
|
+
return {
|
|
6106
|
+
rootKid,
|
|
6107
|
+
pool,
|
|
6108
|
+
/** NOMOS-SPEC-007 §3: certify `child` under `parent`. Refuses anything a verifier would refuse. */
|
|
6109
|
+
async issue(io) {
|
|
6110
|
+
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");
|
|
6111
|
+
const scope = parseScope(io.scope ?? null);
|
|
6112
|
+
if (!scope.ok) throw new RegistryError(`Malformed scope: ${scope.detail}`, "malformed_scope");
|
|
6113
|
+
const parentScopes = await scopesOf(io.parent.kid);
|
|
6114
|
+
if (!parentScopes.ok) throw new RegistryError(`The parent ${io.parent.kid} is not reached from the root: ${parentScopes.reason}`, "parent_not_recognized");
|
|
6115
|
+
const fits = parentScopes.scopes.filter((p) => suitePermitted(p, io.parent.algorithm) && isNarrowerOrEqual(scope.scope, p).ok);
|
|
6116
|
+
if (!fits.length) {
|
|
6117
|
+
const widened = parentScopes.scopes.map((p) => isNarrowerOrEqual(scope.scope, p)).find((x) => !x.ok);
|
|
6118
|
+
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");
|
|
6119
|
+
}
|
|
6120
|
+
const expiresAt = io.expiresAt ?? (io.days ? new Date(now().getTime() + io.days * 864e5) : void 0);
|
|
6121
|
+
if (!expiresAt) throw new RegistryError("Give expiresAt or days: a certificate always expires", "no_expiry");
|
|
6122
|
+
const c = await certify(io.parent, io.child, { scope: io.scope, issuedAt: io.issuedAt ?? now(), expiresAt });
|
|
6123
|
+
await o.store.addCertificate(c);
|
|
6124
|
+
return c;
|
|
6125
|
+
},
|
|
6126
|
+
/** rev. 1.10 §5.1: `parent` revokes a key it certified. The key is dead on every path. */
|
|
6127
|
+
async revokeKey(io) {
|
|
6128
|
+
const certs = (await o.store.certificates()).filter((c) => c.child_kid === io.kid && c.parent_kid === io.parent.kid);
|
|
6129
|
+
if (!certs.length) throw new RegistryError(`${io.parent.kid} never certified ${io.kid}: only a key's parent can revoke it`, "not_the_parent");
|
|
6130
|
+
const st = await signRevocation(io.parent, { kid: io.kid }, io.reason, now());
|
|
6131
|
+
await o.store.addStatement(st);
|
|
6132
|
+
return st;
|
|
6133
|
+
},
|
|
6134
|
+
/** rev. 1.10 §5.1: withdraw one certificate; the key stays valid under any other. */
|
|
6135
|
+
async revokeCertificate(io) {
|
|
6136
|
+
const c = (await o.store.certificates()).find((x) => fingerprint(x) === io.fingerprint);
|
|
6137
|
+
if (!c) throw new RegistryError(`No certificate ${io.fingerprint} in this registry`, "unknown_certificate");
|
|
6138
|
+
const st = await signRevocation(io.parent, { certificate: c }, io.reason, now());
|
|
6139
|
+
await o.store.addStatement(st);
|
|
6140
|
+
return st;
|
|
6141
|
+
},
|
|
6142
|
+
/** The certificates that connect `kid` to the root: what a witness or approver returns with
|
|
6143
|
+
* what it signs (SPEC-009 0.2.0 §4, §5). */
|
|
6144
|
+
async chainFor(kid) {
|
|
6145
|
+
return presentedCertificates(await pool(), { authority: { artifact: { seal: { kid } } } });
|
|
6146
|
+
},
|
|
6147
|
+
/** How `kid` is reached from the root, and with which effective scopes, or why it is not. */
|
|
6148
|
+
async check(kid) {
|
|
6149
|
+
return scopesOf(kid);
|
|
6150
|
+
},
|
|
6151
|
+
certificates: () => o.store.certificates(),
|
|
6152
|
+
statements: () => o.store.statements(),
|
|
6153
|
+
/** rev. 1.10 §5.3: the signed list, carrying the certificates that make it checkable. The
|
|
6154
|
+
* signer should be reached from the root, or the list establishes no date (§5.3). */
|
|
6155
|
+
async revocationList(signer) {
|
|
6156
|
+
const statements = await o.store.statements();
|
|
6157
|
+
return signRevocationList(signer, statements, revocationListCertificates(await o.store.certificates(), statements, signer.kid), now());
|
|
6158
|
+
}
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6161
|
+
function postgresRegistryStore(pool, opts = {}) {
|
|
6162
|
+
const p = opts.prefix ?? "nomos_registry";
|
|
6163
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new RegistryError(`"${p}" is not a plain lower-case prefix`, "bad_prefix");
|
|
6164
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_certificates (
|
|
6165
|
+
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()
|
|
6166
|
+
);
|
|
6167
|
+
CREATE TABLE IF NOT EXISTS ${p}_statements (
|
|
6168
|
+
id bigserial PRIMARY KEY, target text NOT NULL, kid text NOT NULL, statement json NOT NULL, added_at timestamptz NOT NULL DEFAULT now()
|
|
6169
|
+
);`;
|
|
6170
|
+
return {
|
|
6171
|
+
schema,
|
|
6172
|
+
async migrate() {
|
|
6173
|
+
await pool.query(schema);
|
|
6174
|
+
},
|
|
6175
|
+
async certificates() {
|
|
6176
|
+
return (await pool.query(`SELECT certificate FROM ${p}_certificates ORDER BY added_at, fingerprint`)).rows.map((r) => r.certificate);
|
|
6177
|
+
},
|
|
6178
|
+
async addCertificate(c) {
|
|
6179
|
+
await pool.query(
|
|
6180
|
+
`INSERT INTO ${p}_certificates (fingerprint, child_kid, parent_kid, certificate) VALUES ($1, $2, $3, $4::json) ON CONFLICT (fingerprint) DO NOTHING`,
|
|
6181
|
+
[fingerprint(c), c.child_kid, c.parent_kid, JSON.stringify(c)]
|
|
6182
|
+
);
|
|
6183
|
+
},
|
|
6184
|
+
async statements() {
|
|
6185
|
+
return (await pool.query(`SELECT statement FROM ${p}_statements ORDER BY id`)).rows.map((r) => r.statement);
|
|
6186
|
+
},
|
|
6187
|
+
async addStatement(s) {
|
|
6188
|
+
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)]);
|
|
6189
|
+
}
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6192
|
+
|
|
6193
|
+
// src/act/passkeys.ts
|
|
6194
|
+
function cbor(b) {
|
|
6195
|
+
let i = 0;
|
|
6196
|
+
const len = (info) => {
|
|
6197
|
+
if (info < 24) return info;
|
|
6198
|
+
const n = info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : -1;
|
|
6199
|
+
if (n < 0) throw new Error("unsupported CBOR length");
|
|
6200
|
+
let v2 = 0;
|
|
6201
|
+
for (let k = 0; k < n; k++) v2 = v2 * 256 + b[i++];
|
|
6202
|
+
return v2;
|
|
6203
|
+
};
|
|
6204
|
+
const item = (depth) => {
|
|
6205
|
+
if (depth > 16 || i >= b.length) throw new Error("malformed CBOR");
|
|
6206
|
+
const head = b[i++], major = head >> 5, info = head & 31;
|
|
6207
|
+
switch (major) {
|
|
6208
|
+
case 0:
|
|
6209
|
+
return len(info);
|
|
6210
|
+
case 1:
|
|
6211
|
+
return -1 - len(info);
|
|
6212
|
+
case 2: {
|
|
6213
|
+
const n = len(info);
|
|
6214
|
+
const v2 = b.slice(i, i + n);
|
|
6215
|
+
i += n;
|
|
6216
|
+
if (v2.length !== n) throw new Error("truncated CBOR");
|
|
6217
|
+
return v2;
|
|
6218
|
+
}
|
|
6219
|
+
case 3: {
|
|
6220
|
+
const n = len(info);
|
|
6221
|
+
const v2 = fromUtf82(b.slice(i, i + n));
|
|
6222
|
+
i += n;
|
|
6223
|
+
return v2;
|
|
6224
|
+
}
|
|
6225
|
+
case 4: {
|
|
6226
|
+
const n = len(info), a = [];
|
|
6227
|
+
for (let k = 0; k < n; k++) a.push(item(depth + 1));
|
|
6228
|
+
return a;
|
|
6229
|
+
}
|
|
6230
|
+
case 5: {
|
|
6231
|
+
const n = len(info), m = /* @__PURE__ */ new Map();
|
|
6232
|
+
for (let k = 0; k < n; k++) m.set(item(depth + 1), item(depth + 1));
|
|
6233
|
+
return m;
|
|
6234
|
+
}
|
|
6235
|
+
case 7:
|
|
6236
|
+
if (info === 20) return false;
|
|
6237
|
+
if (info === 21) return true;
|
|
6238
|
+
if (info === 22) return null;
|
|
6239
|
+
throw new Error("unsupported CBOR simple value");
|
|
6240
|
+
default:
|
|
6241
|
+
throw new Error("unsupported CBOR type");
|
|
6242
|
+
}
|
|
6243
|
+
};
|
|
6244
|
+
const v = item(0);
|
|
6245
|
+
return { value: v, end: i };
|
|
6246
|
+
}
|
|
6247
|
+
function readAttestation(attestationObject) {
|
|
6248
|
+
const att = cbor(attestationObject).value;
|
|
6249
|
+
if (!(att instanceof Map) || !(att.get("authData") instanceof Uint8Array)) throw new Error("not an attestation object");
|
|
6250
|
+
const a = att.get("authData");
|
|
6251
|
+
if (a.length < 55 || !(a[32] & 64)) throw new Error("no attested credential data");
|
|
6252
|
+
const idLen = a[53] << 8 | a[54];
|
|
6253
|
+
const credentialId = a.slice(55, 55 + idLen);
|
|
6254
|
+
const cose = cbor(a.slice(55 + idLen)).value;
|
|
6255
|
+
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)");
|
|
6256
|
+
const x = cose.get(-2), y = cose.get(-3);
|
|
6257
|
+
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array) || x.length !== 32 || y.length !== 32) throw new Error("malformed COSE key");
|
|
6258
|
+
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);
|
|
6259
|
+
const publicKeyPem = pem("PUBLIC KEY", spki);
|
|
6260
|
+
if (!kidOf(publicKeyPem)) throw new Error("the passkey's key is not a valid P-256 point");
|
|
6261
|
+
return { authData: a, flags: a[32], credentialId, publicKeyPem };
|
|
6262
|
+
}
|
|
6263
|
+
async function signEnrolmentToken(authority, c) {
|
|
6264
|
+
if (!/^[^,\s]+$/.test(c.approver)) throw new Error("An approver id has no commas or spaces (NOMOS-SPEC-007 rev. 1.9 \xA73.7)");
|
|
6265
|
+
const claims = { approver: c.approver, roles: [...c.roles], origin: c.origin.replace(/\/$/, ""), expires_at: c.expiresAt.toISOString(), nonce: toBase64url2(randomBytes3(16)) };
|
|
6266
|
+
const token = { ...claims, kid: authority.kid, algorithm: authority.algorithm, signature: await authority.sign(jcs(claims)) };
|
|
6267
|
+
return toBase64url2(utf82(JSON.stringify(token)));
|
|
6268
|
+
}
|
|
6269
|
+
function verifyEnrolmentToken(token, authorityPem, origin, now) {
|
|
6270
|
+
let t;
|
|
6271
|
+
try {
|
|
6272
|
+
t = JSON.parse(fromUtf82(fromBase64url2(token)));
|
|
6273
|
+
} catch {
|
|
6274
|
+
return { error: "not an enrolment link" };
|
|
6275
|
+
}
|
|
6276
|
+
const claims = { approver: t?.approver, roles: t?.roles, origin: t?.origin, expires_at: t?.expires_at, nonce: t?.nonce };
|
|
6277
|
+
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" };
|
|
6278
|
+
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" };
|
|
6279
|
+
if (claims.origin !== origin) return { error: "this enrolment link is for another service" };
|
|
6280
|
+
if (!(Date.parse(claims.expires_at) > now.getTime())) return { error: "this enrolment link has expired" };
|
|
6281
|
+
return claims;
|
|
6282
|
+
}
|
|
6283
|
+
function memoryPasskeyStore() {
|
|
6284
|
+
const used = /* @__PURE__ */ new Set(), challenges = /* @__PURE__ */ new Map(), creds = [], pend = /* @__PURE__ */ new Map();
|
|
6285
|
+
return {
|
|
6286
|
+
async claimToken(n) {
|
|
6287
|
+
if (used.has(n)) return false;
|
|
6288
|
+
used.add(n);
|
|
6289
|
+
return true;
|
|
6290
|
+
},
|
|
6291
|
+
async tokenUsed(n) {
|
|
6292
|
+
return used.has(n);
|
|
6293
|
+
},
|
|
6294
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6295
|
+
if (!used.has(n)) challenges.set(n, c);
|
|
6296
|
+
},
|
|
6297
|
+
async enrolmentChallenge(n) {
|
|
6298
|
+
return used.has(n) ? null : challenges.get(n) ?? null;
|
|
6299
|
+
},
|
|
6300
|
+
async addCredential(c) {
|
|
6301
|
+
creds.push(structuredClone(c));
|
|
6302
|
+
},
|
|
6303
|
+
async credentials() {
|
|
6304
|
+
return structuredClone(creds);
|
|
6305
|
+
},
|
|
6306
|
+
async createPending(p) {
|
|
6307
|
+
pend.set(p.id, structuredClone(p));
|
|
6308
|
+
},
|
|
6309
|
+
async pending(id) {
|
|
6310
|
+
const p = pend.get(id);
|
|
6311
|
+
return p ? structuredClone(p) : null;
|
|
6312
|
+
},
|
|
6313
|
+
async addAttempt(id, c, a) {
|
|
6314
|
+
const p = pend.get(id);
|
|
6315
|
+
if (!p || p.status !== "pending") return false;
|
|
6316
|
+
p.attempts[c] = { ...a };
|
|
6317
|
+
return true;
|
|
6318
|
+
},
|
|
6319
|
+
async resolvePending(id, out) {
|
|
6320
|
+
const p = pend.get(id);
|
|
6321
|
+
if (!p || p.status !== "pending") return false;
|
|
6322
|
+
Object.assign(p, structuredClone(out));
|
|
6323
|
+
return true;
|
|
6324
|
+
}
|
|
6325
|
+
};
|
|
6326
|
+
}
|
|
6327
|
+
function postgresPasskeyStore(pool, opts = {}) {
|
|
6328
|
+
const p = opts.prefix ?? "nomos_passkeys";
|
|
6329
|
+
if (!/^[a-z_][a-z0-9_]{0,40}$/.test(p)) throw new Error(`"${p}" is not a plain lower-case prefix`);
|
|
6330
|
+
const schema = `CREATE TABLE IF NOT EXISTS ${p}_tokens (nonce text PRIMARY KEY, challenge text, used boolean NOT NULL DEFAULT false);
|
|
6331
|
+
CREATE TABLE IF NOT EXISTS ${p}_credentials (credential_id text PRIMARY KEY, credential json NOT NULL, added_at timestamptz NOT NULL DEFAULT now());
|
|
6332
|
+
CREATE TABLE IF NOT EXISTS ${p}_pending (id text PRIMARY KEY, approval jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());`;
|
|
6333
|
+
return {
|
|
6334
|
+
schema,
|
|
6335
|
+
async migrate() {
|
|
6336
|
+
await pool.query(schema);
|
|
6337
|
+
},
|
|
6338
|
+
async claimToken(n) {
|
|
6339
|
+
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]);
|
|
6340
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6341
|
+
},
|
|
6342
|
+
async tokenUsed(n) {
|
|
6343
|
+
return (await pool.query(`SELECT 1 FROM ${p}_tokens WHERE nonce = $1 AND used = true`, [n])).rows.length > 0;
|
|
6344
|
+
},
|
|
6345
|
+
async saveEnrolmentChallenge(n, c) {
|
|
6346
|
+
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]);
|
|
6347
|
+
},
|
|
6348
|
+
async enrolmentChallenge(n) {
|
|
6349
|
+
return (await pool.query(`SELECT challenge FROM ${p}_tokens WHERE nonce = $1 AND used = false`, [n])).rows[0]?.challenge ?? null;
|
|
6350
|
+
},
|
|
6351
|
+
async addCredential(c) {
|
|
6352
|
+
await pool.query(`INSERT INTO ${p}_credentials (credential_id, credential) VALUES ($1, $2::json)`, [c.credential_id, JSON.stringify(c)]);
|
|
6353
|
+
},
|
|
6354
|
+
async credentials() {
|
|
6355
|
+
return (await pool.query(`SELECT credential FROM ${p}_credentials ORDER BY added_at`)).rows.map((r) => r.credential);
|
|
6356
|
+
},
|
|
6357
|
+
async createPending(a) {
|
|
6358
|
+
await pool.query(`INSERT INTO ${p}_pending (id, approval) VALUES ($1, $2::jsonb)`, [a.id, JSON.stringify(a)]);
|
|
6359
|
+
},
|
|
6360
|
+
async pending(id) {
|
|
6361
|
+
return (await pool.query(`SELECT approval FROM ${p}_pending WHERE id = $1`, [id])).rows[0]?.approval ?? null;
|
|
6362
|
+
},
|
|
6363
|
+
// Each change is one conditional statement: Postgres serializes them on the row, and none
|
|
6364
|
+
// rewrites the whole record, so a stale reader cannot write an old status back.
|
|
6365
|
+
async addAttempt(id, c, a) {
|
|
6366
|
+
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)]);
|
|
6367
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6368
|
+
},
|
|
6369
|
+
async resolvePending(id, out) {
|
|
6370
|
+
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)]);
|
|
6371
|
+
return (r.rowCount ?? r.rows.length) === 1;
|
|
6372
|
+
}
|
|
6373
|
+
};
|
|
6374
|
+
}
|
|
6375
|
+
var MAX_ATTEMPTS = 50;
|
|
6376
|
+
var esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6377
|
+
function passkeyApprover(o) {
|
|
6378
|
+
const base = o.origin.replace(/\/$/, "");
|
|
6379
|
+
const rpId = o.rpId ?? new URL(base).hostname;
|
|
6380
|
+
const store = o.store ?? memoryPasskeyStore();
|
|
6381
|
+
if (!o.store) console.warn("[nomos] passkeyApprover(): using the in-memory store. Enrolled passkeys are lost on restart. Development only; use postgresPasskeyStore() in production.");
|
|
6382
|
+
const now = o.now ?? (() => /* @__PURE__ */ new Date());
|
|
6383
|
+
const rpIdHash = sha256(utf82(rpId));
|
|
6384
|
+
const expired = (p) => p.status === "pending" && Date.parse(p.act.expires_at) <= now().getTime();
|
|
6385
|
+
const statusOf = (p) => expired(p) ? "expired" : p.status;
|
|
6386
|
+
function clientData(b64, type, challenge) {
|
|
6387
|
+
let c;
|
|
6388
|
+
try {
|
|
6389
|
+
c = JSON.parse(fromUtf82(fromBase64url2(b64)));
|
|
6390
|
+
} catch {
|
|
6391
|
+
return "unreadable client data";
|
|
6392
|
+
}
|
|
6393
|
+
if (c?.type !== type) return `expected a ${type} ceremony`;
|
|
6394
|
+
if (c?.challenge !== challenge) return "the challenge does not match";
|
|
6395
|
+
if (c?.origin !== base) return `the ceremony ran on ${c?.origin}, not ${base}`;
|
|
6396
|
+
return null;
|
|
6397
|
+
}
|
|
6398
|
+
async function route(method, path, json) {
|
|
6399
|
+
if (method === "POST" && path === "/nomos/approvals") {
|
|
6400
|
+
const { role, act: act2 } = await json();
|
|
6401
|
+
if (!o.roles.includes(role)) return { status: 404, body: { error: `This service does not approve as ${role}.` } };
|
|
6402
|
+
if (typeof act2?.expires_at !== "string" || !act2?.action) return { status: 400, body: { error: "The request needs the Act." } };
|
|
6403
|
+
const id = randomUUID();
|
|
6404
|
+
await store.createPending({ id, role, act: act2, status: "pending", created_at: now().toISOString(), attempts: {} });
|
|
6405
|
+
await o.onPending?.({ id, role, act: act2, approvalUrl: `${base}/approve/${id}` });
|
|
6406
|
+
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
6407
|
+
}
|
|
6408
|
+
let m = path.match(/^\/nomos\/approvals\/([0-9a-f-]{36})$/);
|
|
6409
|
+
if (method === "GET" && m) {
|
|
6410
|
+
const p = await store.pending(m[1]);
|
|
6411
|
+
if (!p) return null;
|
|
6412
|
+
const s = statusOf(p);
|
|
6413
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...p.key_certs?.length ? { key_certs: p.key_certs } : {} } : {} } };
|
|
6414
|
+
}
|
|
6415
|
+
m = path.match(/^\/approve\/([0-9a-f-]{36})(?:\/(devices|options|assertion|refuse))?$/);
|
|
6416
|
+
if (m) {
|
|
6417
|
+
const p = await store.pending(m[1]);
|
|
6418
|
+
if (!p) return null;
|
|
6419
|
+
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) };
|
|
6420
|
+
if (method !== "POST" || !m[2]) return null;
|
|
6421
|
+
if (statusOf(p) !== "pending") return { status: 409, body: { error: `This request is ${statusOf(p)}.` } };
|
|
6422
|
+
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." } };
|
|
6423
|
+
const req = await json();
|
|
6424
|
+
if (m[2] === "devices") {
|
|
6425
|
+
const mine = (await store.credentials()).filter((c) => c.approver === String(req?.approver ?? "") && c.roles.includes(p.role));
|
|
6426
|
+
return { status: 200, body: { devices: mine.map((c) => ({ credential_id: c.credential_id, label: c.label })) } };
|
|
6427
|
+
}
|
|
6428
|
+
const cred = (await store.credentials()).find((c) => c.credential_id === req?.credential_id);
|
|
6429
|
+
if (!cred || !cred.roles.includes(p.role)) return { status: 403, body: { error: `That passkey does not approve as ${p.role}.` } };
|
|
6430
|
+
if (m[2] === "options") {
|
|
6431
|
+
const signed_at = now().toISOString();
|
|
6432
|
+
const payload2 = consentPayload({ binding_digest: bindingDigest(p.act), role: p.role, decision: "approve", signed_at, kid: cred.kid });
|
|
6433
|
+
const challenge = toBase64url2(sha256(jcs(payload2)));
|
|
6434
|
+
if (Object.keys(p.attempts).length >= MAX_ATTEMPTS) return { status: 429, body: { error: "Too many approval ceremonies for this request." } };
|
|
6435
|
+
const expires_at = new Date(Math.min(Date.parse(p.act.expires_at), now().getTime() + 5 * 6e4)).toISOString();
|
|
6436
|
+
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." } };
|
|
6437
|
+
return { status: 200, body: { challenge, rpId, allowCredentials: [{ type: "public-key", id: cred.credential_id }], userVerification: "required", timeout: 12e4 } };
|
|
6438
|
+
}
|
|
6439
|
+
let offered;
|
|
6440
|
+
try {
|
|
6441
|
+
offered = JSON.parse(fromUtf82(fromBase64url2(String(req.client_data_json ?? "")))).challenge;
|
|
6442
|
+
} catch {
|
|
6443
|
+
return { status: 400, body: { error: "unreadable client data" } };
|
|
6444
|
+
}
|
|
6445
|
+
const attempt = p.attempts[offered];
|
|
6446
|
+
if (!attempt || attempt.credential_id !== cred.credential_id) return { status: 400, body: { error: "This ceremony was not offered for this passkey." } };
|
|
6447
|
+
if (Date.parse(attempt.expires_at) <= now().getTime()) return { status: 400, body: { error: "This ceremony has expired; start again." } };
|
|
6448
|
+
const consent = {
|
|
6449
|
+
binding_digest: bindingDigest(p.act),
|
|
6450
|
+
role: p.role,
|
|
6451
|
+
decision: "approve",
|
|
6452
|
+
signed_at: attempt.signed_at,
|
|
6453
|
+
kid: cred.kid,
|
|
6454
|
+
algorithm: "WebAuthn-ES256",
|
|
6455
|
+
authenticator_data: String(req.authenticator_data ?? ""),
|
|
6456
|
+
client_data_json: String(req.client_data_json ?? ""),
|
|
6457
|
+
signature: String(req.signature ?? "")
|
|
6458
|
+
};
|
|
6459
|
+
const payload = jcs(consentPayload(consent));
|
|
6460
|
+
const cd = clientData(consent.client_data_json, "webauthn.get", toBase64url2(sha256(payload)));
|
|
6461
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6462
|
+
const ad = fromBase64url2(consent.authenticator_data);
|
|
6463
|
+
if (ad.length < 37 || !equal2(ad.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The assertion is for another relying-party id." } };
|
|
6464
|
+
const v = verifySuite("WebAuthn-ES256", cred.public_key_pem, payload, consent);
|
|
6465
|
+
if (!v.ok) return { status: 400, body: { error: `The passkey's signature does not verify (${v.reason}).` } };
|
|
6466
|
+
const key_certs = await o.registry.chainFor(cred.kid);
|
|
6467
|
+
if (!key_certs.length) return { status: 409, body: { error: "This passkey is no longer certified (revoked or expired)." } };
|
|
6468
|
+
if (!await store.resolvePending(p.id, { status: "approved", consent, key_certs })) return { status: 409, body: { error: "This request is no longer pending." } };
|
|
6469
|
+
return { status: 200, body: { status: "approved" } };
|
|
6470
|
+
}
|
|
6471
|
+
if (method === "GET" && path === "/enroll") return { status: 200, type: "text/html", body: enrolPage(o.rpName ?? rpId) };
|
|
6472
|
+
if (method === "POST" && (path === "/nomos/enrol/begin" || path === "/nomos/enrol/finish")) {
|
|
6473
|
+
const req = await json();
|
|
6474
|
+
const claims = verifyEnrolmentToken(String(req?.token ?? ""), o.enrolmentAuthority, base, now());
|
|
6475
|
+
if ("error" in claims) return { status: 403, body: { error: claims.error } };
|
|
6476
|
+
if (!claims.roles.every((r) => o.roles.includes(r))) return { status: 403, body: { error: `This service does not enrol for ${claims.roles.join(", ")}.` } };
|
|
6477
|
+
if (path === "/nomos/enrol/begin") {
|
|
6478
|
+
if (await store.tokenUsed(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6479
|
+
const challenge2 = toBase64url2(randomBytes3(32));
|
|
6480
|
+
await store.saveEnrolmentChallenge(claims.nonce, challenge2);
|
|
6481
|
+
const existing = (await store.credentials()).filter((c) => c.approver === claims.approver);
|
|
6482
|
+
return { status: 200, body: {
|
|
6483
|
+
challenge: challenge2,
|
|
6484
|
+
rp: { id: rpId, name: o.rpName ?? rpId },
|
|
6485
|
+
user: { id: toBase64url2(utf82(claims.approver)), name: claims.approver, displayName: claims.approver },
|
|
6486
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
6487
|
+
authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
|
|
6488
|
+
attestation: "none",
|
|
6489
|
+
timeout: 12e4,
|
|
6490
|
+
excludeCredentials: existing.map((c) => ({ type: "public-key", id: c.credential_id })),
|
|
6491
|
+
approver: claims.approver,
|
|
6492
|
+
roles: claims.roles
|
|
6493
|
+
} };
|
|
6494
|
+
}
|
|
6495
|
+
const challenge = await store.enrolmentChallenge(claims.nonce);
|
|
6496
|
+
if (!challenge) return { status: 403, body: { error: "Start enrolment again: this link has been used or has no ceremony in progress." } };
|
|
6497
|
+
const cd = clientData(String(req.client_data_json ?? ""), "webauthn.create", challenge);
|
|
6498
|
+
if (cd) return { status: 400, body: { error: cd } };
|
|
6499
|
+
let att;
|
|
6500
|
+
try {
|
|
6501
|
+
att = readAttestation(fromBase64url2(String(req.attestation_object ?? "")));
|
|
6502
|
+
} catch (e) {
|
|
6503
|
+
return { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6504
|
+
}
|
|
6505
|
+
if (!equal2(att.authData.slice(0, 32), rpIdHash)) return { status: 400, body: { error: "The passkey was created for another relying-party id." } };
|
|
6506
|
+
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)." } };
|
|
6507
|
+
if (toBase64url2(att.credentialId) !== req.credential_id) return { status: 400, body: { error: "The credential id does not match the attestation." } };
|
|
6508
|
+
if (!await store.claimToken(claims.nonce)) return { status: 403, body: { error: "This enrolment link has been used." } };
|
|
6509
|
+
const kid = kidOf(att.publicKeyPem);
|
|
6510
|
+
const certificate = await o.registry.issue({
|
|
6511
|
+
parent: o.enroller,
|
|
6512
|
+
child: { kid, publicKeyPem: att.publicKeyPem },
|
|
6513
|
+
scope: `consent:${claims.roles.join(",")} approver:${claims.approver} suite:WebAuthn-ES256`,
|
|
6514
|
+
days: o.certificateDays ?? 365
|
|
6515
|
+
});
|
|
6516
|
+
const label = String(req.label ?? "").slice(0, 60) || "Passkey";
|
|
6517
|
+
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() });
|
|
6518
|
+
return { status: 200, body: { enrolled: true, approver: claims.approver, roles: claims.roles, kid, certificate } };
|
|
6519
|
+
}
|
|
6520
|
+
return null;
|
|
6521
|
+
}
|
|
6522
|
+
async function fetchHandler(request) {
|
|
6523
|
+
const out = await route(request.method, new URL(request.url).pathname, () => request.json()).catch((e) => ({ status: 400, body: { error: String(e?.message ?? e) } }));
|
|
6524
|
+
if (!out) return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
|
|
6525
|
+
const html = out.type === "text/html";
|
|
6526
|
+
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() } : {} } });
|
|
6527
|
+
}
|
|
6528
|
+
function node() {
|
|
6529
|
+
return async (req, res, next) => {
|
|
6530
|
+
const path = String(req.originalUrl ?? req.url ?? "").split("?")[0];
|
|
6531
|
+
const json = async () => {
|
|
6532
|
+
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
6533
|
+
const chunks = [];
|
|
6534
|
+
for await (const c of req) chunks.push(c);
|
|
6535
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
6536
|
+
};
|
|
6537
|
+
let out;
|
|
6538
|
+
try {
|
|
6539
|
+
out = await route(req.method, path, json);
|
|
6540
|
+
} catch (e) {
|
|
6541
|
+
out = { status: 400, body: { error: String(e?.message ?? e) } };
|
|
6542
|
+
}
|
|
6543
|
+
if (!out) {
|
|
6544
|
+
if (next) return next();
|
|
6545
|
+
res.statusCode = 404;
|
|
6546
|
+
return res.end(JSON.stringify({ error: "Not found" }));
|
|
6547
|
+
}
|
|
6548
|
+
res.statusCode = out.status;
|
|
6549
|
+
const html = out.type === "text/html";
|
|
6550
|
+
res.setHeader("content-type", html ? "text/html; charset=utf-8" : "application/json");
|
|
6551
|
+
if (html) res.setHeader("content-security-policy", CSP());
|
|
6552
|
+
res.end(html ? String(out.body) : JSON.stringify(out.body));
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
return { store, fetch: fetchHandler, express: node, node };
|
|
6556
|
+
}
|
|
6557
|
+
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}
|
|
6558
|
+
@media (prefers-color-scheme:dark){body{color:#eee;background:#111}table td{border-color:#333}}
|
|
6559
|
+
h2{margin:0 0 12px}table{border-collapse:collapse;margin:12px 0}td{padding:6px 10px;border-bottom:1px solid #ddd}
|
|
6560
|
+
button{font:inherit;padding:10px 18px;border-radius:8px;border:1px solid #888;background:transparent;color:inherit;cursor:pointer;margin:4px 8px 4px 0}
|
|
6561
|
+
button.primary{background:#2563eb;border-color:#2563eb;color:#fff}.muted{color:#777;font-size:13px}#msg{margin-top:16px;font-weight:600}`;
|
|
6562
|
+
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))};
|
|
6563
|
+
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(()=>({}))}));
|
|
6564
|
+
const say=t=>{document.getElementById('msg').textContent=t};`;
|
|
6565
|
+
function approvalPage(p, status, devices) {
|
|
6566
|
+
const a = p.act.action ?? {};
|
|
6567
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc(k)}</td><td><b>${esc(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
6568
|
+
const device = (d) => `<button class="primary" data-cred="${esc(d.credential_id)}">Approve${d.approver ? ` as ${esc(d.approver)}` : ""} \xB7 ${esc(d.label)}</button>`;
|
|
6569
|
+
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>`;
|
|
6570
|
+
const body = status === "pending" ? `${choose}<button id="refuse">Refuse</button>` : `<p><b>${esc(status)}</b></p>`;
|
|
6571
|
+
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>
|
|
6572
|
+
<h2>Approve as ${esc(p.role)}?</h2>
|
|
6573
|
+
<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>
|
|
6574
|
+
<table>${params}</table>
|
|
6575
|
+
<p class="muted">Your passkey signs this exact change and nothing else. It expires at ${esc(p.act.expires_at)}.</p>
|
|
6576
|
+
${body}<div id="msg" data-id="${esc(p.id)}"></div>
|
|
6577
|
+
<script>${APPROVE_JS}</script>`;
|
|
6578
|
+
}
|
|
6579
|
+
var APPROVE_JS = `${B642}
|
|
6580
|
+
const id=document.getElementById('msg').dataset.id;
|
|
6581
|
+
async function approve(credential_id){
|
|
6582
|
+
try{
|
|
6583
|
+
const o=await post('/approve/'+id+'/options',{credential_id});
|
|
6584
|
+
if(!o.ok) return say(o.body.error||'Not allowed.');
|
|
6585
|
+
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}});
|
|
6586
|
+
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)});
|
|
6587
|
+
say(r.ok?'Approved. You can close this page.':(r.body.error||'Not approved.'));
|
|
6588
|
+
if(r.ok) document.querySelectorAll('button').forEach(b=>b.disabled=true);
|
|
6589
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6590
|
+
}
|
|
6591
|
+
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)}
|
|
6592
|
+
function wire(root){root.querySelectorAll('[data-cred]').forEach(b=>b.addEventListener('click',()=>approve(b.dataset.cred)))}
|
|
6593
|
+
wire(document);
|
|
6594
|
+
document.getElementById('refuse')?.addEventListener('click',refuse);
|
|
6595
|
+
document.getElementById('find')?.addEventListener('click',async()=>{
|
|
6596
|
+
const r=await post('/approve/'+id+'/devices',{approver:document.getElementById('who').value});
|
|
6597
|
+
const box=document.getElementById('devices');box.textContent='';
|
|
6598
|
+
if(!r.body.devices?.length) return say('No passkey of yours approves this.');
|
|
6599
|
+
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)}
|
|
6600
|
+
wire(box);
|
|
6601
|
+
});`;
|
|
6602
|
+
function enrolPage(rpName) {
|
|
6603
|
+
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>
|
|
6604
|
+
<h2>Enrol a passkey for ${esc(rpName)}</h2>
|
|
6605
|
+
<p id="who" class="muted">Checking your enrolment link\u2026</p>
|
|
6606
|
+
<p><label>Name this device <input id="label" value="" placeholder="e.g. iPhone"></label></p>
|
|
6607
|
+
<button class="primary" id="go" disabled>Create passkey</button>
|
|
6608
|
+
<div id="msg"></div>
|
|
6609
|
+
<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>
|
|
6610
|
+
<script>${ENROL_JS}</script>`;
|
|
6611
|
+
}
|
|
6612
|
+
var ENROL_JS = `${B642}
|
|
6613
|
+
const token=location.hash.slice(1);let opts=null;
|
|
6614
|
+
(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}
|
|
6615
|
+
opts=r.body;document.getElementById('who').textContent='Enrolling '+opts.approver+' to approve as '+opts.roles.join(', ')+'.';document.getElementById('go').disabled=false})();
|
|
6616
|
+
async function enrol(){
|
|
6617
|
+
try{
|
|
6618
|
+
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)}))}});
|
|
6619
|
+
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)});
|
|
6620
|
+
say(r.ok?'Enrolled. This device can now approve as '+r.body.roles.join(', ')+'.':(r.body.error||'Not enrolled.'));
|
|
6621
|
+
if(r.ok) document.getElementById('go').disabled=true;
|
|
6622
|
+
}catch(e){say(e.name==='NotAllowedError'?'Cancelled.':String(e.message||e))}
|
|
6623
|
+
}
|
|
6624
|
+
document.getElementById('go').addEventListener('click',enrol);`;
|
|
6625
|
+
var scriptHash = (js) => `'sha256-${btoa(String.fromCharCode(...sha256(utf82(js))))}'`;
|
|
6626
|
+
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'`;
|
|
6627
|
+
|
|
5704
6628
|
// src/act/carrier.ts
|
|
5705
6629
|
var STATUS_OF = { AUTHORIZED: "committed", ESCALATED: "escalated", INCOMPLETE: "incomplete", DENIED: "denied", REPLAYED: "replayed" };
|
|
5706
6630
|
var NomosActError = class extends Error {
|
|
@@ -5737,6 +6661,7 @@ async function act(o) {
|
|
|
5737
6661
|
}
|
|
5738
6662
|
}
|
|
5739
6663
|
const testimony = [];
|
|
6664
|
+
const available = [...issuer2.key_certs ?? []];
|
|
5740
6665
|
const unavailable = [];
|
|
5741
6666
|
for (const claim of witnessed) {
|
|
5742
6667
|
const w = witnesses.find((x) => x.claims.includes(claim));
|
|
@@ -5760,8 +6685,10 @@ async function act(o) {
|
|
|
5760
6685
|
continue;
|
|
5761
6686
|
}
|
|
5762
6687
|
testimony.push(t);
|
|
6688
|
+
if (Array.isArray(body2.key_certs)) available.push(...body2.key_certs);
|
|
5763
6689
|
}
|
|
5764
|
-
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts:
|
|
6690
|
+
const theAct = buildAct({ relyingParty: rp.relying_party_id, action: o.action, artifact, keyCerts: [], testimony, presenterFacts: o.presenterFacts, inForce, windowSeconds });
|
|
6691
|
+
present(theAct, available);
|
|
5765
6692
|
let { status, body } = await submit(f, rp.act_endpoint, theAct);
|
|
5766
6693
|
let approvalUrl;
|
|
5767
6694
|
while (status === 202 && body.decision === "ESCALATED") {
|
|
@@ -5769,16 +6696,18 @@ async function act(o) {
|
|
|
5769
6696
|
const endpoint = (issuer2.approvals ?? []).find((a) => a.role === role)?.endpoint;
|
|
5770
6697
|
if (!endpoint) return done("escalated", body, theAct, { summary: `Needs approval from ${role}, and ${issuer2.issuer_id} publishes no approval endpoint for that role.` });
|
|
5771
6698
|
const deadline = Math.min(Date.parse(theAct.expires_at), Date.now() + (o.approvalTimeoutMs ?? Infinity));
|
|
5772
|
-
const
|
|
6699
|
+
const answer2 = await requestConsent(f, endpoint, role, theAct, deadline, o.pollMs ?? 1e3, (url) => {
|
|
5773
6700
|
approvalUrl = url;
|
|
5774
6701
|
o.onApproval?.({ role, url, actDigest: body.act_digest });
|
|
5775
6702
|
});
|
|
5776
|
-
if (
|
|
5777
|
-
if (!
|
|
6703
|
+
if (answer2 === "refused") return done("denied", body, theAct, { summary: `The ${role} refused this action.` });
|
|
6704
|
+
if (!answer2) {
|
|
5778
6705
|
const expired = Date.now() >= Date.parse(theAct.expires_at);
|
|
5779
6706
|
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}.` });
|
|
5780
6707
|
}
|
|
5781
|
-
theAct.consents.push(consent);
|
|
6708
|
+
theAct.consents.push(answer2.consent);
|
|
6709
|
+
if (Array.isArray(answer2.keyCerts)) available.push(...answer2.keyCerts);
|
|
6710
|
+
present(theAct, available);
|
|
5782
6711
|
const before = Number(body.consents_valid ?? 0);
|
|
5783
6712
|
({ status, body } = await submit(f, rp.act_endpoint, theAct));
|
|
5784
6713
|
if (status === 202 && Number(body.consents_valid ?? 0) <= before) {
|
|
@@ -5799,6 +6728,13 @@ async function act(o) {
|
|
|
5799
6728
|
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 : ""}`
|
|
5800
6729
|
});
|
|
5801
6730
|
}
|
|
6731
|
+
function present(theAct, available) {
|
|
6732
|
+
const set = presentedCertificates(available, theAct);
|
|
6733
|
+
if (set.length > MAX_PRESENTED) {
|
|
6734
|
+
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.`);
|
|
6735
|
+
}
|
|
6736
|
+
theAct.authority.key_certs = set;
|
|
6737
|
+
}
|
|
5802
6738
|
function done(status, verdict, theAct, extra) {
|
|
5803
6739
|
return { status, verdict, act: theAct, ...extra };
|
|
5804
6740
|
}
|
|
@@ -5816,13 +6752,13 @@ async function submit(f, endpoint, theAct) {
|
|
|
5816
6752
|
async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPending) {
|
|
5817
6753
|
const res = await f(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ role, act: { ...theAct, consents: [] } }) });
|
|
5818
6754
|
const body = await res.json().catch(() => ({}));
|
|
5819
|
-
if (res.status === 200 && body.status === "approved") return body.consent;
|
|
6755
|
+
if (res.status === 200 && body.status === "approved") return { consent: body.consent, keyCerts: body.key_certs };
|
|
5820
6756
|
if (res.status !== 202 || !body.poll_url) return null;
|
|
5821
6757
|
onPending(body.approval_url);
|
|
5822
6758
|
while (Date.now() < deadline) {
|
|
5823
6759
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
5824
6760
|
const p = await (await f(body.poll_url)).json().catch(() => ({}));
|
|
5825
|
-
if (p.status === "approved") return p.consent;
|
|
6761
|
+
if (p.status === "approved") return { consent: p.consent, keyCerts: p.key_certs };
|
|
5826
6762
|
if (p.status === "refused") return "refused";
|
|
5827
6763
|
if (p.status === "expired") return null;
|
|
5828
6764
|
}
|
|
@@ -5830,7 +6766,6 @@ async function requestConsent(f, endpoint, role, theAct, deadline, pollMs, onPen
|
|
|
5830
6766
|
}
|
|
5831
6767
|
|
|
5832
6768
|
// src/act/roles.ts
|
|
5833
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
5834
6769
|
function serve(route) {
|
|
5835
6770
|
async function fetchHandler(request) {
|
|
5836
6771
|
const out = await route({ method: request.method, path: new URL(request.url).pathname, json: () => request.json() });
|
|
@@ -5845,7 +6780,7 @@ function serve(route) {
|
|
|
5845
6780
|
if (req.body && typeof req.body === "object" && !(req.body instanceof Uint8Array)) return req.body;
|
|
5846
6781
|
const chunks = [];
|
|
5847
6782
|
for await (const c of req) chunks.push(c);
|
|
5848
|
-
return JSON.parse(
|
|
6783
|
+
return JSON.parse(fromUtf82(concat2(...chunks)) || "{}");
|
|
5849
6784
|
};
|
|
5850
6785
|
let out;
|
|
5851
6786
|
try {
|
|
@@ -5869,7 +6804,9 @@ var day = 864e5;
|
|
|
5869
6804
|
function issuer(o) {
|
|
5870
6805
|
const byId = new Map(o.authorities.map((a) => [a.meta.artifact_id, a]));
|
|
5871
6806
|
const base = o.origin.replace(/\/$/, "");
|
|
5872
|
-
const
|
|
6807
|
+
const pool = async () => typeof o.keyCerts === "function" ? await o.keyCerts() : o.keyCerts;
|
|
6808
|
+
const revocationsUrl = o.revocations ? `${base}/nomos/revocations` : o.revocationsUrl;
|
|
6809
|
+
const build = (keyCerts) => ({
|
|
5873
6810
|
nomos_document: "issuer",
|
|
5874
6811
|
version: "1",
|
|
5875
6812
|
issuer_id: o.issuerId,
|
|
@@ -5880,17 +6817,24 @@ function issuer(o) {
|
|
|
5880
6817
|
artifact_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}.nomos`,
|
|
5881
6818
|
in_force_url: `${base}/nomos/authorities/${encodeURIComponent(a.meta.artifact_id)}/in-force`
|
|
5882
6819
|
})),
|
|
5883
|
-
key_certs:
|
|
6820
|
+
key_certs: keyCerts,
|
|
5884
6821
|
...o.witnesses ? { witnesses: o.witnesses } : {},
|
|
5885
6822
|
...o.approvals ? { approvals: o.approvals } : {},
|
|
5886
|
-
...
|
|
6823
|
+
...revocationsUrl ? { revocations_url: revocationsUrl } : {},
|
|
5887
6824
|
valid_until: new Date(Date.now() + day).toISOString()
|
|
5888
6825
|
});
|
|
6826
|
+
const document = () => {
|
|
6827
|
+
if (typeof o.keyCerts === "function") throw new Error("issuer(): keyCerts is a function, so the document must be read with `await resolveDocument()`");
|
|
6828
|
+
return build(o.keyCerts);
|
|
6829
|
+
};
|
|
6830
|
+
const resolveDocument = async () => build(await pool());
|
|
5889
6831
|
return {
|
|
5890
6832
|
document,
|
|
6833
|
+
resolveDocument,
|
|
5891
6834
|
...serve(async ({ method, path }) => {
|
|
5892
6835
|
if (method !== "GET") return null;
|
|
5893
|
-
if (path === WELL_KNOWN.issuer) return { status: 200, body:
|
|
6836
|
+
if (path === WELL_KNOWN.issuer) return { status: 200, body: await resolveDocument() };
|
|
6837
|
+
if (path === "/nomos/revocations" && o.revocations) return { status: 200, body: await o.revocations() };
|
|
5894
6838
|
const m = path.match(/^\/nomos\/authorities\/([^/]+?)(\.nomos|\/in-force)$/);
|
|
5895
6839
|
const art = m && byId.get(decodeURIComponent(m[1]));
|
|
5896
6840
|
if (!art) return null;
|
|
@@ -5925,13 +6869,14 @@ function witness(o) {
|
|
|
5925
6869
|
if (value2 === void 0) return { status: 404, body: { error: `${o.witnessId} knows nothing about ${about}.` } };
|
|
5926
6870
|
const max = o.maxValiditySeconds ?? 900;
|
|
5927
6871
|
const seconds = Math.max(1, Math.min(max, Number(req?.valid_for_seconds) || max));
|
|
5928
|
-
return { status: 200, body: { testimony: await signTestimony(o.signer, { claim, value: value2, about, validUntil: new Date(Date.now() + seconds * 1e3) }) } };
|
|
6872
|
+
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 } : {} } };
|
|
5929
6873
|
})
|
|
5930
6874
|
};
|
|
5931
6875
|
}
|
|
5932
6876
|
function approver(o) {
|
|
5933
6877
|
const base = o.origin.replace(/\/$/, "");
|
|
5934
6878
|
const pending = /* @__PURE__ */ new Map();
|
|
6879
|
+
const chain = o.keyCerts?.length ? { key_certs: o.keyCerts } : {};
|
|
5935
6880
|
const statusOf = (p) => p.status === "pending" && Date.parse(p.act.expires_at) < Date.now() ? "expired" : p.status;
|
|
5936
6881
|
return {
|
|
5937
6882
|
pending,
|
|
@@ -5941,9 +6886,9 @@ function approver(o) {
|
|
|
5941
6886
|
if (!o.roles.includes(role)) return { status: 404, body: { error: `This approver does not consent as ${role}.` } };
|
|
5942
6887
|
if (o.decide === "auto" || typeof o.decide === "function") {
|
|
5943
6888
|
const ok = o.decide === "auto" ? true : await o.decide({ role, act: act2 });
|
|
5944
|
-
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role) } } : { status: 200, body: { status: "refused" } };
|
|
6889
|
+
return ok ? { status: 200, body: { status: "approved", consent: await signConsent(o.signer, act2, role), ...chain } } : { status: 200, body: { status: "refused" } };
|
|
5945
6890
|
}
|
|
5946
|
-
const id =
|
|
6891
|
+
const id = randomUUID();
|
|
5947
6892
|
pending.set(id, { role, act: act2, status: "pending" });
|
|
5948
6893
|
return { status: 202, body: { approval_id: id, status: "pending", poll_url: `${base}/nomos/approvals/${id}`, approval_url: `${base}/approve/${id}` } };
|
|
5949
6894
|
}
|
|
@@ -5952,7 +6897,7 @@ function approver(o) {
|
|
|
5952
6897
|
const p = pending.get(m[1]);
|
|
5953
6898
|
if (!p) return null;
|
|
5954
6899
|
const s = statusOf(p);
|
|
5955
|
-
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent } : {} } };
|
|
6900
|
+
return { status: 200, body: { status: s, ...s === "approved" ? { consent: p.consent, ...chain } : {} } };
|
|
5956
6901
|
}
|
|
5957
6902
|
m = path.match(/^\/approve\/([0-9a-f-]+)(\/(approve|refuse))?$/);
|
|
5958
6903
|
if (m) {
|
|
@@ -5972,22 +6917,23 @@ function approver(o) {
|
|
|
5972
6917
|
})
|
|
5973
6918
|
};
|
|
5974
6919
|
}
|
|
5975
|
-
var
|
|
6920
|
+
var esc2 = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
5976
6921
|
function page(p, status, id) {
|
|
5977
6922
|
const a = p.act.action ?? {};
|
|
5978
|
-
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${
|
|
6923
|
+
const params = Object.entries(a.params ?? {}).map(([k, v]) => `<tr><td>${esc2(k)}</td><td><b>${esc2(JSON.stringify(v))}</b></td></tr>`).join("");
|
|
5979
6924
|
const buttons = status === "pending" ? `<form method="post" action="/approve/${id}/approve" style="display:inline"><button style="padding:10px 18px">Approve</button></form>
|
|
5980
|
-
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${
|
|
5981
|
-
return `<!doctype html><meta charset="utf-8"><title>Approve: ${
|
|
6925
|
+
<form method="post" action="/approve/${id}/refuse" style="display:inline"><button style="padding:10px 18px">Refuse</button></form>` : `<p><b>${esc2(status)}</b></p>`;
|
|
6926
|
+
return `<!doctype html><meta charset="utf-8"><title>Approve: ${esc2(a.type)}</title>
|
|
5982
6927
|
<body style="font:15px system-ui;max-width:560px;margin:40px auto;padding:0 16px">
|
|
5983
|
-
<h2>Approve as ${
|
|
5984
|
-
<p><b>${
|
|
6928
|
+
<h2>Approve as ${esc2(p.role)}?</h2>
|
|
6929
|
+
<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>
|
|
5985
6930
|
<table style="border-collapse:collapse" cellpadding="4">${params}</table>
|
|
5986
|
-
<p style="color:#666">Your approval signs this exact change (digest ${
|
|
6931
|
+
<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>
|
|
5987
6932
|
${buttons}</body>`;
|
|
5988
6933
|
}
|
|
5989
6934
|
export {
|
|
5990
6935
|
ACT_MEDIA_TYPE,
|
|
6936
|
+
MAX_PRESENTED,
|
|
5991
6937
|
Nomos,
|
|
5992
6938
|
NomosAPIError,
|
|
5993
6939
|
NomosActError,
|
|
@@ -6002,6 +6948,8 @@ export {
|
|
|
6002
6948
|
NomosRateLimitError,
|
|
6003
6949
|
NomosValidationError,
|
|
6004
6950
|
NomosWebhookSignatureError,
|
|
6951
|
+
RegistryError,
|
|
6952
|
+
RevocationUnavailable,
|
|
6005
6953
|
WELL_KNOWN,
|
|
6006
6954
|
accept,
|
|
6007
6955
|
act,
|
|
@@ -6009,24 +6957,40 @@ export {
|
|
|
6009
6957
|
bindingDigest,
|
|
6010
6958
|
buildAct,
|
|
6011
6959
|
can,
|
|
6960
|
+
certificateRegistry,
|
|
6012
6961
|
certify,
|
|
6013
6962
|
fetchDocument,
|
|
6014
6963
|
generateSigner,
|
|
6015
6964
|
issuer,
|
|
6016
6965
|
kidOf2 as kidOf,
|
|
6017
6966
|
localSigner,
|
|
6967
|
+
memoryPasskeyStore,
|
|
6968
|
+
memoryRegistryStore,
|
|
6018
6969
|
memoryStore,
|
|
6019
6970
|
nomos,
|
|
6020
6971
|
originOf,
|
|
6972
|
+
passkeyApprover,
|
|
6973
|
+
postgresPasskeyStore,
|
|
6974
|
+
postgresRegistryStore,
|
|
6975
|
+
postgresStore,
|
|
6976
|
+
presentedCertificates,
|
|
6977
|
+
readAttestation,
|
|
6978
|
+
reliedKids,
|
|
6979
|
+
revocationListCertificates,
|
|
6980
|
+
revocationSource,
|
|
6021
6981
|
seal,
|
|
6022
6982
|
seededSigner,
|
|
6023
6983
|
signConsent,
|
|
6984
|
+
signEnrolmentToken,
|
|
6024
6985
|
signInForce,
|
|
6986
|
+
signRevocation,
|
|
6987
|
+
signRevocationList,
|
|
6025
6988
|
signTestimony,
|
|
6026
6989
|
statusFor,
|
|
6027
6990
|
verifyAct,
|
|
6028
6991
|
verifyArtifact,
|
|
6029
6992
|
verifyChain,
|
|
6993
|
+
verifyEnrolmentToken,
|
|
6030
6994
|
verifySeal,
|
|
6031
6995
|
witness
|
|
6032
6996
|
};
|