@blamejs/core 0.14.6 → 0.14.7
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/README.md +3 -2
- package/lib/agent-event-bus.js +4 -4
- package/lib/agent-idempotency.js +6 -6
- package/lib/agent-orchestrator.js +9 -9
- package/lib/agent-posture-chain.js +10 -10
- package/lib/agent-saga.js +6 -7
- package/lib/agent-snapshot.js +8 -8
- package/lib/agent-stream.js +3 -3
- package/lib/agent-tenant.js +4 -4
- package/lib/agent-trace.js +5 -5
- package/lib/ai-disclosure.js +3 -3
- package/lib/app.js +2 -2
- package/lib/archive-read.js +1 -1
- package/lib/archive-tar-read.js +1 -1
- package/lib/archive-wrap.js +5 -5
- package/lib/audit-tools.js +65 -5
- package/lib/audit.js +2 -2
- package/lib/auth/ciba.js +1 -1
- package/lib/auth/dpop.js +1 -1
- package/lib/auth/fal.js +1 -1
- package/lib/auth/fido-mds3.js +2 -3
- package/lib/auth/jwt-external.js +2 -2
- package/lib/auth/oauth.js +9 -9
- package/lib/auth/oid4vci.js +7 -7
- package/lib/auth/oid4vp.js +1 -1
- package/lib/auth/openid-federation.js +5 -5
- package/lib/auth/passkey.js +6 -6
- package/lib/auth/saml.js +1 -1
- package/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/backup/index.js +18 -18
- package/lib/cache.js +4 -4
- package/lib/calendar.js +5 -5
- package/lib/circuit-breaker.js +1 -1
- package/lib/cms-codec.js +2 -2
- package/lib/compliance.js +14 -14
- package/lib/crypto-field.js +58 -21
- package/lib/crypto.js +5 -6
- package/lib/db-query.js +131 -9
- package/lib/db.js +106 -22
- package/lib/external-db.js +64 -16
- package/lib/framework-schema.js +4 -4
- package/lib/guard-list-id.js +2 -2
- package/lib/guard-list-unsubscribe.js +1 -2
- package/lib/incident-report.js +150 -0
- package/lib/mail-crypto-smime.js +1 -1
- package/lib/mail-deploy.js +3 -3
- package/lib/mail-server-managesieve.js +2 -2
- package/lib/mail-server-pop3.js +2 -2
- package/lib/mail-store.js +1 -1
- package/lib/metrics.js +8 -8
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/dpop.js +5 -5
- package/lib/middleware/idempotency-key.js +21 -22
- package/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/network-dns-resolver.js +2 -2
- package/lib/network-dns.js +1 -2
- package/lib/network-tls.js +0 -1
- package/lib/outbox.js +1 -1
- package/lib/pqc-agent.js +1 -1
- package/lib/retention.js +1 -1
- package/lib/retry.js +1 -1
- package/lib/safe-archive.js +2 -2
- package/lib/safe-ical.js +2 -2
- package/lib/safe-mime.js +1 -1
- package/lib/self-update-standalone-verifier.js +1 -1
- package/lib/self-update.js +2 -2
- package/lib/static.js +1 -1
- package/lib/subject.js +2 -2
- package/lib/vault/index.js +64 -1
- package/lib/vault/rotate.js +19 -0
- package/lib/vendor-data.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/archive-wrap.js
CHANGED
|
@@ -272,7 +272,7 @@ function _encryptForRecipient(bytes, opts) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
if (r.publicKey) {
|
|
275
|
-
//
|
|
275
|
+
// B.crypto.encrypt falls back to
|
|
276
276
|
// ML-KEM-only when ecPublicKey is undefined (with a one-shot
|
|
277
277
|
// audit). For archive-wrap's recipient contract the hybrid leg
|
|
278
278
|
// (P-384 ECDH defence-in-depth backstop on top of ML-KEM-1024)
|
|
@@ -343,7 +343,7 @@ function sniffEnvelope(bytes) {
|
|
|
343
343
|
if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
|
|
344
344
|
return "none";
|
|
345
345
|
}
|
|
346
|
-
//
|
|
346
|
+
// `Buffer.from(uint8Array)` copies
|
|
347
347
|
// the entire input, turning a constant-time 5-byte probe into an
|
|
348
348
|
// O(n) allocation. Use the zero-copy view form so the sniff is
|
|
349
349
|
// truly cheap regardless of input size.
|
|
@@ -351,7 +351,7 @@ function sniffEnvelope(bytes) {
|
|
|
351
351
|
? bytes
|
|
352
352
|
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
353
353
|
if (buf.length < 5) return "none";
|
|
354
|
-
//
|
|
354
|
+
// Match on the 5-byte ASCII magic
|
|
355
355
|
// alone, NOT on the full header (which requires version + saltLen
|
|
356
356
|
// bytes). A truncated envelope (`BAWRP` + nothing else) is still a
|
|
357
357
|
// recipient envelope; the unwrap call surfaces the truncation with
|
|
@@ -413,7 +413,7 @@ async function wrapWithPassphrase(bytes, opts) {
|
|
|
413
413
|
// alphabet. Operators sourcing passphrases from a random-bytes
|
|
414
414
|
// generator (high entropy density) pass without issue; operators
|
|
415
415
|
// typing dictionary phrases trip the gate.
|
|
416
|
-
//
|
|
416
|
+
// Typeof NaN === "number" passes
|
|
417
417
|
// typeof gate but bypasses downstream comparisons. Use isFinite
|
|
418
418
|
// so NaN / Infinity can't slip past the entropy gate.
|
|
419
419
|
var minEntropy;
|
|
@@ -516,7 +516,7 @@ async function unwrapWithPassphrase(sealed, opts) {
|
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
function _estimatePassphraseEntropyBits(passphrase) {
|
|
519
|
-
//
|
|
519
|
+
// Buffer passphrases (CSPRNG-
|
|
520
520
|
// generated random bytes) shouldn't be UTF-8 decoded for entropy
|
|
521
521
|
// estimation; the decoding artifacts (invalid sequences, BOM,
|
|
522
522
|
// surrogate pairs) make the alphabet-class measure unstable and
|
package/lib/audit-tools.js
CHANGED
|
@@ -75,9 +75,39 @@ var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
|
|
|
75
75
|
// so importing db at audit-tools' top would close the cycle. Lazy
|
|
76
76
|
// keeps the load order one-way.
|
|
77
77
|
var db = lazyRequire(function () { return require("./db"); });
|
|
78
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
78
79
|
|
|
79
80
|
var AuditToolsError = defineClass("AuditToolsError", { alwaysPermanent: true });
|
|
80
81
|
|
|
82
|
+
// Dual-control gate constants for the audit_log physical purge. The
|
|
83
|
+
// purge erases signed audit history, so when an operator has declared
|
|
84
|
+
// audit_log under b.db.declareRequireDualControl the deletion requires
|
|
85
|
+
// a consumed m-of-n grant whose action matches AUDIT_LOG_PURGE_ACTION —
|
|
86
|
+
// the same separation-of-duties control b.db.eraseHard enforces (NIST
|
|
87
|
+
// SP 800-53 AU-9 + AC-5, HIPAA 45 CFR 164.312(b), PCI-DSS v4.0 10.5.1 /
|
|
88
|
+
// 10.7, SEC 17a-4(f), CWE-778).
|
|
89
|
+
var AUDIT_LOG_GATE_TABLE = "audit_log";
|
|
90
|
+
var AUDIT_LOG_PURGE_ACTION = "auditTools.purge";
|
|
91
|
+
|
|
92
|
+
function _resolveDualControlGate(opts) {
|
|
93
|
+
var checker = typeof opts.checkDualControlGate === "function"
|
|
94
|
+
? opts.checkDualControlGate
|
|
95
|
+
: function (t) { return db()._checkDualControlGate(t); };
|
|
96
|
+
try { return checker(AUDIT_LOG_GATE_TABLE); }
|
|
97
|
+
catch (_e) { return null; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _emitPurgeDenied(gate, reason) {
|
|
101
|
+
try {
|
|
102
|
+
audit().safeEmit({
|
|
103
|
+
action: "auditTools.purge.denied",
|
|
104
|
+
outcome: "denied",
|
|
105
|
+
reason: reason,
|
|
106
|
+
metadata: { table: AUDIT_LOG_GATE_TABLE, m: gate.m, n: gate.n, posture: gate.posture || null },
|
|
107
|
+
});
|
|
108
|
+
} catch (_e) { /* drop-silent — denial audit is best-effort */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
var BUNDLE_FORMAT = "blamejs-audit-bundle-v1";
|
|
82
112
|
var KIND_ARCHIVE = "archive";
|
|
83
113
|
var KIND_EXPORT = "export";
|
|
@@ -149,7 +179,7 @@ function _rowToWireForm(row) {
|
|
|
149
179
|
return out;
|
|
150
180
|
}
|
|
151
181
|
|
|
152
|
-
//
|
|
182
|
+
// Operator-facing wire helper that surfaces recordedAt as
|
|
153
183
|
// ISO-8601 / RFC 3339 alongside the existing Unix-ms integer.
|
|
154
184
|
// Auditors comparing rows against external SIEM events expect ISO
|
|
155
185
|
// with explicit Z; the framework's primary ms storage stays
|
|
@@ -806,10 +836,11 @@ function _defaultVerifyCheckpointSignature(checkpoint) {
|
|
|
806
836
|
* `lastPurgedRowHash` becomes the new chain origin.
|
|
807
837
|
*
|
|
808
838
|
* @opts
|
|
809
|
-
* confirm:
|
|
810
|
-
* archive:
|
|
811
|
-
* passphrase:
|
|
812
|
-
* verifySignature:
|
|
839
|
+
* confirm: true, // exact `true` required
|
|
840
|
+
* archive: string, // path to a verified archive bundle
|
|
841
|
+
* passphrase: Buffer|string, // bundle decryption passphrase
|
|
842
|
+
* verifySignature: function(checkpoint),// auditor pubkey override
|
|
843
|
+
* dualControlGrant: object, // required when audit_log is declared under b.db.declareRequireDualControl — from b.dualControl.consume({ action: "auditTools.purge" })
|
|
813
844
|
*
|
|
814
845
|
* @example
|
|
815
846
|
* var result = await b.auditTools.purge({
|
|
@@ -846,6 +877,34 @@ async function purge(opts) {
|
|
|
846
877
|
"purge: bundle kind is '" + v.kind + "', must be 'archive'");
|
|
847
878
|
}
|
|
848
879
|
|
|
880
|
+
// Dual-control gate. When audit_log is declared under
|
|
881
|
+
// b.db.declareRequireDualControl, the physical purge requires a
|
|
882
|
+
// consumed m-of-n grant — confirm:true alone is not enough. Mirrors
|
|
883
|
+
// b.db.eraseHard, and additionally binds the grant's action so a
|
|
884
|
+
// grant minted for a different operation can't be replayed here.
|
|
885
|
+
var dcGate = _resolveDualControlGate(opts);
|
|
886
|
+
if (dcGate) {
|
|
887
|
+
var grant = opts.dualControlGrant;
|
|
888
|
+
if (!grant) {
|
|
889
|
+
_emitPurgeDenied(dcGate, "no-grant");
|
|
890
|
+
throw new AuditToolsError("audit-tools/dual-control-required",
|
|
891
|
+
"purge: audit_log is under dual control (m=" + dcGate.m + ", n=" + dcGate.n +
|
|
892
|
+
"); pass opts.dualControlGrant from b.dualControl.consume({ action: \"" +
|
|
893
|
+
AUDIT_LOG_PURGE_ACTION + "\" }).");
|
|
894
|
+
}
|
|
895
|
+
if (grant.ready !== true) {
|
|
896
|
+
_emitPurgeDenied(dcGate, "grant-not-ready");
|
|
897
|
+
throw new AuditToolsError("audit-tools/dual-control-grant-not-ready",
|
|
898
|
+
"purge: opts.dualControlGrant.ready must be true (a consumed m-of-n grant)");
|
|
899
|
+
}
|
|
900
|
+
if (grant.action !== AUDIT_LOG_PURGE_ACTION) {
|
|
901
|
+
_emitPurgeDenied(dcGate, "grant-action-mismatch");
|
|
902
|
+
throw new AuditToolsError("audit-tools/dual-control-grant-mismatch",
|
|
903
|
+
"purge: dualControlGrant.action is '" + grant.action + "', must be '" +
|
|
904
|
+
AUDIT_LOG_PURGE_ACTION + "'");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
849
908
|
// 2. Refuse if the archive doesn't start at the next purge point. Keeps
|
|
850
909
|
// the chain anchor monotonic — operators can't jump-purge a middle range.
|
|
851
910
|
var readAnchor = opts.readAnchor || _defaultReadPurgeAnchor;
|
|
@@ -881,6 +940,7 @@ async function purge(opts) {
|
|
|
881
940
|
lastPurgedCounter: Number(v.range.lastCounter),
|
|
882
941
|
lastPurgedRowHash: v.range.lastRowHash,
|
|
883
942
|
archiveBundleId: result.archiveBundleId,
|
|
943
|
+
dualControlConsumed: !!dcGate,
|
|
884
944
|
};
|
|
885
945
|
}
|
|
886
946
|
|
package/lib/audit.js
CHANGED
|
@@ -305,7 +305,7 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
305
305
|
"sandbox", // b.sandbox (sandbox.run / sandbox.run.refused — operator-supplied transform isolation)
|
|
306
306
|
"safeurl", // b.safeUrl.parse (safeurl.idn_homograph.refused — UTS #39 mixed-script host-label refusal)
|
|
307
307
|
"http", // b.middleware.bodyParser (http.chunked.malformed.refused — RFC 9112 §7.1 chunked-decode failure with Connection: close) // RFC number in prose
|
|
308
|
-
"cryptofield", // b.cryptoField.eraseRow (cryptofield.vacuum.skipped —
|
|
308
|
+
"cryptofield", // b.cryptoField.eraseRow (cryptofield.vacuum.skipped — vacuum-after-erase signal when DB not initialized at erase time)
|
|
309
309
|
"acme", // b.acme (acme.account.registered / order.* / cert.issued / cert.renewed / cert.renew.skipped — RFC 8555 + RFC 9773 ARI workflow)
|
|
310
310
|
"cert", // b.cert (cert.account.generated / cert.issued / cert.renewed / cert.renew-failed / cert.challenge-cleanup — turnkey cert-manager lifecycle)
|
|
311
311
|
"tls", // b.router 0-RTT posture (tls.0rtt.refused / tls.0rtt.replayed) — RFC 8446 §8 anti-replay surface // RFC number in prose
|
|
@@ -1620,7 +1620,7 @@ async function assertSegregation(opts) {
|
|
|
1620
1620
|
return { ok: ok, missing: missing };
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
|
-
// applyPosture —
|
|
1623
|
+
// applyPosture — cascade hook. b.compliance.set(posture)
|
|
1624
1624
|
// calls this to record the active posture so audit emissions can
|
|
1625
1625
|
// surface the regulatory regime in metadata where downstream tooling
|
|
1626
1626
|
// (forensic export, SIEM correlation) needs it. The chain itself is
|
package/lib/auth/ciba.js
CHANGED
|
@@ -577,7 +577,7 @@ function create(opts) {
|
|
|
577
577
|
"ciba.parseNotification: empty bearer or no expected token configured");
|
|
578
578
|
}
|
|
579
579
|
// Constant-time compare on the SHA3 hash of both tokens —
|
|
580
|
-
// matches the project-wide discipline
|
|
580
|
+
// matches the project-wide discipline. Both
|
|
581
581
|
// sides are fixed-width sha3-512 hex strings; timingSafeEqual
|
|
582
582
|
// adds explicit defense-in-depth over `!==` even though equal-
|
|
583
583
|
// length JS string compare is already broadly understood as
|
package/lib/auth/dpop.js
CHANGED
|
@@ -437,7 +437,7 @@ async function verify(proof, opts) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
// nonce — when caller supplies expected nonce, payload MUST match.
|
|
440
|
-
// Constant-time compare
|
|
440
|
+
// Constant-time compare: the nonce is a server-
|
|
441
441
|
// issued secret-shaped value matched against attacker-controlled
|
|
442
442
|
// payload bytes. RFC 9449 §8 mandates the value be unpredictable;
|
|
443
443
|
// a leaking compare reveals prefix bytes over many attempts. ath
|
package/lib/auth/fal.js
CHANGED
|
@@ -179,7 +179,7 @@ function fromAssertion(opts) {
|
|
|
179
179
|
return FAL3;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
//
|
|
182
|
+
// FAL2 per NIST SP 800-63C-4 §5.2 requires "injection
|
|
183
183
|
// protection" on the back-channel: either the back-channel itself is
|
|
184
184
|
// encrypted-and-authenticated (mTLS / signed transport) OR the
|
|
185
185
|
// assertion is encrypted to the RP. A plain HTTP back-channel with
|
package/lib/auth/fido-mds3.js
CHANGED
|
@@ -73,7 +73,7 @@ var REFUSE_STATUS = {
|
|
|
73
73
|
// FIDO MDS3 §3.1.4 — attestation-key compromise means the
|
|
74
74
|
// manufacturer's batch-signing key is suspect; every credential
|
|
75
75
|
// attested under that key MUST be refused. Pre-v0.9.2 this token
|
|
76
|
-
// was missing from the refuse-list
|
|
76
|
+
// was missing from the refuse-list.
|
|
77
77
|
ATTESTATION_KEY_COMPROMISE: 1,
|
|
78
78
|
};
|
|
79
79
|
|
|
@@ -362,7 +362,6 @@ function _verifyAndParseBlob(token) {
|
|
|
362
362
|
// cache; an attacker serving an ancient signed-but-expired BLOB
|
|
363
363
|
// could keep operators on a revoked-authenticator-list-frozen-at-X.
|
|
364
364
|
// Refuse at parse time so neither fetch nor cache lookup honors it.
|
|
365
|
-
// (Audit 2026-05-11.)
|
|
366
365
|
if (nextUpdate.getTime() < Date.now()) {
|
|
367
366
|
throw new FidoMds3Error("fido-mds3/blob-stale",
|
|
368
367
|
"BLOB payload nextUpdate \"" + payload.nextUpdate +
|
|
@@ -619,7 +618,7 @@ function verifyAuthenticator(blob, registrationInfo, vopts) {
|
|
|
619
618
|
}
|
|
620
619
|
var entry = lookupAaguid(blob, registrationInfo.aaguid);
|
|
621
620
|
if (!entry) {
|
|
622
|
-
// Fail-CLOSED default for unknown AAGUIDs
|
|
621
|
+
// Fail-CLOSED default for unknown AAGUIDs.
|
|
623
622
|
// Pre-v0.9.2 default was `ok: true, reason: "aaguid-not-in-blob"`
|
|
624
623
|
// — an attacker registering a credential with an AAGUID not in
|
|
625
624
|
// the BLOB (rogue authenticator, fake hardware) silently passed.
|
package/lib/auth/jwt-external.js
CHANGED
|
@@ -271,7 +271,7 @@ function _selectKey(keys, header, vopts) {
|
|
|
271
271
|
throw new AuthError("auth-jwt-external/no-matching-kid",
|
|
272
272
|
"no JWKS key matches header.kid='" + header.kid + "'");
|
|
273
273
|
}
|
|
274
|
-
// Refuse kid-less tokens by default
|
|
274
|
+
// Refuse kid-less tokens by default. JWKS
|
|
275
275
|
// rotation creates a window where the rotated-out key is still
|
|
276
276
|
// cached but the rotated-in key is already published; an
|
|
277
277
|
// attacker shipping a kid-less token gets the lone-key path
|
|
@@ -301,7 +301,7 @@ async function verifyExternal(token, opts) {
|
|
|
301
301
|
"audience", "issuer", "subject", "clockSkewMs",
|
|
302
302
|
// v0.9.4 — opt-out for the kid-less-token JWKS-of-one refusal
|
|
303
303
|
// (default refuses; non-conforming IdPs that emit kid-less tokens
|
|
304
|
-
// set this true).
|
|
304
|
+
// set this true).
|
|
305
305
|
"allowKidlessJwks",
|
|
306
306
|
], "auth.jwt.verifyExternal");
|
|
307
307
|
|
package/lib/auth/oauth.js
CHANGED
|
@@ -621,7 +621,7 @@ function create(opts) {
|
|
|
621
621
|
// constrained tokens (DPoP / mTLS) can opt out by NOT supplying
|
|
622
622
|
// a seen callback.
|
|
623
623
|
//
|
|
624
|
-
// Atomic check-and-insert
|
|
624
|
+
// Atomic check-and-insert — pre-v0.9.3 the
|
|
625
625
|
// check ran via `ropts.seen(token)` which was a check-then-act
|
|
626
626
|
// race: two concurrent refresh requests landed on the same
|
|
627
627
|
// event-loop tick could both see `seen === false` and both POST
|
|
@@ -648,7 +648,7 @@ function create(opts) {
|
|
|
648
648
|
// Spec contract: inserted===true → first sighting (OK);
|
|
649
649
|
// inserted===false → replay. v0.9.3 had this inverted, which
|
|
650
650
|
// broke every first refresh attempt for operators reusing an
|
|
651
|
-
// existing b.nonceStore-style backend.
|
|
651
|
+
// existing b.nonceStore-style backend.
|
|
652
652
|
alreadySeen = inserted === false;
|
|
653
653
|
} else if (typeof ropts.seen === "function") {
|
|
654
654
|
// Legacy non-atomic path. Documented as a check-then-act race;
|
|
@@ -773,7 +773,7 @@ function create(opts) {
|
|
|
773
773
|
// Constant-time compare on the CSRF state token. Project
|
|
774
774
|
// discipline (auth/dpop.js, mail-srs.js, webhook.js) is
|
|
775
775
|
// timingSafeEqual for any secret-shaped value compared
|
|
776
|
-
// against attacker-controlled input.
|
|
776
|
+
// against attacker-controlled input.
|
|
777
777
|
if (typeof query.state !== "string" ||
|
|
778
778
|
!cryptoTimingSafeEqual(query.state, popts.expectedState)) {
|
|
779
779
|
throw new OAuthError("auth-oauth/state-mismatch",
|
|
@@ -971,7 +971,7 @@ function create(opts) {
|
|
|
971
971
|
// surface as `["admin", "read"]` and the operator's scope
|
|
972
972
|
// allowlist saw two distinct scopes. Spec-strict split on
|
|
973
973
|
// single-space + reject scope tokens that contain non-token
|
|
974
|
-
// chars.
|
|
974
|
+
// chars.
|
|
975
975
|
scope: raw.scope ? raw.scope.split(" ").filter(function (s) { return s.length > 0; }) : scope.slice(),
|
|
976
976
|
raw: raw,
|
|
977
977
|
};
|
|
@@ -1052,7 +1052,7 @@ function create(opts) {
|
|
|
1052
1052
|
// verifier in the framework (jwt.js, jwt-external.js, dpop.js)
|
|
1053
1053
|
// refuses; verifyIdToken previously silently ignored, letting an
|
|
1054
1054
|
// attacker-controlled OP ship critical extensions the verifier
|
|
1055
|
-
// doesn't understand.
|
|
1055
|
+
// doesn't understand.
|
|
1056
1056
|
if (header.crit !== undefined && header.crit !== null) {
|
|
1057
1057
|
throw new OAuthError("auth-oauth/crit-not-supported",
|
|
1058
1058
|
"ID token JWS header carries 'crit' extension list; this verifier does not " +
|
|
@@ -1072,7 +1072,7 @@ function create(opts) {
|
|
|
1072
1072
|
// key was still cached at the IdP but the rotated-in key is
|
|
1073
1073
|
// already published. Refuse kid-less tokens unconditionally —
|
|
1074
1074
|
// every modern IdP includes kid; absent kid is a spec smell.
|
|
1075
|
-
//
|
|
1075
|
+
// Operators with non-conforming IdPs that
|
|
1076
1076
|
// genuinely emit kid-less tokens can opt out via
|
|
1077
1077
|
// vopts.allowKidlessJwks = true with a logged warning.
|
|
1078
1078
|
if (!match) {
|
|
@@ -1117,7 +1117,7 @@ function create(opts) {
|
|
|
1117
1117
|
// ES256 signature attempted against an RS256 key returned by a
|
|
1118
1118
|
// hostile or buggy IdP with duplicate kids). Wrap so the panic
|
|
1119
1119
|
// becomes a typed AuthError, matching the discipline in
|
|
1120
|
-
// jwt-external.js + dpop.js.
|
|
1120
|
+
// jwt-external.js + dpop.js.
|
|
1121
1121
|
var verified;
|
|
1122
1122
|
try {
|
|
1123
1123
|
verified = nodeCrypto.verify(params.hash, Buffer.from(signingInput, "ascii"), verifyOpts, sig);
|
|
@@ -1179,7 +1179,7 @@ function create(opts) {
|
|
|
1179
1179
|
}
|
|
1180
1180
|
if (vopts.nonce && !vopts.skipNonceCheck) {
|
|
1181
1181
|
// Constant-time nonce compare — secret-shaped value matched
|
|
1182
|
-
// against attacker-controlled payload.
|
|
1182
|
+
// against attacker-controlled payload.
|
|
1183
1183
|
if (typeof payload.nonce !== "string" ||
|
|
1184
1184
|
!cryptoTimingSafeEqual(payload.nonce, vopts.nonce)) {
|
|
1185
1185
|
throw new OAuthError("auth-oauth/nonce-mismatch",
|
|
@@ -1212,7 +1212,7 @@ function create(opts) {
|
|
|
1212
1212
|
// supplied; an operator typo could ship `http://` or
|
|
1213
1213
|
// `javascript:`. Route through the framework's URL gate before
|
|
1214
1214
|
// emitting so the URL is validated the same way as every other
|
|
1215
|
-
// operator-supplied OAuth URL
|
|
1215
|
+
// operator-supplied OAuth URL.
|
|
1216
1216
|
_validateUrl(uopts.postLogoutRedirectUri, allowHttp, "postLogoutRedirectUri");
|
|
1217
1217
|
params.set("post_logout_redirect_uri", uopts.postLogoutRedirectUri);
|
|
1218
1218
|
}
|
package/lib/auth/oid4vci.js
CHANGED
|
@@ -117,7 +117,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
117
117
|
"credential issuance: proof JWT alg \"" + header.alg + "\" not in issuer-supported set " +
|
|
118
118
|
"(alg-allowlist gate — refused before key lookup)");
|
|
119
119
|
}
|
|
120
|
-
//
|
|
120
|
+
// RFC 7515 §4.1.11 — refuse non-empty `crit`. Pre-v0.9.x
|
|
121
121
|
// silently ignored, letting an attacker-controlled wallet declare
|
|
122
122
|
// critical extensions the verifier doesn't understand.
|
|
123
123
|
if (header.crit !== undefined && header.crit !== null) {
|
|
@@ -136,7 +136,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
136
136
|
}
|
|
137
137
|
if (expectedCNonce !== null) {
|
|
138
138
|
// Constant-time c_nonce compare — secret-shaped value vs
|
|
139
|
-
// attacker-controlled wallet payload.
|
|
139
|
+
// attacker-controlled wallet payload.
|
|
140
140
|
if (typeof payload.nonce !== "string" ||
|
|
141
141
|
!timingSafeEqual(payload.nonce, expectedCNonce)) {
|
|
142
142
|
throw new AuthError("auth-oid4vci/wrong-proof-nonce",
|
|
@@ -148,14 +148,14 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
148
148
|
"credential issuance: proof JWT must include iat");
|
|
149
149
|
}
|
|
150
150
|
var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
|
|
151
|
-
//
|
|
151
|
+
// Use C.TIME for the 60s skew tolerance rather than a bare
|
|
152
152
|
// 60 literal; matches the framework's constants discipline.
|
|
153
153
|
var iatSkewSec = C.TIME.seconds(60) / C.TIME.seconds(1);
|
|
154
154
|
if (payload.iat > nowSec + iatSkewSec) {
|
|
155
155
|
throw new AuthError("auth-oid4vci/proof-iat-future",
|
|
156
156
|
"credential issuance: proof JWT iat is in the future");
|
|
157
157
|
}
|
|
158
|
-
//
|
|
158
|
+
// Operator-tunable proof max-age. Default 10 minutes per
|
|
159
159
|
// OID4VCI §7.2.1.1; operators with longer-lived wallet flows raise.
|
|
160
160
|
var effectiveMaxAgeMs = (typeof proofMaxAgeMs === "number" && isFinite(proofMaxAgeMs) && proofMaxAgeMs > 0)
|
|
161
161
|
? proofMaxAgeMs
|
|
@@ -304,12 +304,12 @@ function create(opts) {
|
|
|
304
304
|
var preAuthTtl = opts.preAuthCodeTtlMs || DEFAULT_PRE_AUTH_TTL_MS;
|
|
305
305
|
var accessTokenTtl = opts.accessTokenTtlMs || DEFAULT_ACCESS_TOKEN_TTL;
|
|
306
306
|
var cNonceTtl = opts.cNonceTtlMs || DEFAULT_C_NONCE_TTL_MS;
|
|
307
|
-
//
|
|
307
|
+
// Operator-tunable proof iat-too-old window. Default 10
|
|
308
308
|
// minutes per OID4VCI §7.2.1.1.
|
|
309
309
|
var proofMaxAgeMs = (typeof opts.proofMaxAgeMs === "number" && isFinite(opts.proofMaxAgeMs) && opts.proofMaxAgeMs > 0)
|
|
310
310
|
? opts.proofMaxAgeMs
|
|
311
311
|
: C.TIME.minutes(10);
|
|
312
|
-
//
|
|
312
|
+
// Access-token single-use. OID4VCI §7's credential endpoint
|
|
313
313
|
// does NOT inherently make the access token single-use; pre-v0.9.x
|
|
314
314
|
// c_nonce rotation alone defended against proof replay, but a stolen
|
|
315
315
|
// access token combined with a fresh proof could re-mint
|
|
@@ -575,7 +575,7 @@ function create(opts) {
|
|
|
575
575
|
var newCNonce = generateToken(16); // 128-bit c_nonce
|
|
576
576
|
await cNonceStore.set(iopts.accessToken, newCNonce);
|
|
577
577
|
|
|
578
|
-
//
|
|
578
|
+
// When single-use is on (default), DELETE the access token
|
|
579
579
|
// after successful credential mint. A stolen access token paired
|
|
580
580
|
// with a fresh proof would otherwise re-mint credentials; the
|
|
581
581
|
// c_nonce rotation alone defends against proof replay but not
|
package/lib/auth/oid4vp.js
CHANGED
|
@@ -452,7 +452,7 @@ function create(opts) {
|
|
|
452
452
|
continue;
|
|
453
453
|
}
|
|
454
454
|
try {
|
|
455
|
-
// Per-presentation vct enforcement
|
|
455
|
+
// Per-presentation vct enforcement: when
|
|
456
456
|
// DCQL's `vct_values` has 1 entry, `expectedVct` pins it.
|
|
457
457
|
// With 2+ entries the verifier's expectedVct opt can't hold
|
|
458
458
|
// a list, so we verify-without-expected and then validate
|
|
@@ -169,7 +169,7 @@ function verifyEntityStatement(jwt, jwks, vopts) {
|
|
|
169
169
|
"verifyEntityStatement: no JWKS key matches kid \"" + parsed.header.kid + "\"");
|
|
170
170
|
}
|
|
171
171
|
} else {
|
|
172
|
-
//
|
|
172
|
+
// Refuse kid-less entity statements unless the operator
|
|
173
173
|
// explicitly opts in. JWKS rotation creates a window where the
|
|
174
174
|
// rotated-out key is still cached but the rotated-in key is already
|
|
175
175
|
// published; a kid-less statement during that window gets the
|
|
@@ -218,7 +218,7 @@ function verifyEntityStatement(jwt, jwks, vopts) {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
|
|
221
|
-
//
|
|
221
|
+
// Operator-tunable clock skew (sibling primitives accept
|
|
222
222
|
// tunable). Default matches the prior fixed 60s.
|
|
223
223
|
var skew = (typeof vopts.maxClockSkewSec === "number" && isFinite(vopts.maxClockSkewSec) && vopts.maxClockSkewSec >= 0)
|
|
224
224
|
? vopts.maxClockSkewSec
|
|
@@ -436,7 +436,7 @@ async function buildTrustChain(opts) {
|
|
|
436
436
|
var chain = [];
|
|
437
437
|
var current = opts.leafEntityId;
|
|
438
438
|
var depth = 0;
|
|
439
|
-
//
|
|
439
|
+
// Visited-set cycle guard. The maxDepth cap alone caps the
|
|
440
440
|
// loop count but doesn't distinguish "long chain" from "cyclic
|
|
441
441
|
// chain"; a hostile authority that lists itself in authority_hints
|
|
442
442
|
// walks the verifier until depth runs out and then surfaces as
|
|
@@ -486,7 +486,7 @@ async function buildTrustChain(opts) {
|
|
|
486
486
|
// operators with multiple federations usually have one anchor
|
|
487
487
|
// active; we walk in order and pick the first success.
|
|
488
488
|
// Track every per-authority failure reason and surface them on
|
|
489
|
-
// `no-ascent` rather than masking
|
|
489
|
+
// `no-ascent` rather than masking — silently
|
|
490
490
|
// swallowing `catch (_e) {}` lets a hostile intermediate that
|
|
491
491
|
// serves a malformed-then-valid pair shape-walk the verifier.
|
|
492
492
|
// We continue past 404 / fetch errors but refuse on
|
|
@@ -513,7 +513,7 @@ async function buildTrustChain(opts) {
|
|
|
513
513
|
chain[chain.length - 1].claims.jwks = parsedSub.claims.jwks || chain[chain.length - 1].claims.jwks;
|
|
514
514
|
chain[chain.length - 1].subordinateJwt = subordinateJwt;
|
|
515
515
|
chain[chain.length - 1].subordinate = parsedSub.claims;
|
|
516
|
-
//
|
|
516
|
+
// Refuse revisit. A trust anchor terminates the loop
|
|
517
517
|
// before re-entry, so a revisit here ALWAYS means a cyclic
|
|
518
518
|
// authority_hints graph.
|
|
519
519
|
if (visited[authority]) {
|
package/lib/auth/passkey.js
CHANGED
|
@@ -77,7 +77,7 @@ function _requireString(v, name) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// WebAuthn extensions allowlist. Pre-v0.9.x `opts.extensions`
|
|
81
81
|
// was forwarded verbatim to the vendor, letting an operator (or a
|
|
82
82
|
// caller threading user-input through opts) ship arbitrary extension
|
|
83
83
|
// keys to the authenticator. Restrict to the framework-supported
|
|
@@ -234,7 +234,7 @@ async function verifyRegistration(opts) {
|
|
|
234
234
|
// <input autocomplete="webauthn">.
|
|
235
235
|
// Null-prototype map so `opts.mediation === "__proto__"` /
|
|
236
236
|
// `"constructor"` can't truthy-match an inherited property and slip
|
|
237
|
-
// past the allowlist
|
|
237
|
+
// past the allowlist.
|
|
238
238
|
var ALLOWED_MEDIATION = Object.assign(Object.create(null),
|
|
239
239
|
{ silent: 1, optional: 1, required: 1, conditional: 1 });
|
|
240
240
|
|
|
@@ -314,7 +314,7 @@ function _b64urlExtInput(value, name, maxBytes) {
|
|
|
314
314
|
// browser turns it into an ArrayBuffer before passing to the
|
|
315
315
|
// authenticator).
|
|
316
316
|
//
|
|
317
|
-
//
|
|
317
|
+
// When `maxBytes` is set, refuse decoded inputs longer than
|
|
318
318
|
// the cap. Per CTAP2.1 §6.5 PRF salts are 32 bytes; pre-v0.9.x the
|
|
319
319
|
// framework accepted arbitrary length, which is undefined behavior on
|
|
320
320
|
// authenticators that may truncate / reject / behave inconsistently.
|
|
@@ -364,7 +364,7 @@ function _prfExt(args) {
|
|
|
364
364
|
throw new AuthError("auth-passkey/missing-prf-first",
|
|
365
365
|
"extensions.prf eval.first is required");
|
|
366
366
|
}
|
|
367
|
-
//
|
|
367
|
+
// CTAP2.1 §6.5 caps PRF salts at 32 bytes.
|
|
368
368
|
var out = { prf: { eval: { first: _b64urlExtInput(args.eval.first, "eval.first", MAX_EXT_INPUT_BYTES) } } };
|
|
369
369
|
if (args.eval.second !== undefined && args.eval.second !== null) {
|
|
370
370
|
out.prf.eval.second = _b64urlExtInput(args.eval.second, "eval.second", MAX_EXT_INPUT_BYTES);
|
|
@@ -461,7 +461,7 @@ async function verifyAuthentication(opts) {
|
|
|
461
461
|
throw new AuthError("auth-passkey/missing-credential",
|
|
462
462
|
"opts.credential { id, publicKey, counter? } is required");
|
|
463
463
|
}
|
|
464
|
-
// Counter regression bypass fix
|
|
464
|
+
// Counter regression bypass fix — pre-v0.9.2
|
|
465
465
|
// shape `opts.credential.counter || 0` silently zeroed an
|
|
466
466
|
// undefined / null / NaN counter, defeating CTAP 2.1 clone-
|
|
467
467
|
// detection on credentials whose stored counter is > 0. An
|
|
@@ -525,7 +525,7 @@ async function verifyAuthentication(opts) {
|
|
|
525
525
|
* @signature b.auth.passkey.compareBackupState(prev, current)
|
|
526
526
|
* @since 0.9.57
|
|
527
527
|
*
|
|
528
|
-
*
|
|
528
|
+
* WebAuthn L3 §6.1.3. Inspect the credential's persisted BE
|
|
529
529
|
* (backupEligible) + BS (backupState) flags against the values
|
|
530
530
|
* surfaced on a fresh assertion. Returns a normalized verdict the
|
|
531
531
|
* operator routes into audit / step-up decisions:
|
package/lib/auth/saml.js
CHANGED
|
@@ -717,7 +717,7 @@ function create(opts) {
|
|
|
717
717
|
// Constant-time compare against the AuthnRequest ID the
|
|
718
718
|
// operator stored — protects against timing-based InResponseTo
|
|
719
719
|
// probing. timingSafeEqual returns false for missing /
|
|
720
|
-
// length-mismatch without leaking.
|
|
720
|
+
// length-mismatch without leaking.
|
|
721
721
|
if (inResponseTo === null || inResponseTo === undefined ||
|
|
722
722
|
!timingSafeEqual(inResponseTo, vopts.expectedInResponseTo)) {
|
|
723
723
|
throw new AuthError("auth-saml/bad-in-response-to",
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -492,7 +492,7 @@ async function verify(presentation, opts) {
|
|
|
492
492
|
jwtExternal._assertAlgKtyMatch(alg, issuerKey);
|
|
493
493
|
}
|
|
494
494
|
var jwtParsed = _verifyJwt(jwt, issuerKey, alg);
|
|
495
|
-
//
|
|
495
|
+
// Post-verify header compare. Pre-verify we parsed the
|
|
496
496
|
// header bytes to look up the key; _verifyJwt parses again from the
|
|
497
497
|
// cryptographically-verified signing input. Both decodes MUST yield
|
|
498
498
|
// the same JSON; a mismatch indicates a JWS-canonicalization or
|
|
@@ -538,7 +538,6 @@ async function verify(presentation, opts) {
|
|
|
538
538
|
// selective-disclosure-jwt §4.1.1). Earlier the framework defaulted
|
|
539
539
|
// to its own DEFAULT_HASH_ALG (`sha3-512`) which broke verification
|
|
540
540
|
// against spec-conformant issuers when `_sd_alg` was omitted.
|
|
541
|
-
// (Audit 2026-05-11.)
|
|
542
541
|
var hashAlg = jwtParsed.payload._sd_alg || "sha-256";
|
|
543
542
|
if (!SUPPORTED_HASH_ALGS[hashAlg]) {
|
|
544
543
|
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
@@ -566,7 +565,7 @@ async function verify(presentation, opts) {
|
|
|
566
565
|
// Disclosure-replay defense — a holder presenting the same _sd
|
|
567
566
|
// digest twice (with the same or different values) is malformed
|
|
568
567
|
// per spec and is the shape of a partial-disclosure smuggling
|
|
569
|
-
// attack. Refuse on duplicate digest.
|
|
568
|
+
// attack. Refuse on duplicate digest.
|
|
570
569
|
if (seenDigests[digest]) {
|
|
571
570
|
throw new AuthError("auth-sd-jwt-vc/disclosure-replay",
|
|
572
571
|
"verify: disclosure digest \"" + digest.slice(0, 12) +
|
|
@@ -622,9 +621,7 @@ async function verify(presentation, opts) {
|
|
|
622
621
|
"verify: KB-JWT nonce mismatch (replay defense)");
|
|
623
622
|
}
|
|
624
623
|
// Validate KB-JWT sd_hash matches the presentation, using the
|
|
625
|
-
// credential's declared `_sd_alg
|
|
626
|
-
// hardcoded sha256 regardless of issuer's choice, breaking
|
|
627
|
-
// verification when issuer used sha3-512).
|
|
624
|
+
// credential's declared `_sd_alg`.
|
|
628
625
|
var kbHashInput = jwt + "~";
|
|
629
626
|
if (disclosureParts.length > 0) kbHashInput += disclosureParts.join("~") + "~";
|
|
630
627
|
var kbNodeHash = SUPPORTED_HASH_ALGS[hashAlg];
|