@blamejs/core 0.15.13 → 0.15.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/a2a-tasks.js +38 -6
- package/lib/agent-event-bus.js +13 -0
- package/lib/agent-idempotency.js +5 -1
- package/lib/agent-snapshot.js +32 -2
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +9 -7
- package/lib/archive-tar-read.js +51 -8
- package/lib/archive.js +4 -2
- package/lib/asn1-der.js +70 -22
- package/lib/atomic-file.js +204 -2
- package/lib/audit-chain.js +54 -5
- package/lib/audit-daily-review.js +12 -2
- package/lib/audit-sign.js +2 -1
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +7 -2
- package/lib/auth/access-lock.js +2 -2
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +6 -1
- package/lib/auth/fido-mds3.js +5 -1
- package/lib/auth/jwt.js +2 -2
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +33 -13
- package/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/auth/status-list.js +14 -2
- package/lib/auth/step-up.js +9 -1
- package/lib/auth-bot-challenge.js +21 -2
- package/lib/backup/bundle.js +7 -2
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +41 -20
- package/lib/backup/manifest.js +7 -1
- package/lib/break-glass.js +41 -22
- package/lib/cbor.js +34 -11
- package/lib/cdn-cache-control.js +7 -3
- package/lib/cert.js +5 -3
- package/lib/cli.js +5 -1
- package/lib/cloud-events.js +3 -2
- package/lib/cluster-storage.js +7 -3
- package/lib/codepoint-class.js +17 -0
- package/lib/compliance-eaa.js +1 -1
- package/lib/compliance-sanctions.js +9 -7
- package/lib/compliance.js +1 -1
- package/lib/config-drift.js +22 -8
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +11 -4
- package/lib/cookies.js +10 -2
- package/lib/cose.js +20 -0
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +29 -23
- package/lib/crypto.js +18 -4
- package/lib/csp.js +4 -0
- package/lib/daemon.js +4 -1
- package/lib/data-act.js +27 -4
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +69 -9
- package/lib/db.js +32 -15
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +22 -8
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db.js +60 -4
- package/lib/fda-21cfr11.js +30 -5
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/gate-contract.js +46 -5
- package/lib/gdpr-ropa.js +18 -9
- package/lib/graphql-federation.js +17 -4
- package/lib/guard-all.js +2 -2
- package/lib/guard-dsn.js +1 -1
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-html.js +9 -11
- package/lib/guard-imap-command.js +1 -1
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +14 -6
- package/lib/guard-mail-move.js +1 -1
- package/lib/guard-managesieve-command.js +1 -1
- package/lib/guard-pop3-command.js +1 -1
- package/lib/guard-smtp-command.js +1 -1
- package/lib/guard-svg.js +8 -9
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cookie-jar.js +33 -12
- package/lib/http-client.js +225 -53
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/incident-report.js +9 -6
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/legal-hold.js +24 -8
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arf.js +1 -1
- package/lib/mail-auth.js +3 -3
- package/lib/mail-bimi.js +16 -16
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-deploy.js +9 -5
- package/lib/mail-greylist.js +2 -4
- package/lib/mail-helo.js +2 -4
- package/lib/mail-journal.js +11 -8
- package/lib/mail-mdn.js +8 -4
- package/lib/mail-rbl.js +2 -4
- package/lib/mail-scan.js +3 -5
- package/lib/mail-server-jmap.js +4 -1
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-tls.js +9 -2
- package/lib/mail-spam-score.js +2 -4
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail.js +22 -2
- package/lib/markup-tokenizer.js +24 -0
- package/lib/mcp.js +6 -4
- package/lib/mdoc.js +26 -3
- package/lib/metrics.js +14 -3
- package/lib/middleware/api-encrypt.js +2 -2
- package/lib/middleware/body-parser.js +10 -4
- package/lib/middleware/bot-guard.js +26 -18
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +60 -21
- package/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/middleware/fetch-metadata.js +28 -4
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +25 -16
- package/lib/middleware/scim-server.js +2 -1
- package/lib/middleware/security-headers.js +24 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +2 -2
- package/lib/money.js +1 -1
- package/lib/mtls-ca.js +10 -6
- package/lib/network-dns-resolver.js +1 -1
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +2 -1
- package/lib/network-smtp-policy.js +23 -5
- package/lib/network-tls.js +27 -5
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +1 -1
- package/lib/nist-crosswalk.js +1 -1
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +9 -0
- package/lib/object-store/azure-blob.js +1 -2
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +6 -4
- package/lib/object-store/http-put.js +1 -2
- package/lib/object-store/http-request.js +30 -1
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4.js +1 -2
- package/lib/observability-otlp-exporter.js +20 -4
- package/lib/outbox.js +11 -4
- package/lib/parsers/safe-xml.js +1 -1
- package/lib/parsers/safe-yaml.js +21 -3
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +201 -23
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +19 -0
- package/lib/retention.js +20 -4
- package/lib/router.js +17 -4
- package/lib/safe-ical.js +2 -2
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +44 -0
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-vcard.js +1 -1
- package/lib/sandbox-worker.js +6 -0
- package/lib/sandbox.js +1 -1
- package/lib/scheduler.js +17 -1
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/session.js +27 -3
- package/lib/sql.js +3 -3
- package/lib/static.js +65 -13
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +52 -19
- package/lib/tsa.js +5 -2
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +4 -1
- package/lib/vc.js +1 -1
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/webhook.js +16 -1
- package/lib/websocket.js +1 -1
- package/lib/worm.js +1 -1
- package/lib/ws-client.js +57 -46
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/audit-tools.js
CHANGED
|
@@ -58,6 +58,7 @@ var nodeFs = require("node:fs");
|
|
|
58
58
|
var nodePath = require("node:path");
|
|
59
59
|
var pkg = require("../package.json");
|
|
60
60
|
var atomicFile = require("./atomic-file");
|
|
61
|
+
var C = require("./constants");
|
|
61
62
|
var auditChain = require("./audit-chain");
|
|
62
63
|
var canonicalJson = require("./canonical-json");
|
|
63
64
|
var auditSign = require("./audit-sign");
|
|
@@ -349,6 +350,12 @@ async function _defaultReadPredecessorRowHash(firstCounter) {
|
|
|
349
350
|
async function _buildBundle(args) {
|
|
350
351
|
var kind = args.kind;
|
|
351
352
|
var rows = args.rows;
|
|
353
|
+
// Verification witnesses: the rows between the purgeable slice's tip and
|
|
354
|
+
// the covering checkpoint's anchored counter. They ride the bundle so the
|
|
355
|
+
// chain can be walked up to the SIGNED atRowHash, but they are NOT part of
|
|
356
|
+
// the purgeable range (the manifest range below is computed from `rows`,
|
|
357
|
+
// and purge() only deletes [first..lastCounter]).
|
|
358
|
+
var witnessRows = args.witnessRows || [];
|
|
352
359
|
var checkpoint = args.checkpoint || null;
|
|
353
360
|
var passphrase = args.passphrase;
|
|
354
361
|
var predecessorRowHash = args.predecessorRowHash;
|
|
@@ -357,8 +364,9 @@ async function _buildBundle(args) {
|
|
|
357
364
|
var lastRow = rows[rows.length - 1];
|
|
358
365
|
var files = {};
|
|
359
366
|
|
|
360
|
-
// 1. Encrypt the rows JSONL
|
|
361
|
-
|
|
367
|
+
// 1. Encrypt the rows JSONL — purgeable slice followed by any witnesses
|
|
368
|
+
// (contiguous, ascending) so the verifier walks one unbroken chain.
|
|
369
|
+
var jsonl = rows.concat(witnessRows).map(function (r) {
|
|
362
370
|
return JSON.stringify(_rowToWireForm(r));
|
|
363
371
|
}).join("\n") + "\n";
|
|
364
372
|
var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
|
|
@@ -437,26 +445,39 @@ async function _readBundle(inDir, passphrase) {
|
|
|
437
445
|
"bundle directory does not exist: " + inDir);
|
|
438
446
|
}
|
|
439
447
|
var manifestPath = nodePath.join(inDir, "manifest.json");
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
var manifest = safeJson.parse(
|
|
448
|
+
// Capped fd-bound read (no existsSync check-then-read window): an externally-
|
|
449
|
+
// supplied bundle manifest is parsed before verification, so an oversized
|
|
450
|
+
// manifest.json would OOM the verifier before safeJson sees it. 4 MiB is far
|
|
451
|
+
// above any real manifest.
|
|
452
|
+
var manifest = safeJson.parse(atomicFile.fdSafeReadSync(manifestPath, {
|
|
453
|
+
maxBytes: C.BYTES.mib(4), encoding: "utf8",
|
|
454
|
+
errorFor: function (kind, detail) {
|
|
455
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-manifest", "manifest.json missing in " + inDir);
|
|
456
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/bad-format", "manifest.json too large (" + detail.size + " > " + detail.max + ")");
|
|
457
|
+
return new AuditToolsError("audit-tools/bad-format", "manifest.json unreadable: " + kind);
|
|
458
|
+
},
|
|
459
|
+
}), { maxBytes: C.BYTES.mib(4) });
|
|
445
460
|
if (!manifest || manifest.format !== BUNDLE_FORMAT) {
|
|
446
461
|
throw new AuditToolsError("audit-tools/bad-format",
|
|
447
462
|
"manifest.format is not " + BUNDLE_FORMAT);
|
|
448
463
|
}
|
|
449
|
-
if (!VALID_KINDS
|
|
464
|
+
if (!Object.prototype.hasOwnProperty.call(VALID_KINDS, manifest.kind)) {
|
|
450
465
|
throw new AuditToolsError("audit-tools/bad-kind",
|
|
451
466
|
"manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
|
|
452
467
|
}
|
|
453
468
|
|
|
454
469
|
var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
470
|
+
// Capped fd-bound read: rows.enc is the PQC-encrypted archived audit slice
|
|
471
|
+
// (can be large); a hostile multi-GB blob would be read + decrypted in memory
|
|
472
|
+
// before the checksum check. 512 MiB ceiling bounds it (opt-tunable).
|
|
473
|
+
var rowsEnc = atomicFile.fdSafeReadSync(rowsEncPath, {
|
|
474
|
+
maxBytes: C.BYTES.mib(512),
|
|
475
|
+
errorFor: function (kind) {
|
|
476
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc missing in " + inDir);
|
|
477
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/rows-too-large", "rows.enc exceeds the bundle size cap");
|
|
478
|
+
return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc unreadable: " + kind);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
460
481
|
if (manifest.checksum && manifest.checksum.rowsSha3_512 &&
|
|
461
482
|
backupCrypto.checksum(rowsEnc) !== manifest.checksum.rowsSha3_512) {
|
|
462
483
|
throw new AuditToolsError("audit-tools/rows-checksum-mismatch",
|
|
@@ -470,11 +491,16 @@ async function _readBundle(inDir, passphrase) {
|
|
|
470
491
|
var checkpoint = null;
|
|
471
492
|
if (manifest.kind === KIND_ARCHIVE) {
|
|
472
493
|
var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
494
|
+
// Capped fd-bound read: checkpoint.enc is a single PQC-encrypted
|
|
495
|
+
// audit_checkpoints row (a few KiB); 4 MiB bounds a hostile blob.
|
|
496
|
+
var ckptEnc = atomicFile.fdSafeReadSync(ckptPath, {
|
|
497
|
+
maxBytes: C.BYTES.mib(4),
|
|
498
|
+
errorFor: function (kind) {
|
|
499
|
+
if (kind === "enoent") return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
|
|
500
|
+
if (kind === "too-large") return new AuditToolsError("audit-tools/checkpoint-too-large", "checkpoint.enc exceeds the cap");
|
|
501
|
+
return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc unreadable: " + kind);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
478
504
|
if (manifest.checksum && manifest.checksum.checkpointSha3_512 &&
|
|
479
505
|
backupCrypto.checksum(ckptEnc) !== manifest.checksum.checkpointSha3_512) {
|
|
480
506
|
throw new AuditToolsError("audit-tools/checkpoint-checksum-mismatch",
|
|
@@ -562,10 +588,31 @@ async function archive(opts) {
|
|
|
562
588
|
|
|
563
589
|
var predecessorRowHash = await readPredecessorHash(firstCounter);
|
|
564
590
|
|
|
591
|
+
// The signed checkpoint is the bundle's only unforgeable anchor; it commits
|
|
592
|
+
// the row at checkpoint.atMonotonicCounter. When that counter sits BEYOND
|
|
593
|
+
// the purgeable slice's tip (the operator archived a subset older than the
|
|
594
|
+
// last checkpoint), carry the in-between rows as verification witnesses so
|
|
595
|
+
// verifyBundle can chain-walk up to the anchored row and bind atRowHash to
|
|
596
|
+
// it. Without them an attacker could pair any genuine high-counter
|
|
597
|
+
// checkpoint with a wholly fabricated slice. The witnesses are NOT purged.
|
|
598
|
+
var anchorCounter = Number(checkpoint.atMonotonicCounter);
|
|
599
|
+
var witnessRows = [];
|
|
600
|
+
if (anchorCounter > lastCounter) {
|
|
601
|
+
witnessRows = await readRows({ firstCounter: lastCounter + 1, lastCounter: anchorCounter });
|
|
602
|
+
var witnessTip = witnessRows.length ? Number(witnessRows[witnessRows.length - 1].monotonicCounter) : null;
|
|
603
|
+
if (witnessTip !== anchorCounter) {
|
|
604
|
+
throw new AuditToolsError("audit-tools/anchor-rows-missing",
|
|
605
|
+
"archive: covering checkpoint anchors counter=" + anchorCounter +
|
|
606
|
+
" but the rows up to it are not all available (read up to " + witnessTip +
|
|
607
|
+
") — cannot prove the slice chains to the signed anchor");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
565
611
|
if (returnBytes) {
|
|
566
612
|
var built = await _buildBundle({
|
|
567
613
|
kind: KIND_ARCHIVE,
|
|
568
614
|
rows: rows,
|
|
615
|
+
witnessRows: witnessRows,
|
|
569
616
|
checkpoint: checkpoint,
|
|
570
617
|
passphrase: opts.passphrase,
|
|
571
618
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -582,6 +629,7 @@ async function archive(opts) {
|
|
|
582
629
|
outDir: opts.out,
|
|
583
630
|
kind: KIND_ARCHIVE,
|
|
584
631
|
rows: rows,
|
|
632
|
+
witnessRows: witnessRows,
|
|
585
633
|
checkpoint: checkpoint,
|
|
586
634
|
passphrase: opts.passphrase,
|
|
587
635
|
predecessorRowHash: predecessorRowHash,
|
|
@@ -769,26 +817,41 @@ async function verifyBundle(opts) {
|
|
|
769
817
|
};
|
|
770
818
|
}
|
|
771
819
|
|
|
772
|
-
// 2. Confirm the stored firstRowHash + lastRowHash match the
|
|
820
|
+
// 2. Confirm the stored firstRowHash + lastRowHash match the PURGEABLE
|
|
821
|
+
// slice boundary. The slice tip is the row at range.lastCounter — not
|
|
822
|
+
// necessarily the physical last row, which may be a verification witness
|
|
823
|
+
// carried past the slice so the chain can reach the signed checkpoint.
|
|
824
|
+
var lastCounterN = Number(read.manifest.range.lastCounter);
|
|
825
|
+
var sliceLastRow = null;
|
|
826
|
+
for (var ri = 0; ri < read.rows.length; ri++) {
|
|
827
|
+
if (Number(read.rows[ri].monotonicCounter) === lastCounterN) { sliceLastRow = read.rows[ri]; break; }
|
|
828
|
+
}
|
|
773
829
|
if (read.rows[0].rowHash !== read.manifest.range.firstRowHash) {
|
|
774
830
|
return {
|
|
775
831
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
776
832
|
reason: "manifest.range.firstRowHash does not match first row's rowHash",
|
|
777
833
|
};
|
|
778
834
|
}
|
|
779
|
-
if (
|
|
835
|
+
if (!sliceLastRow || sliceLastRow.rowHash !== read.manifest.range.lastRowHash) {
|
|
780
836
|
return {
|
|
781
837
|
ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
|
|
782
|
-
reason: "manifest.range.lastRowHash does not match
|
|
838
|
+
reason: "manifest.range.lastRowHash does not match the slice row at lastCounter",
|
|
783
839
|
};
|
|
784
840
|
}
|
|
785
841
|
|
|
786
|
-
// 3. (archive only) verify the covering checkpoint signature
|
|
842
|
+
// 3. (archive only) verify the covering checkpoint signature AND bind its
|
|
843
|
+
// anchored rowHash to the slice. The signature alone proves only that a
|
|
844
|
+
// checkpoint exists for some (counter, rowHash); without binding atRowHash
|
|
845
|
+
// to the archived rows, any genuine high-counter checkpoint could be paired
|
|
846
|
+
// with a wholly fabricated slice. The row at checkpoint.atMonotonicCounter
|
|
847
|
+
// must be present in the bundle (slice or witness) and its rowHash must
|
|
848
|
+
// equal checkpoint.atRowHash — mirroring b.audit.verifyCheckpoints against
|
|
849
|
+
// the live table.
|
|
787
850
|
if (read.manifest.kind === KIND_ARCHIVE) {
|
|
788
851
|
if (!read.checkpoint) {
|
|
789
852
|
return { ok: false, kind: KIND_ARCHIVE, reason: "checkpoint missing from archive bundle" };
|
|
790
853
|
}
|
|
791
|
-
if (Number(read.checkpoint.atMonotonicCounter) <
|
|
854
|
+
if (Number(read.checkpoint.atMonotonicCounter) < lastCounterN) {
|
|
792
855
|
return {
|
|
793
856
|
ok: false, kind: KIND_ARCHIVE,
|
|
794
857
|
reason: "checkpoint atMonotonicCounter (" + read.checkpoint.atMonotonicCounter +
|
|
@@ -805,6 +868,28 @@ async function verifyBundle(opts) {
|
|
|
805
868
|
};
|
|
806
869
|
}
|
|
807
870
|
}
|
|
871
|
+
// Bind: the row the signature anchors must be in the bundle and match.
|
|
872
|
+
var anchorCounterN = Number(read.checkpoint.atMonotonicCounter);
|
|
873
|
+
var anchoredRow = null;
|
|
874
|
+
for (var ai = 0; ai < read.rows.length; ai++) {
|
|
875
|
+
if (Number(read.rows[ai].monotonicCounter) === anchorCounterN) { anchoredRow = read.rows[ai]; break; }
|
|
876
|
+
}
|
|
877
|
+
if (!anchoredRow) {
|
|
878
|
+
return {
|
|
879
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
880
|
+
reason: "checkpoint anchors counter=" + anchorCounterN +
|
|
881
|
+
" but no such row is present in the bundle — checkpoint not bound to the archived slice",
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (anchoredRow.rowHash !== read.checkpoint.atRowHash) {
|
|
885
|
+
return {
|
|
886
|
+
ok: false, kind: KIND_ARCHIVE,
|
|
887
|
+
reason: "checkpoint atRowHash does not match the bundle row at counter=" + anchorCounterN +
|
|
888
|
+
" — the signed anchor does not bind this slice",
|
|
889
|
+
expected: read.checkpoint.atRowHash,
|
|
890
|
+
actual: anchoredRow.rowHash,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
808
893
|
}
|
|
809
894
|
|
|
810
895
|
return {
|
package/lib/audit.js
CHANGED
|
@@ -797,7 +797,12 @@ async function query(criteria) {
|
|
|
797
797
|
if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
|
|
798
798
|
if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
|
|
799
799
|
|
|
800
|
-
|
|
800
|
+
// order: "asc" (default, chronological) | "desc" (newest-first). A capped
|
|
801
|
+
// query (limit set) returns the FIRST `limit` rows in this order — so a
|
|
802
|
+
// consumer that wants the most RECENT events under a cap (e.g. the daily
|
|
803
|
+
// review) must pass order:"desc", else `limit` keeps the OLDEST and drops the
|
|
804
|
+
// newest events in the window.
|
|
805
|
+
q.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
801
806
|
if (criteria.limit != null) q.limit(criteria.limit);
|
|
802
807
|
if (criteria.offset != null) q.offset(criteria.offset);
|
|
803
808
|
|
|
@@ -834,7 +839,7 @@ async function _queryCluster(criteria) {
|
|
|
834
839
|
if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
|
|
835
840
|
if (criteria.outcome) qb.where("outcome", criteria.outcome);
|
|
836
841
|
|
|
837
|
-
qb.orderBy("monotonicCounter", "asc");
|
|
842
|
+
qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
|
|
838
843
|
if (criteria.limit != null) qb.limit(criteria.limit);
|
|
839
844
|
if (criteria.offset != null) qb.offset(criteria.offset);
|
|
840
845
|
|
package/lib/auth/access-lock.js
CHANGED
|
@@ -54,7 +54,7 @@ var SAFE_METHODS = Object.freeze({ GET: 1, HEAD: 1, OPTIONS: 1 });
|
|
|
54
54
|
function _normalizeMode(mode) {
|
|
55
55
|
if (typeof mode !== "string") return null;
|
|
56
56
|
var m = mode.toLowerCase();
|
|
57
|
-
return VALID_MODES
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(VALID_MODES, m) ? m : null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function create(opts) {
|
|
@@ -164,7 +164,7 @@ function create(opts) {
|
|
|
164
164
|
if (_hasUnlockRole(req)) return next();
|
|
165
165
|
if (currentMode === "read-only") {
|
|
166
166
|
var method = (req.method || "GET").toUpperCase();
|
|
167
|
-
if (SAFE_METHODS
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(SAFE_METHODS, method)) return next();
|
|
168
168
|
_emitAudit("refused", "denied", { mode: currentMode, method: method, path: req.url });
|
|
169
169
|
_emitMetric("refused", 1, { mode: currentMode, reason: "non-safe-method" });
|
|
170
170
|
return _refuse(res, "non-safe-method-in-read-only");
|
|
@@ -343,7 +343,7 @@ function create(opts) {
|
|
|
343
343
|
var secret = opts.secret;
|
|
344
344
|
|
|
345
345
|
var providerKey = opts.provider !== undefined ? opts.provider : DEFAULT_PROVIDER;
|
|
346
|
-
if (typeof providerKey !== "string" || !PROVIDERS
|
|
346
|
+
if (typeof providerKey !== "string" || !Object.prototype.hasOwnProperty.call(PROVIDERS, providerKey)) {
|
|
347
347
|
var supported = Object.keys(PROVIDERS).join(", ");
|
|
348
348
|
throw new BotChallengeError("bot-challenge/bad-opt",
|
|
349
349
|
"provider: expected one of [" + supported + "], got " + JSON.stringify(providerKey));
|
package/lib/auth/ciba.js
CHANGED
|
@@ -452,6 +452,23 @@ function create(opts) {
|
|
|
452
452
|
});
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// Verify an OIDC ID token (if present) via the composed inner OAuth client,
|
|
456
|
+
// returning its verified claims, or null when no id_token was supplied. A
|
|
457
|
+
// present-but-invalid id_token throws (fail-closed) so a forged token's
|
|
458
|
+
// sub/acr/amr can never be returned as trusted.
|
|
459
|
+
async function _verifyIdTokenIfPresent(idToken, auditKey, op) {
|
|
460
|
+
if (!idToken) return null;
|
|
461
|
+
try {
|
|
462
|
+
return await inner.verifyIdToken(idToken);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
_emitAudit("id_token_verify_fail", "failure",
|
|
465
|
+
auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
|
|
466
|
+
throw new AuthError("auth-ciba/id-token-invalid",
|
|
467
|
+
"ciba." + op + ": id_token failed verification: " +
|
|
468
|
+
((e && e.code) || (e && e.message) || String(e)));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
455
472
|
async function pollToken(popts) {
|
|
456
473
|
popts = popts || {};
|
|
457
474
|
if (typeof popts.authReqId !== "string" || popts.authReqId.length === 0) {
|
|
@@ -514,6 +531,12 @@ function create(opts) {
|
|
|
514
531
|
}
|
|
515
532
|
// Token issued — clear interval tracking for this authReqId.
|
|
516
533
|
_intervalState.delete(popts.authReqId);
|
|
534
|
+
// Verify the ID token (OIDC Core §3.1.3.7) via the composed inner OAuth
|
|
535
|
+
// client — it applies the create()-level issuer / clientId / JWKS /
|
|
536
|
+
// accepted-algorithms and enforces signature + iss/aud/exp. Returning an
|
|
537
|
+
// unverified id_token let a forged token's sub/acr/amr be trusted.
|
|
538
|
+
var pollClaims = await _verifyIdTokenIfPresent(rv.id_token,
|
|
539
|
+
"auth-ciba:" + popts.authReqId, "pollToken");
|
|
517
540
|
_emitAudit("token_received", "success", {
|
|
518
541
|
authReqIdHash: sha3Hash("auth-ciba:" + popts.authReqId),
|
|
519
542
|
});
|
|
@@ -521,6 +544,7 @@ function create(opts) {
|
|
|
521
544
|
return {
|
|
522
545
|
accessToken: rv.access_token || null,
|
|
523
546
|
idToken: rv.id_token || null,
|
|
547
|
+
claims: pollClaims,
|
|
524
548
|
refreshToken: rv.refresh_token || null,
|
|
525
549
|
tokenType: rv.token_type || null,
|
|
526
550
|
scope: rv.scope || null,
|
|
@@ -545,17 +569,23 @@ function create(opts) {
|
|
|
545
569
|
* - In **push** mode the body carries the full token-response
|
|
546
570
|
* object; no follow-up call needed.
|
|
547
571
|
*
|
|
572
|
+
* Async because a pushed `id_token` is verified (signature + iss/aud/exp)
|
|
573
|
+
* via the composed inner OAuth client before it is returned — the
|
|
574
|
+
* verified claims are surfaced as `claims`. A present-but-invalid
|
|
575
|
+
* id_token throws `auth-ciba/id-token-invalid` (the notification-token
|
|
576
|
+
* bearer authenticates the caller, never the token itself).
|
|
577
|
+
*
|
|
548
578
|
* @opts
|
|
549
579
|
* { body?: object } // pre-parsed body; defaults to req.body
|
|
550
580
|
*
|
|
551
581
|
* @example
|
|
552
|
-
* app.post("/ciba/notify", function (req, res) {
|
|
553
|
-
* var info = ciba.parseNotification(req, { body: req.body });
|
|
554
|
-
* // → { authReqId, accessToken, idToken, ... }
|
|
582
|
+
* app.post("/ciba/notify", async function (req, res) {
|
|
583
|
+
* var info = await ciba.parseNotification(req, { body: req.body });
|
|
584
|
+
* // → { authReqId, accessToken, idToken, claims, ... }
|
|
555
585
|
* res.statusCode = 204; res.end();
|
|
556
586
|
* });
|
|
557
587
|
*/
|
|
558
|
-
function parseNotification(req, popts) {
|
|
588
|
+
async function parseNotification(req, popts) {
|
|
559
589
|
popts = popts || {};
|
|
560
590
|
if (!req || !req.headers) {
|
|
561
591
|
throw new AuthError("auth-ciba/bad-notification-req",
|
|
@@ -605,6 +635,14 @@ function create(opts) {
|
|
|
605
635
|
throw new AuthError("auth-ciba/no-auth-req-id-in-body",
|
|
606
636
|
"ciba.parseNotification: body missing auth_req_id");
|
|
607
637
|
}
|
|
638
|
+
// Verify the pushed ID token (CIBA §10.2 / OIDC Core MUST) via the
|
|
639
|
+
// composed inner OAuth client before returning it as trusted. The
|
|
640
|
+
// notification-token bearer authenticates the CALLER, not the token; a
|
|
641
|
+
// leaked/observed notification token previously let an attacker inject
|
|
642
|
+
// arbitrary id_token claims the RP would trust on the documented
|
|
643
|
+
// "no follow-up call" push path.
|
|
644
|
+
var pushClaims = await _verifyIdTokenIfPresent(body.id_token,
|
|
645
|
+
"auth-ciba:" + body.auth_req_id, "parseNotification");
|
|
608
646
|
_emitAudit("notification_received", "success", {
|
|
609
647
|
authReqIdHash: sha3Hash("auth-ciba:" + body.auth_req_id),
|
|
610
648
|
mode: deliveryMode,
|
|
@@ -614,6 +652,7 @@ function create(opts) {
|
|
|
614
652
|
authReqId: body.auth_req_id,
|
|
615
653
|
accessToken: body.access_token || null,
|
|
616
654
|
idToken: body.id_token || null,
|
|
655
|
+
claims: pushClaims,
|
|
617
656
|
refreshToken: body.refresh_token || null,
|
|
618
657
|
tokenType: body.token_type || null,
|
|
619
658
|
scope: body.scope || null,
|
package/lib/auth/dpop.js
CHANGED
|
@@ -92,7 +92,7 @@ function thumbprint(key) {
|
|
|
92
92
|
if (!key || typeof key !== "object" || typeof key.kty !== "string" || key.kty.length === 0) {
|
|
93
93
|
throw new AuthError("auth-dpop/bad-jwk", "jwk must be an object with a kty");
|
|
94
94
|
}
|
|
95
|
-
if (!DPOP_KTY
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(DPOP_KTY, key.kty)) {
|
|
96
96
|
throw new AuthError("auth-dpop/refused-kty", "jwk.kty='" + key.kty + "' is not allowed (DPoP requires asymmetric kty)");
|
|
97
97
|
}
|
|
98
98
|
// The RFC 7638 thumbprint itself is computed by b.jwk.
|
|
@@ -348,6 +348,11 @@ async function verify(proof, opts) {
|
|
|
348
348
|
"DPoP proof declares 'crit' header — refused");
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// Bind the declared alg to the embedded JWK's kty/curve before handing
|
|
352
|
+
// the self-asserted key to node:crypto — every other JWS verifier in the
|
|
353
|
+
// framework enforces this (alg-confusion family, CVE-2026-22817 class).
|
|
354
|
+
jwtExternal._assertAlgKtyMatch(header.alg, header.jwk);
|
|
355
|
+
|
|
351
356
|
// Verify signature against the embedded jwk
|
|
352
357
|
var key = _jwkToKeyObject(header.jwk);
|
|
353
358
|
var params = _signParamsForAlg(header.alg);
|
package/lib/auth/fido-mds3.js
CHANGED
|
@@ -43,6 +43,7 @@ var safeJson = require("../safe-json");
|
|
|
43
43
|
var safeBuffer = require("../safe-buffer");
|
|
44
44
|
var lazyRequire = require("../lazy-require");
|
|
45
45
|
var validateOpts = require("../validate-opts");
|
|
46
|
+
var x509Chain = require("../x509-chain");
|
|
46
47
|
var jwtExternal = require("./jwt-external");
|
|
47
48
|
var _wa = require("../vendor/simplewebauthn-server.cjs");
|
|
48
49
|
var { FidoMds3Error } = require("../framework-error");
|
|
@@ -239,7 +240,10 @@ function _validateChain(x5c, rootPems) {
|
|
|
239
240
|
var root;
|
|
240
241
|
try { root = new nodeCrypto.X509Certificate(rootPems[r]); }
|
|
241
242
|
catch (_e) { continue; }
|
|
242
|
-
|
|
243
|
+
// issuerValidlyIssued enforces basicConstraints cA:TRUE on the root in
|
|
244
|
+
// addition to the issuance + signature linkage — a non-CA cert cannot
|
|
245
|
+
// anchor the x5c chain (basicConstraints bypass, CVE-2002-0862 class).
|
|
246
|
+
if (x509Chain.issuerValidlyIssued(root, tail)) {
|
|
243
247
|
anchored = true;
|
|
244
248
|
break;
|
|
245
249
|
}
|
package/lib/auth/jwt.js
CHANGED
|
@@ -117,7 +117,7 @@ function _toKeyObject(pemOrKey, kind) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function _resolveAlgorithm(alg) {
|
|
120
|
-
if (typeof alg !== "string" || !ALGORITHM_TO_NODE
|
|
120
|
+
if (typeof alg !== "string" || !Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, alg)) {
|
|
121
121
|
throw new AuthError("auth-jwt/unsupported-algorithm",
|
|
122
122
|
"algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")");
|
|
123
123
|
}
|
|
@@ -222,7 +222,7 @@ async function verify(token, opts) {
|
|
|
222
222
|
// Validate the allowlist itself — typoed entries should surface here,
|
|
223
223
|
// not as silent "every token rejected."
|
|
224
224
|
for (var i = 0; i < allowed.length; i++) {
|
|
225
|
-
if (!ALGORITHM_TO_NODE
|
|
225
|
+
if (!Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, allowed[i])) {
|
|
226
226
|
throw new AuthError("auth-jwt/unsupported-algorithm",
|
|
227
227
|
"opts.algorithms[" + i + "] = '" + allowed[i] + "' is not in the supported list (" +
|
|
228
228
|
SUPPORTED_ALGORITHMS.join(", ") + ")");
|
package/lib/auth/lockout.js
CHANGED
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
82
|
var C = require("../constants");
|
|
83
|
+
var numericBounds = require("../numeric-bounds");
|
|
83
84
|
var lazyRequire = require("../lazy-require");
|
|
84
85
|
var requestHelpers = require("../request-helpers");
|
|
85
86
|
var validateOpts = require("../validate-opts");
|
|
@@ -119,7 +120,7 @@ function _requireString(name, val) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function _requirePositiveInt(name, val) {
|
|
122
|
-
if (
|
|
123
|
+
if (!numericBounds.isPositiveFiniteInt(val)) {
|
|
123
124
|
throw _err("BAD_OPT", name + ": expected positive integer, got " +
|
|
124
125
|
typeof val + " " + JSON.stringify(val));
|
|
125
126
|
}
|
|
@@ -261,7 +262,22 @@ function create(opts) {
|
|
|
261
262
|
|
|
262
263
|
// ---- Public surface ----
|
|
263
264
|
|
|
264
|
-
|
|
265
|
+
// Per-key serialization of the failure counter (read→increment→write on an
|
|
266
|
+
// async store): concurrent recordFailure calls for the same key would lose
|
|
267
|
+
// updates, letting parallel failures stay under the lockout threshold. A
|
|
268
|
+
// per-key promise chain applies them sequentially in-process.
|
|
269
|
+
var _recordChains = new Map();
|
|
270
|
+
function recordFailure(key, callOpts) {
|
|
271
|
+
var prev = _recordChains.get(key) || Promise.resolve();
|
|
272
|
+
var run = prev.then(function () { return _doRecordFailure(key, callOpts); },
|
|
273
|
+
function () { return _doRecordFailure(key, callOpts); });
|
|
274
|
+
var tail = run.then(function () {}, function () {});
|
|
275
|
+
_recordChains.set(key, tail);
|
|
276
|
+
tail.then(function () { if (_recordChains.get(key) === tail) _recordChains.delete(key); });
|
|
277
|
+
return run;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function _doRecordFailure(key, callOpts) {
|
|
265
281
|
_requireKey(key);
|
|
266
282
|
callOpts = callOpts || {};
|
|
267
283
|
var now = clock();
|
package/lib/auth/passkey.js
CHANGED
|
@@ -385,7 +385,7 @@ function _largeBlobExt(args) {
|
|
|
385
385
|
var SUPPORT = { preferred: 1, required: 1 };
|
|
386
386
|
var modes = 0;
|
|
387
387
|
if (args.support !== undefined) {
|
|
388
|
-
if (!SUPPORT
|
|
388
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORT, args.support)) {
|
|
389
389
|
throw new AuthError("auth-passkey/bad-largeblob-support",
|
|
390
390
|
"extensions.largeBlob support must be 'preferred' or 'required'");
|
|
391
391
|
}
|
package/lib/auth/password.js
CHANGED
|
@@ -282,7 +282,7 @@ function policy(opts) {
|
|
|
282
282
|
// Apply named profile FIRST, then operator opts on top so the
|
|
283
283
|
// operator can override profile defaults per-field.
|
|
284
284
|
if (typeof opts.profile === "string" && opts.profile.length > 0) {
|
|
285
|
-
if (!POLICY_PROFILES
|
|
285
|
+
if (!Object.prototype.hasOwnProperty.call(POLICY_PROFILES, opts.profile)) {
|
|
286
286
|
throw new AuthError("auth-password/bad-policy",
|
|
287
287
|
"policy.profile must be one of " + Object.keys(POLICY_PROFILES).join("/") +
|
|
288
288
|
", got " + JSON.stringify(opts.profile));
|
package/lib/auth/saml.js
CHANGED
|
@@ -170,7 +170,7 @@ function _verifyXmldsig(envelope, signatureNode, certPem) {
|
|
|
170
170
|
}
|
|
171
171
|
var sigMethodNode = _findChild(signedInfo, "SignatureMethod");
|
|
172
172
|
var sigAlgo = sigMethodNode && _attr(sigMethodNode, "Algorithm");
|
|
173
|
-
if (!SUPPORTED_SIG
|
|
173
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_SIG, sigAlgo)) {
|
|
174
174
|
throw new AuthError("auth-saml/unsupported-sig-alg",
|
|
175
175
|
"Unsupported SignatureMethod: " + sigAlgo);
|
|
176
176
|
}
|
|
@@ -184,7 +184,7 @@ function _verifyXmldsig(envelope, signatureNode, certPem) {
|
|
|
184
184
|
var refId = refUri.substring(1);
|
|
185
185
|
var digestMethodNode = _findChild(refNode, "DigestMethod");
|
|
186
186
|
var digestAlgo = digestMethodNode && _attr(digestMethodNode, "Algorithm");
|
|
187
|
-
if (!SUPPORTED_DIGEST
|
|
187
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_DIGEST, digestAlgo)) {
|
|
188
188
|
throw new AuthError("auth-saml/unsupported-digest",
|
|
189
189
|
"Unsupported DigestMethod: " + digestAlgo);
|
|
190
190
|
}
|
|
@@ -745,26 +745,46 @@ function create(opts) {
|
|
|
745
745
|
var cNotOnOrAfter = _attr(conditions, "NotOnOrAfter");
|
|
746
746
|
if (cNotBefore) {
|
|
747
747
|
var cnb = Date.parse(cNotBefore) / 1000; // ms→s
|
|
748
|
-
|
|
748
|
+
// Fail CLOSED on a present-but-unparseable bound (mirrors the Bearer
|
|
749
|
+
// SCD path at line ~708) instead of skipping the window via isFinite().
|
|
750
|
+
if (!isFinite(cnb)) {
|
|
751
|
+
throw new AuthError("auth-saml/conditions-bad-timestamp",
|
|
752
|
+
"Conditions NotBefore is present but unparseable");
|
|
753
|
+
}
|
|
754
|
+
if (cnb > nowSec + clockSkewSec) {
|
|
749
755
|
throw new AuthError("auth-saml/conditions-not-yet-valid",
|
|
750
756
|
"Conditions NotBefore is in the future");
|
|
751
757
|
}
|
|
752
758
|
}
|
|
753
759
|
if (cNotOnOrAfter) {
|
|
754
760
|
var cnoa = Date.parse(cNotOnOrAfter) / 1000; // ms→s
|
|
755
|
-
if (isFinite(cnoa)
|
|
761
|
+
if (!isFinite(cnoa)) {
|
|
762
|
+
throw new AuthError("auth-saml/conditions-bad-timestamp",
|
|
763
|
+
"Conditions NotOnOrAfter is present but unparseable");
|
|
764
|
+
}
|
|
765
|
+
if (cnoa < nowSec - clockSkewSec) {
|
|
756
766
|
throw new AuthError("auth-saml/conditions-expired",
|
|
757
767
|
"Conditions NotOnOrAfter has passed");
|
|
758
768
|
}
|
|
759
769
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
770
|
+
}
|
|
771
|
+
// Audience binding — a signed assertion is bound to THIS SP via
|
|
772
|
+
// AudienceRestriction. A missing Conditions or AudienceRestriction means
|
|
773
|
+
// it is not bound here (audience-confusion: an assertion minted for another
|
|
774
|
+
// SP). Fail closed when an audience is configured; opt out only via
|
|
775
|
+
// vopts.requireAudienceRestriction === false.
|
|
776
|
+
if (audience && vopts.requireAudienceRestriction !== false) {
|
|
777
|
+
var ar = conditions && _findChild(conditions, "AudienceRestriction", SAML_NS.assertion);
|
|
778
|
+
if (!ar) {
|
|
779
|
+
throw new AuthError("auth-saml/no-audience-restriction",
|
|
780
|
+
"verifyResponse: assertion has no AudienceRestriction binding it to \"" +
|
|
781
|
+
audience + "\" (audience-confusion defense; set requireAudienceRestriction:false to opt out)");
|
|
782
|
+
}
|
|
783
|
+
var audiences = _findAllChildren(ar, "Audience", SAML_NS.assertion).map(_textContent);
|
|
784
|
+
if (audiences.indexOf(audience) === -1) {
|
|
785
|
+
throw new AuthError("auth-saml/wrong-audience",
|
|
786
|
+
"Audience \"" + audience + "\" not in assertion's AudienceRestriction (got " +
|
|
787
|
+
JSON.stringify(audiences) + ")");
|
|
768
788
|
}
|
|
769
789
|
}
|
|
770
790
|
|
|
@@ -1897,7 +1917,7 @@ function _verifyEmbeddedXmlDsig(xml, idpVerifyKey, idpVerifyAlg, expectedRootLoc
|
|
|
1897
1917
|
// Allow either sha3-512 (framework default) or the SHA-2 family.
|
|
1898
1918
|
var digestAlgName;
|
|
1899
1919
|
if (digestUri === "http://www.w3.org/2007/05/xmldsig-more#sha3-512") digestAlgName = "sha3-512";
|
|
1900
|
-
else if (SUPPORTED_DIGEST
|
|
1920
|
+
else if (Object.prototype.hasOwnProperty.call(SUPPORTED_DIGEST, digestUri)) digestAlgName = SUPPORTED_DIGEST[digestUri];
|
|
1901
1921
|
else {
|
|
1902
1922
|
throw new AuthError("auth-saml/unsupported-digest",
|
|
1903
1923
|
"_verifyEmbeddedXmlDsig: DigestMethod " + digestUri + " not supported");
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -212,7 +212,7 @@ function issue(opts) {
|
|
|
212
212
|
"issue: algorithm must be one of " + SUPPORTED_ALGS.join(", "));
|
|
213
213
|
}
|
|
214
214
|
var hashAlg = opts.hashAlg || DEFAULT_HASH_ALG;
|
|
215
|
-
if (!SUPPORTED_HASH_ALGS
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
|
|
216
216
|
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
217
217
|
"issue: hashAlg must be one of " + Object.keys(SUPPORTED_HASH_ALGS).join(", "));
|
|
218
218
|
}
|
|
@@ -413,8 +413,8 @@ async function verify(presentation, opts) {
|
|
|
413
413
|
validateOpts.requireObject(opts, "auth.sdJwtVc.verify", AuthError);
|
|
414
414
|
validateOpts(opts, [
|
|
415
415
|
"issuerKeyResolver", "audience", "nonce",
|
|
416
|
-
"now", "expectedVct", "maxClockSkewSec",
|
|
417
|
-
"requireKeyBinding",
|
|
416
|
+
"now", "expectedVct", "expectedIssuer", "maxClockSkewSec",
|
|
417
|
+
"requireKeyBinding", "requireExp",
|
|
418
418
|
"keyAttestationVerifier", "requireKeyAttestation",
|
|
419
419
|
], "auth.sdJwtVc.verify");
|
|
420
420
|
|
|
@@ -528,6 +528,15 @@ async function verify(presentation, opts) {
|
|
|
528
528
|
throw new AuthError("auth-sd-jwt-vc/iat-future",
|
|
529
529
|
"verify: iat is in the future (clock skew?)");
|
|
530
530
|
}
|
|
531
|
+
// SD-JWT-VC makes `exp` OPTIONAL, so the default only checks it when present.
|
|
532
|
+
// Operators with a time-bounded trust expectation (esp. for FOREIGN issuers
|
|
533
|
+
// resolved via issuerKeyResolver) pass requireExp:true to fail closed on a
|
|
534
|
+
// missing / non-numeric exp — mirroring jwt-external's unconditional exp
|
|
535
|
+
// requirement (a credential that never expires is accepted otherwise).
|
|
536
|
+
if (opts.requireExp === true && typeof jwtParsed.payload.exp !== "number") {
|
|
537
|
+
throw new AuthError("auth-sd-jwt-vc/missing-exp",
|
|
538
|
+
"verify: requireExp is set but the credential has no numeric exp claim");
|
|
539
|
+
}
|
|
531
540
|
if (typeof jwtParsed.payload.exp === "number" && jwtParsed.payload.exp < nowSec - skew) {
|
|
532
541
|
throw new AuthError("auth-sd-jwt-vc/expired",
|
|
533
542
|
"verify: token is expired");
|
|
@@ -556,7 +565,7 @@ async function verify(presentation, opts) {
|
|
|
556
565
|
// to its own DEFAULT_HASH_ALG (`sha3-512`) which broke verification
|
|
557
566
|
// against spec-conformant issuers when `_sd_alg` was omitted.
|
|
558
567
|
var hashAlg = jwtParsed.payload._sd_alg || "sha-256";
|
|
559
|
-
if (!SUPPORTED_HASH_ALGS
|
|
568
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
|
|
560
569
|
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
561
570
|
"verify: _sd_alg \"" + hashAlg + "\" not supported");
|
|
562
571
|
}
|
|
@@ -638,6 +647,17 @@ async function verify(presentation, opts) {
|
|
|
638
647
|
jwtExternal._assertAlgKtyMatch(kbAlg, holderKey);
|
|
639
648
|
var holderKeyObj = bCrypto.importPublicJwk(holderKey);
|
|
640
649
|
var kbParsed = _verifyJwt(maybeKbJwt, holderKeyObj, kbAlg);
|
|
650
|
+
// A KB-JWT proves holder possession of THIS presentation's bytes (sd_hash),
|
|
651
|
+
// but binds it to a verifier + a fresh challenge only via aud + nonce. If
|
|
652
|
+
// the caller processes a KB-JWT without supplying both, the aud/nonce
|
|
653
|
+
// compares below would silently skip — making the presentation replayable
|
|
654
|
+
// and acceptable at any verifier. Fail CLOSED: require both (oid4vp always
|
|
655
|
+
// supplies them). RFC: SD-JWT-VC §KB-JWT / OIDC4VP nonce binding.
|
|
656
|
+
if (typeof opts.audience !== "string" || opts.audience.length === 0 ||
|
|
657
|
+
typeof opts.nonce !== "string" || opts.nonce.length === 0) {
|
|
658
|
+
throw new AuthError("auth-sd-jwt-vc/missing-replay-binding",
|
|
659
|
+
"verify: a KB-JWT requires opts.audience + opts.nonce to bind the presentation to this verifier and a fresh challenge (replay / audience-redirection defense)");
|
|
660
|
+
}
|
|
641
661
|
// Constant-time compares: the nonce is a verifier-issued replay-defense
|
|
642
662
|
// value, so a short-circuiting !== leaks a matching-prefix timing oracle.
|
|
643
663
|
// Matches the sd_hash check below (the framework's hash/token discipline).
|