@blamejs/core 0.15.62 → 0.15.64
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 +4 -0
- package/lib/auth/jar.js +2 -1
- package/lib/auth/jwt-external.js +14 -3
- package/lib/auth/oauth.js +14 -0
- package/lib/auth/oid4vci.js +56 -17
- package/lib/auth/sd-jwt-vc.js +7 -0
- package/lib/http-message-signature.js +10 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.64 (2026-06-29) — **Token and HTTP-message-signature verifiers now reject a non-finite clock-skew or tolerance value instead of letting it disable the expiry, not-before, and future-dating checks.** Several verifiers read an operator-supplied clock-skew or tolerance value with a bare type check that accepted Infinity and NaN. Because the resulting comparison (for example exp + skew < now, or age > tolerance) is always false when the skew or tolerance is Infinity, a misconfigured non-finite value silently disabled the time-window check — so an expired token, a not-yet-valid token, a future-dated signature, or a replayed (too-old) signature would verify. The affected paths are the shared external-JWT verifier b.auth.jwt.verifyExternal (which the JAR / signed-request-object path also uses), the SD-JWT-VC credential verifier, the OAuth ID-token and client-attestation verifiers, and the HTTP message signature verifier's freshness and clock-skew windows. Each now requires a present skew or tolerance to be a non-negative finite number: the throw-based JWT, SD-JWT-VC, and OAuth verifiers reject a malformed value, and the verdict-returning HTTP-message-signature verifier falls back to its safe default rather than disabling the window. A build check now fails if any verifier reads one of these options without a finiteness guard. **Added:** *verifyExternal and jar.parse accept an `now` clock override* — b.auth.jwt.verifyExternal and b.auth.jar.parse now accept an optional `now` (a finite epoch-milliseconds value) to evaluate a token's exp / nbf / iat as of a specific instant instead of the wall clock — consistent with b.auth.sdJwtVc.verify. A negative clockSkewMs is no longer accepted as a way to shift the evaluation time; use `now`. **Security:** *Non-finite clock-skew / tolerance no longer disables a verifier's time-window check* — b.auth.jwt.verifyExternal, b.auth.sdJwtVc.verify, b.auth.oauth (ID-token and client-attestation verification), and the HTTP message signature verifier read their clock-skew, max-clock-skew, max-PoP-age, and tolerance options with a bare `typeof === "number"` check that let Infinity and NaN through. With such a value the expiry / not-before / future-dating comparison is always false, so the check was silently skipped and an expired or not-yet-valid token — or a future-dated or replayed signature — would verify. The JWT, SD-JWT-VC, and OAuth verifiers now reject a non-finite or negative skew (the option is operator configuration, not attacker-controlled), and the HTTP message signature verifier falls back to its default tolerance and skew. The CWT, DPoP, SAML, and OpenID Federation verifiers already enforced this and are unchanged. **Detectors:** *Verifier clock-skew / tolerance options must be finite-guarded* — A build check fails if a verifier reads a clock-skew or tolerance option with a bare `typeof === "number"` without a finiteness guard (a non-negative-finite check, an inline isFinite fallback, or a create-time non-negative-finite schema), preventing a future verifier from reintroducing the disable-the-window class.
|
|
12
|
+
|
|
13
|
+
- v0.15.63 (2026-06-29) — **OID4VCI now enforces single-use of a pre-authorized code and of a single-use access token under concurrency, so two simultaneous requests can no longer mint two credentials from one.** On the OID4VCI credential issuer, two single-use values were consumed by a delete whose result was ignored, so concurrent requests could each act on the same value. exchangePreAuthorizedCode read the pre-authorized code's entry, validated the transaction code, then deleted the code and minted an access token without checking that its own call had removed the entry — two simultaneous /token requests with the same code each saw the entry, each deleted it, and each minted a distinct access token, issuing two access tokens (and ultimately two credentials) from a code OID4VCI requires to be single-use. issueCredential had the same shape: with single-use access tokens (the default), it minted the credential first and deleted the access token afterward as best-effort cleanup, so two concurrent requests bearing the same token both read it and the same not-yet-rotated c_nonce, both proofs verified, and both minted a credential. Both paths now claim the value with an atomic delete and proceed only when that delete succeeded; the losing request is refused. The transaction-code and proof checks still run first, so a bad transaction code or proof does not consume the value (a wallet can retry). **Security:** *OID4VCI pre-authorized code and access token are single-use under concurrency* — exchangePreAuthorizedCode and issueCredential consumed their single-use value (the pre-authorized code, and the single-use access token) with a delete whose return was discarded, and in issueCredential's case after the credential was already minted. Two concurrent requests carrying the same value therefore each succeeded — minting two access tokens from one pre-authorized code, or two credentials from one single-use access token — defeating the single-use guarantee OID4VCI §3.5 requires (an authorization intended for one credential could yield two). Both paths now delete the value atomically and issue only if that delete removed it, refusing the request that lost the race; the transaction-code and proof verifications run before the claim, so an invalid attempt does not burn the value. If the operator's credential issuer throws after the access token has been claimed (a transient signer outage), the token is restored so the wallet can retry rather than being permanently consumed without a credential.
|
|
14
|
+
|
|
11
15
|
- v0.15.62 (2026-06-29) — **ARC evaluation now reads each hop's instance with the same strict parser the signature checks use, so a crafted ARC-Authentication-Results header can no longer forge the upstream auth-results surfaced to downstream policy.** b.mail.arc.evaluate returns finalAr — the most recent hop's ARC-Authentication-Results, the receiver's view of the upstream authentication results — which operators may key downstream decisions on. The instance tag (i=) on each ARC header was parsed by three different regexes: the indexing pass that drives the AMS/AS signature checks required i= with no surrounding space and at most three digits, while the finalAr extraction and the AMS header-retention test accepted a looser form (a space around =, unbounded digits). When a sealer signs without covering ARC-Authentication-Results in its AMS h= (permitted by RFC 8617 and supported by the verifier), an attacker holding no key could append a second ARC-Authentication-Results written so the strict pass ignored it while the loose pass consumed it — forging finalAr on a chain that still verified as pass. All ARC instance reads now route through one strict parser, so the evaluation surfaces the same hop the signatures validated. The release also repairs the b.mail.arc.sign excludeAarFromAms option (it was read but rejected by option validation, so the documented opt-out was unreachable) and routes the ARC-Seal signature's b= stripping through the shared tag-aware helper. **Fixed:** *ARC finalAr is read from the strictly-indexed hop, not a looser rescan* — b.mail.arc.evaluate extracted finalAr (and validated the per-hop AMS header retention) with a regex that accepted ARC instance tags the signature-indexing pass rejected — a space around i= or more than three digits. A sealer that omits ARC-Authentication-Results from its AMS h= leaves the AAR outside signature coverage; an attacker could then inject a second ARC-Authentication-Results whose instance the strict crypto pass skipped but the finalAr pass accepted, presenting attacker-chosen upstream auth-results on a chain that still reported pass. Every ARC instance read now goes through a single strict parser, so finalAr is always the AAR the chain's signatures actually covered. · *b.mail.arc.sign accepts excludeAarFromAms again* — The excludeAarFromAms option was read when building the AMS h= list but was missing from the function's option allow-list, so passing it raised an unknown-option error — the documented opt-out could not be used. It is now accepted. · *ARC-Seal b= stripping uses the shared tag-aware helper* — The ARC-Seal verification stripped the signature's b= value with a regex that could mis-zero a value containing b= inside another tag; it now uses the same tag-aware stripper as DKIM, so canonicalization matches the signer in every case. **Detectors:** *ARC instance parsing must use the shared strict reader* — A check fails the build if any ARC instance (i=) parsing regex is added outside the single shared reader, preventing a future divergence between the signature-indexing pass and the finalAr / header-retention passes.
|
|
12
16
|
|
|
13
17
|
- v0.15.61 (2026-06-29) — **The local and Redis job queues fence completion, failure, and lease extension on the lease the caller actually holds, so a worker finishing after its lease expired can no longer disturb a job another worker has since taken over.** On the local and Redis queue backends, complete(), fail(), and extendLease() identified a job only by its id. When a worker's lease expired, the sweep returned the job to the ready set and another worker leased and began running it; if the original worker then finished late, its complete() could mark the new worker's in-progress job done (and double-fire a cron repeat or re-release flow children), and its fail() could re-queue or dead-letter a job the new worker was still executing. Each lease now carries the job's attempts value (incremented once per lease), and complete(), fail(), and extendLease() act only when that value still matches — so a call from a worker that no longer holds the lease changes nothing. The generic consumer threads this automatically; the SQS backend already bound these actions to the message's receipt handle and is unchanged. **Fixed:** *Local and Redis queues bind complete/fail/extendLease to the held lease* — A long-running handler whose lease expired and was swept could have its job re-leased to a second worker; when the first worker finished, complete() marked the second worker's in-progress job done — double-firing a cron-recurring job's next enqueue and re-releasing its flow dependents — while fail() re-queued or dead-lettered the job the second worker was still running (re-executing or discarding in-flight work). The backends now fence each of these calls on the leased attempts value, which is bumped once per lease; only the worker that holds the current lease can complete, fail, or extend it. A stale call returns without mutating the queue. This brings the local and Redis backends to parity with the SQS backend, which already bound these actions to the message receipt handle.
|
package/lib/auth/jar.js
CHANGED
|
@@ -139,7 +139,7 @@ async function parse(jar, opts) {
|
|
|
139
139
|
}
|
|
140
140
|
validateOpts.requireObject(opts, "jar.parse", AuthJarError);
|
|
141
141
|
validateOpts(opts, [
|
|
142
|
-
"clientId", "audience", "algorithms", "jwks", "jwksUri", "keyResolver", "clockSkewMs",
|
|
142
|
+
"clientId", "audience", "algorithms", "jwks", "jwksUri", "keyResolver", "clockSkewMs", "now",
|
|
143
143
|
], "jar.parse");
|
|
144
144
|
validateOpts.requireNonEmptyString(opts.clientId, "jar.parse: clientId", AuthJarError, "auth-jar/bad-client-id");
|
|
145
145
|
validateOpts.requireNonEmptyString(opts.audience, "jar.parse: audience", AuthJarError, "auth-jar/bad-audience");
|
|
@@ -156,6 +156,7 @@ async function parse(jar, opts) {
|
|
|
156
156
|
issuer: opts.clientId,
|
|
157
157
|
audience: opts.audience,
|
|
158
158
|
clockSkewMs: opts.clockSkewMs,
|
|
159
|
+
now: opts.now,
|
|
159
160
|
});
|
|
160
161
|
// RFC 9101 §10.8 — the request object MUST be explicitly typed so a JWT
|
|
161
162
|
// minted for another purpose (id_token / access-token / logout-token)
|
package/lib/auth/jwt-external.js
CHANGED
|
@@ -56,6 +56,7 @@ var lazyRequire = require("../lazy-require");
|
|
|
56
56
|
var validateOpts = require("../validate-opts");
|
|
57
57
|
var C = require("../constants");
|
|
58
58
|
var bCrypto = require("../crypto");
|
|
59
|
+
var numericBounds = require("../numeric-bounds");
|
|
59
60
|
var { AuthError } = require("../framework-error");
|
|
60
61
|
|
|
61
62
|
var httpClient = lazyRequire(function () { return require("../http-client"); });
|
|
@@ -421,7 +422,7 @@ async function verifyExternal(token, opts) {
|
|
|
421
422
|
opts = opts || {};
|
|
422
423
|
validateOpts(opts, [
|
|
423
424
|
"algorithms", "jwks", "jwksUri", "jwksCacheMs", "keyResolver",
|
|
424
|
-
"audience", "issuer", "subject", "clockSkewMs",
|
|
425
|
+
"audience", "issuer", "subject", "clockSkewMs", "now",
|
|
425
426
|
// v0.9.4 — opt-out for the kid-less-token JWKS-of-one refusal
|
|
426
427
|
// (default refuses; non-conforming IdPs that emit kid-less tokens
|
|
427
428
|
// set this true).
|
|
@@ -564,9 +565,19 @@ async function verifyExternal(token, opts) {
|
|
|
564
565
|
"signature verification failed");
|
|
565
566
|
}
|
|
566
567
|
|
|
567
|
-
// Claim validation.
|
|
568
|
+
// Claim validation. A present clockSkewMs must be a non-negative finite
|
|
569
|
+
// integer: a bare `typeof === "number"` lets Infinity / NaN through, and
|
|
570
|
+
// `exp + Infinity < now` (or NaN) is always false — silently disabling the
|
|
571
|
+
// expiry/nbf/iat gates so an expired or not-yet-valid token verifies. Reject
|
|
572
|
+
// a malformed skew loudly (it is operator config, not token-controlled).
|
|
573
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.clockSkewMs,
|
|
574
|
+
"verifyExternal: opts.clockSkewMs", AuthError, "auth-jwt-external/bad-clock-skew");
|
|
568
575
|
var clockSkewMs = typeof opts.clockSkewMs === "number" ? opts.clockSkewMs : DEFAULT_CLOCK_SKEW_MS;
|
|
569
|
-
|
|
576
|
+
// opts.now (finite ms) overrides the wall clock for the time checks —
|
|
577
|
+
// consistent with sdJwtVc.verify; lets a caller evaluate the token as of a
|
|
578
|
+
// specific instant without abusing a (now-rejected) negative skew.
|
|
579
|
+
var nowMs = (typeof opts.now === "number" && isFinite(opts.now)) ? opts.now : Date.now();
|
|
580
|
+
var nowSec = Math.floor(nowMs / C.TIME.seconds(1));
|
|
570
581
|
var skewSec = Math.floor(clockSkewMs / C.TIME.seconds(1));
|
|
571
582
|
|
|
572
583
|
if (typeof payload.exp !== "number") {
|
package/lib/auth/oauth.js
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
var nodeCrypto = require("node:crypto");
|
|
108
108
|
var cache = require("../cache");
|
|
109
109
|
var C = require("../constants");
|
|
110
|
+
var numericBounds = require("../numeric-bounds");
|
|
110
111
|
var safeAsync = require("../safe-async");
|
|
111
112
|
var bCrypto = require("../crypto");
|
|
112
113
|
var { generateBytes, timingSafeEqual: cryptoTimingSafeEqual } = bCrypto;
|
|
@@ -886,6 +887,11 @@ async function verifyClientAttestation(attestationJwt, popJwt, vopts) {
|
|
|
886
887
|
"client-attestation: missing 'cnf.jwk' confirmation key (RFC 7800)");
|
|
887
888
|
}
|
|
888
889
|
var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
|
|
890
|
+
// A present skew/maxAge must be a non-negative finite integer; a bare typeof
|
|
891
|
+
// check lets Infinity/NaN through, and `exp + Infinity < now` (or NaN) is
|
|
892
|
+
// always false — disabling the attestation/PoP expiry gates.
|
|
893
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(vopts.clockSkewSec,
|
|
894
|
+
"verifyClientAttestation: opts.clockSkewSec", OAuthError, "auth-oauth/bad-clock-skew");
|
|
889
895
|
var skewSec = typeof vopts.clockSkewSec === "number" ? vopts.clockSkewSec : (C.TIME.minutes(1) / C.TIME.seconds(1));
|
|
890
896
|
if (typeof ap.exp !== "number" || ap.exp + skewSec < nowSec) {
|
|
891
897
|
throw new OAuthError("auth-oauth/attestation-expired",
|
|
@@ -918,6 +924,8 @@ async function verifyClientAttestation(attestationJwt, popJwt, vopts) {
|
|
|
918
924
|
if (typeof pp.iat !== "number") {
|
|
919
925
|
throw new OAuthError("auth-oauth/attestation-pop-no-iat", "client-attestation-pop: missing 'iat'");
|
|
920
926
|
}
|
|
927
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(vopts.maxPopAgeSec,
|
|
928
|
+
"verifyClientAttestation: opts.maxPopAgeSec", OAuthError, "auth-oauth/bad-pop-max-age");
|
|
921
929
|
var maxAge = typeof vopts.maxPopAgeSec === "number" ? vopts.maxPopAgeSec : DEFAULT_POP_MAX_AGE_SEC;
|
|
922
930
|
if (pp.iat - skewSec > nowSec) {
|
|
923
931
|
throw new OAuthError("auth-oauth/attestation-pop-iat-future", "client-attestation-pop: iat in the future");
|
|
@@ -992,6 +1000,12 @@ function create(opts) {
|
|
|
992
1000
|
"requires PKCE for all clients. Remove the opt or upgrade the IdP.");
|
|
993
1001
|
}
|
|
994
1002
|
var pkce = true;
|
|
1003
|
+
// A present clockSkewMs must be a non-negative finite integer; Infinity/NaN
|
|
1004
|
+
// would disable verifyIdToken's exp gate (`exp + Infinity < now` is always
|
|
1005
|
+
// false → an expired ID token verifies). Reject a malformed skew at config
|
|
1006
|
+
// time so the operator catches it.
|
|
1007
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.clockSkewMs,
|
|
1008
|
+
"oauth.create: opts.clockSkewMs", OAuthError, "auth-oauth/bad-clock-skew");
|
|
995
1009
|
var clockSkewMs = typeof opts.clockSkewMs === "number" ? opts.clockSkewMs : DEFAULT_CLOCK_SKEW_MS;
|
|
996
1010
|
var discoveryCacheMs = typeof opts.discoveryCacheMs === "number"
|
|
997
1011
|
? opts.discoveryCacheMs : DEFAULT_DISCOVERY_CACHE_MS;
|
package/lib/auth/oid4vci.js
CHANGED
|
@@ -665,7 +665,21 @@ function create(opts) {
|
|
|
665
665
|
"exchangePreAuthorizedCode: tx_code does not match");
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
|
-
|
|
668
|
+
// Single-use enforcement under concurrency: claim the code by deleting it
|
|
669
|
+
// and gate issuance on having WON that delete. codeStore.del returns true
|
|
670
|
+
// only for the caller that removed the entry (a single DELETE ... WHERE /
|
|
671
|
+
// redis DEL is atomic — exactly one of two racing redemptions gets a true
|
|
672
|
+
// return, the other false). Without gating on the return, two concurrent
|
|
673
|
+
// /token requests with the same pre-authorized_code (and matching tx_code)
|
|
674
|
+
// both read the entry, both delete, and both mint an access token — issuing
|
|
675
|
+
// two credentials from a code RFC OID4VCI §3.5 mandates be single-use. The
|
|
676
|
+
// tx_code check above runs first and throws without consuming, so a wrong
|
|
677
|
+
// tx_code does not burn the code (a retrying wallet is unaffected).
|
|
678
|
+
var claimed = await codeStore.del(eopts.preAuthCode);
|
|
679
|
+
if (!claimed) {
|
|
680
|
+
throw new AuthError("auth-oid4vci/invalid-pre-auth-code",
|
|
681
|
+
"exchangePreAuthorizedCode: pre-authorized_code already redeemed");
|
|
682
|
+
}
|
|
669
683
|
var accessToken = generateToken(32); // 256-bit access token
|
|
670
684
|
var cNonce = generateToken(16); // 128-bit c_nonce
|
|
671
685
|
var record = {
|
|
@@ -760,30 +774,55 @@ function create(opts) {
|
|
|
760
774
|
throw new AuthError("auth-oid4vci/no-claims",
|
|
761
775
|
"issueCredential: claims required (operator looks up the subject's data and supplies them)");
|
|
762
776
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
777
|
+
// Single-use access token: CLAIM it (atomic, gated delete) BEFORE minting,
|
|
778
|
+
// so two concurrent issueCredential calls bearing the same token can't both
|
|
779
|
+
// produce a credential. atStore.del returns true only for the caller that
|
|
780
|
+
// removed the entry; the loser is refused. The proof and claims checks above
|
|
781
|
+
// run first, so a bad proof does not burn the token (a wallet can retry).
|
|
782
|
+
// Done before the mint because deleting it only as post-mint cleanup let two
|
|
783
|
+
// racing requests both read the token and both mint (re-minting from a
|
|
784
|
+
// single-use token). c_nonce rotation alone does not stop this — both
|
|
785
|
+
// requests read the same un-rotated c_nonce.
|
|
786
|
+
if (accessTokenSingleUse) {
|
|
787
|
+
var atClaimed = await atStore.del(iopts.accessToken);
|
|
788
|
+
if (!atClaimed) {
|
|
789
|
+
throw new AuthError("auth-oid4vci/access-token-consumed",
|
|
790
|
+
"issueCredential: access token already used (single-use)");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
var sdJwtToken;
|
|
794
|
+
try {
|
|
795
|
+
sdJwtToken = await opts.sdJwtIssuer.issue({
|
|
796
|
+
vct: spec.vct,
|
|
797
|
+
subject: record.subject,
|
|
798
|
+
claims: iopts.claims,
|
|
799
|
+
selectivelyDisclosed: iopts.selectivelyDisclosed || Object.keys(iopts.claims),
|
|
800
|
+
holderKey: verified.jwk,
|
|
801
|
+
ttlMs: iopts.ttlMs,
|
|
802
|
+
});
|
|
803
|
+
} catch (e) {
|
|
804
|
+
// Issuance failed AFTER the single-use access token was claimed (the
|
|
805
|
+
// operator's issuer threw — a transient signer/KMS outage or a validation
|
|
806
|
+
// error). Restore the token so the wallet can retry: the claim exists
|
|
807
|
+
// only to stop a concurrent double-mint, not to burn the token when no
|
|
808
|
+
// credential was returned. The next attempt re-claims atomically, so this
|
|
809
|
+
// opens no double-mint window.
|
|
810
|
+
if (accessTokenSingleUse) {
|
|
811
|
+
try { await atStore.set(iopts.accessToken, record); } catch (_e) { /* best-effort restore */ }
|
|
812
|
+
}
|
|
813
|
+
throw e;
|
|
814
|
+
}
|
|
771
815
|
|
|
772
816
|
// Rotate c_nonce so a replayed proof-JWT for a follow-up
|
|
773
817
|
// batch_credential request is rejected.
|
|
774
818
|
var newCNonce = generateToken(16); // 128-bit c_nonce
|
|
775
819
|
await cNonceStore.set(iopts.accessToken, newCNonce);
|
|
776
820
|
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
// c_nonce rotation alone defends against proof replay but not
|
|
781
|
-
// against an attacker who exfiltrated the access token. The
|
|
782
|
-
// accompanying c_nonce entry expires with its TTL; deleting it
|
|
783
|
-
// explicitly tightens cleanup.
|
|
821
|
+
// The single-use access token was already claimed (atomically) before the
|
|
822
|
+
// mint above. Here we only clean up its now-orphaned c_nonce entry (it would
|
|
823
|
+
// otherwise expire with its TTL). Best-effort.
|
|
784
824
|
if (accessTokenSingleUse) {
|
|
785
825
|
try {
|
|
786
|
-
await atStore.del(iopts.accessToken);
|
|
787
826
|
await cNonceStore.del(iopts.accessToken);
|
|
788
827
|
} catch (_e) { /* drop-silent — cleanup is best-effort */ }
|
|
789
828
|
}
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
|
|
63
63
|
var nodeCrypto = require("node:crypto");
|
|
64
64
|
var bCrypto = require("../crypto");
|
|
65
|
+
var numericBounds = require("../numeric-bounds");
|
|
65
66
|
var safeBuffer = require("../safe-buffer");
|
|
66
67
|
var safeJson = require("../safe-json");
|
|
67
68
|
var validateOpts = require("../validate-opts");
|
|
@@ -523,6 +524,12 @@ async function verify(presentation, opts) {
|
|
|
523
524
|
// 2. Validate iss / iat / exp / vct
|
|
524
525
|
var nowSec = (typeof opts.now === "number" && isFinite(opts.now))
|
|
525
526
|
? Math.floor(opts.now / 1000) : Math.floor(Date.now() / 1000); // ms→s conversion factor
|
|
527
|
+
// A present maxClockSkewSec must be a non-negative finite integer; a bare
|
|
528
|
+
// typeof check lets Infinity/NaN through, and `exp < nowSec - Infinity` (or
|
|
529
|
+
// NaN) is always false — silently accepting an expired credential. Reject a
|
|
530
|
+
// malformed skew (operator config).
|
|
531
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.maxClockSkewSec,
|
|
532
|
+
"verify: opts.maxClockSkewSec", AuthError, "auth-sd-jwt-vc/bad-clock-skew");
|
|
526
533
|
var skew = (typeof opts.maxClockSkewSec === "number") ? opts.maxClockSkewSec : 60; // allow:raw-time-literal — default 60s clock-skew tolerance
|
|
527
534
|
if (typeof jwtParsed.payload.iat === "number" && jwtParsed.payload.iat > nowSec + skew) {
|
|
528
535
|
throw new AuthError("auth-sd-jwt-vc/iat-future",
|
|
@@ -561,8 +561,16 @@ function verify(msg, opts) {
|
|
|
561
561
|
// itself still has to verify — only the floor is waived).
|
|
562
562
|
validateOpts.optionalNonEmptyStringArray(opts.requiredComponents,
|
|
563
563
|
"httpSig.verify: requiredComponents", HttpSigError, "BAD_OPT");
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
// A present toleranceMs / clockSkewMs must be a non-negative finite number;
|
|
565
|
+
// a bare typeof check lets Infinity/NaN through, and `ageMs > Infinity` (the
|
|
566
|
+
// expiry gate) or `-ageMs > Infinity` (the future-dating gate) is always
|
|
567
|
+
// false — silently accepting a stale (replayed) or future-dated signature.
|
|
568
|
+
// verify() returns verdicts rather than throwing, so a malformed value falls
|
|
569
|
+
// back to the safe default instead of disabling the window.
|
|
570
|
+
var toleranceMs = (typeof opts.toleranceMs === "number" && isFinite(opts.toleranceMs) && opts.toleranceMs >= 0)
|
|
571
|
+
? opts.toleranceMs : DEFAULT_TOLERANCE_MS;
|
|
572
|
+
var clockSkewMs = (typeof opts.clockSkewMs === "number" && isFinite(opts.clockSkewMs) && opts.clockSkewMs >= 0)
|
|
573
|
+
? opts.clockSkewMs : DEFAULT_CLOCK_SKEW_MS;
|
|
566
574
|
var nowMs = opts.now ? opts.now() : Date.now();
|
|
567
575
|
|
|
568
576
|
var sigInput = _resolveHeader(m.headers, "signature-input");
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f9782178-0584-49aa-8fd6-3029b774391a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-29T22:06:28.985Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.15.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.64",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.64",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.15.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.64",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.15.
|
|
57
|
+
"ref": "@blamejs/core@0.15.64",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|