@blamejs/core 0.14.21 → 0.14.24
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 +6 -0
- package/README.md +2 -2
- package/index.js +5 -1
- package/lib/auth/jar.js +190 -28
- package/lib/auth/jwt-external.js +213 -0
- package/lib/auth/oauth.js +115 -101
- package/lib/compliance.js +37 -0
- package/lib/crypto-field.js +111 -5
- package/lib/db-query.js +123 -0
- package/lib/external-db-migrate.js +19 -7
- package/lib/external-db.js +508 -20
- package/lib/framework-error.js +6 -0
- package/lib/http-client.js +3 -4
- package/lib/lro.js +3 -4
- package/lib/mail-auth.js +236 -0
- package/lib/mail-dkim.js +1 -0
- package/lib/mail-server-mx.js +276 -7
- package/lib/mail.js +8 -4
- package/lib/middleware/deny-response.js +2 -10
- package/lib/middleware/health.js +1 -4
- package/lib/middleware/trace-log-correlation.js +3 -6
- package/lib/validate-opts.js +34 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/auth/jwt-external.js
CHANGED
|
@@ -109,6 +109,14 @@ function _b64urlDecode(s) {
|
|
|
109
109
|
return Buffer.from(padded, "base64");
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// EC named-curve → the one ES* alg whose hash matches it (RFC 7518 §3.4).
|
|
113
|
+
// A P-256 key signs ES256 and only ES256; the curve fixes the hash, so a
|
|
114
|
+
// header alg of ES384 over a P-256 signature is self-inconsistent and a
|
|
115
|
+
// conforming verifier rejects it. Naming the binding here lets the signer
|
|
116
|
+
// derive the right header alg from the key instead of trusting a caller-
|
|
117
|
+
// supplied alg the key can't actually produce.
|
|
118
|
+
var _EC_CURVE_ALG = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
|
|
119
|
+
|
|
112
120
|
function _verifyParamsForAlg(alg) {
|
|
113
121
|
if (alg === "RS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
|
|
114
122
|
if (alg === "RS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
|
|
@@ -124,6 +132,104 @@ function _verifyParamsForAlg(alg) {
|
|
|
124
132
|
"alg '" + alg + "' is not supported by verifyExternal");
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
// _toPrivateKey — import the operator's classical signing key from any of
|
|
136
|
+
// the three shapes node:crypto understands (KeyObject / PEM string|Buffer /
|
|
137
|
+
// private JWK). The PQC framework signer (lib/auth/jwt.js) never travels
|
|
138
|
+
// this path; this is the classical-interop importer only.
|
|
139
|
+
function _toPrivateKey(value, label) {
|
|
140
|
+
if (!value) {
|
|
141
|
+
throw new AuthError("auth-jwt-external/sign-no-key", label + ": privateKey is required");
|
|
142
|
+
}
|
|
143
|
+
if (value instanceof nodeCrypto.KeyObject) return value;
|
|
144
|
+
try {
|
|
145
|
+
if (typeof value === "string" || Buffer.isBuffer(value)) {
|
|
146
|
+
return nodeCrypto.createPrivateKey({ key: value, format: "pem" });
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "object" && value.kty) {
|
|
149
|
+
return nodeCrypto.createPrivateKey({ key: value, format: "jwk" });
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
throw new AuthError("auth-jwt-external/sign-bad-key",
|
|
153
|
+
label + ": private key parse failed: " + ((e && e.message) || String(e)));
|
|
154
|
+
}
|
|
155
|
+
throw new AuthError("auth-jwt-external/sign-bad-key",
|
|
156
|
+
label + ": privateKey must be a PEM string/Buffer, private JWK object, or KeyObject");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// _resolveSignAlg — derive the JWS `alg` for a private key, validating any
|
|
160
|
+
// explicit override against what the key can actually produce. A signer
|
|
161
|
+
// that emitted a fixed `alg` header while signing with an incompatible key
|
|
162
|
+
// (e.g. an `ES256` header over an Ed25519 signature) would mint a token no
|
|
163
|
+
// conforming verifier accepts; deriving the alg from the key — or refusing
|
|
164
|
+
// an incompatible explicit alg BEFORE signing — closes that self-invalid
|
|
165
|
+
// shape. RFC 7518 §3.1 maps each `alg` to the key type it requires.
|
|
166
|
+
function _resolveSignAlg(explicitAlg, privateKey, label) {
|
|
167
|
+
var kty = privateKey.asymmetricKeyType;
|
|
168
|
+
var defaultAlg, compatible;
|
|
169
|
+
if (kty === "ec") {
|
|
170
|
+
var curve = (privateKey.asymmetricKeyDetails && privateKey.asymmetricKeyDetails.namedCurve) || "";
|
|
171
|
+
defaultAlg = _EC_CURVE_ALG[curve];
|
|
172
|
+
if (!defaultAlg) {
|
|
173
|
+
throw new AuthError("auth-jwt-external/sign-key-unsupported",
|
|
174
|
+
label + ": EC curve '" + curve + "' has no JWS alg (use P-256 / P-384 / P-521)");
|
|
175
|
+
}
|
|
176
|
+
compatible = [defaultAlg]; // an EC curve pins exactly one ES alg
|
|
177
|
+
} else if (kty === "rsa") {
|
|
178
|
+
defaultAlg = "RS256";
|
|
179
|
+
compatible = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"];
|
|
180
|
+
} else if (kty === "rsa-pss") {
|
|
181
|
+
defaultAlg = "PS256";
|
|
182
|
+
compatible = ["PS256", "PS384", "PS512"]; // an RSA-PSS key cannot produce an RS* signature
|
|
183
|
+
} else if (kty === "ed25519" || kty === "ed448") {
|
|
184
|
+
defaultAlg = "EdDSA";
|
|
185
|
+
compatible = ["EdDSA"];
|
|
186
|
+
} else {
|
|
187
|
+
throw new AuthError("auth-jwt-external/sign-key-unsupported",
|
|
188
|
+
label + ": key type '" + String(kty) + "' is not a supported JWS signing key (EC / RSA / Ed25519 / Ed448)");
|
|
189
|
+
}
|
|
190
|
+
if (explicitAlg === undefined || explicitAlg === null) return defaultAlg;
|
|
191
|
+
if (explicitAlg === "none" || REFUSED_ALGS.indexOf(explicitAlg) !== -1) {
|
|
192
|
+
throw new AuthError("auth-jwt-external/sign-alg-refused",
|
|
193
|
+
label + ": alg '" + explicitAlg + "' is refused (HMAC / none are never valid for an asymmetric signer)");
|
|
194
|
+
}
|
|
195
|
+
if (SUPPORTED_CLASSICAL_ALGS.indexOf(explicitAlg) === -1) {
|
|
196
|
+
throw new AuthError("auth-jwt-external/sign-alg-unsupported",
|
|
197
|
+
label + ": alg '" + explicitAlg + "' is not a supported classical JWS algorithm (" +
|
|
198
|
+
SUPPORTED_CLASSICAL_ALGS.join(", ") + ")");
|
|
199
|
+
}
|
|
200
|
+
if (compatible.indexOf(explicitAlg) === -1) {
|
|
201
|
+
throw new AuthError("auth-jwt-external/sign-alg-key-mismatch",
|
|
202
|
+
label + ": alg '" + explicitAlg + "' is incompatible with the " + kty +
|
|
203
|
+
" key (compatible: " + compatible.join(", ") + ")");
|
|
204
|
+
}
|
|
205
|
+
return explicitAlg;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// _signCompactJws — produce the compact JWS serialization (protected
|
|
209
|
+
// header . payload . signature) for an already-resolved alg + imported
|
|
210
|
+
// private key. Header and payload are JCS-independent here: they are
|
|
211
|
+
// serialized exactly once by the signer, base64url-encoded, and that byte
|
|
212
|
+
// string IS the signing input, so there is no canonicalization gap a
|
|
213
|
+
// verifier could diverge on.
|
|
214
|
+
function _signCompactJws(header, payload, privateKey, alg) {
|
|
215
|
+
var params = _verifyParamsForAlg(alg);
|
|
216
|
+
var headerB64 = bCrypto.toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
|
|
217
|
+
var payloadB64 = bCrypto.toBase64Url(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
218
|
+
var signingInput = headerB64 + "." + payloadB64;
|
|
219
|
+
var input = Buffer.from(signingInput, "ascii");
|
|
220
|
+
var sig;
|
|
221
|
+
if (params.hash === null) {
|
|
222
|
+
sig = nodeCrypto.sign(null, input, privateKey); // EdDSA — no prehash
|
|
223
|
+
} else {
|
|
224
|
+
var keyParam = { key: privateKey };
|
|
225
|
+
if (params.padding !== undefined) keyParam.padding = params.padding;
|
|
226
|
+
if (params.saltLength !== undefined) keyParam.saltLength = params.saltLength;
|
|
227
|
+
if (params.dsaEncoding !== undefined) keyParam.dsaEncoding = params.dsaEncoding;
|
|
228
|
+
sig = nodeCrypto.sign(params.hash, input, keyParam);
|
|
229
|
+
}
|
|
230
|
+
return signingInput + "." + bCrypto.toBase64Url(sig);
|
|
231
|
+
}
|
|
232
|
+
|
|
127
233
|
function _jwkToKey(jwk) {
|
|
128
234
|
try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
|
|
129
235
|
catch (e) {
|
|
@@ -512,12 +618,119 @@ async function verifyExternal(token, opts) {
|
|
|
512
618
|
return { header: header, claims: payload };
|
|
513
619
|
}
|
|
514
620
|
|
|
621
|
+
/**
|
|
622
|
+
* @primitive b.auth.jws.sign
|
|
623
|
+
* @signature b.auth.jws.sign(claims, opts)
|
|
624
|
+
* @since 0.14.22
|
|
625
|
+
* @status stable
|
|
626
|
+
* @compliance soc2
|
|
627
|
+
* @related b.auth.jar.build, b.auth.jar.parse
|
|
628
|
+
*
|
|
629
|
+
* Mint a compact JWS (RFC 7515) over <code>claims</code> using a classical
|
|
630
|
+
* asymmetric algorithm — RS/PS256/384/512, ES256/384/512, or EdDSA. This
|
|
631
|
+
* primitive exists strictly for <strong>interop with external ecosystems</strong>:
|
|
632
|
+
* OAuth/OIDC OPs and RPs (and the wallet / FAPI profiles built on them)
|
|
633
|
+
* require a request object / assertion signed with a classical JWS alg, and
|
|
634
|
+
* the framework's own token signer (<code>b.auth.jwt.sign</code>) is
|
|
635
|
+
* PQC-only (ML-DSA / SLH-DSA). It is <strong>never the framework-internal
|
|
636
|
+
* token default</strong>; <code>lib/jwt.js</code> remains the signer for
|
|
637
|
+
* tokens blamejs itself issues. The verify counterpart is
|
|
638
|
+
* <code>b.auth.jwt.verifyExternal</code>; this is its inverse for the cases
|
|
639
|
+
* where blamejs is the client emitting a signed object to a third party.
|
|
640
|
+
*
|
|
641
|
+
* The signing <code>alg</code> is derived from the key type (RFC 7518 §3.1)
|
|
642
|
+
* so the header alg always matches the signature the key can actually
|
|
643
|
+
* produce; an explicit <code>opts.alg</code> is validated against the key
|
|
644
|
+
* and refused if incompatible. <code>alg: "none"</code> and HMAC algs are
|
|
645
|
+
* refused outright — an asymmetric signer never emits them. The protected
|
|
646
|
+
* header always carries <code>alg</code>; <code>typ</code> and <code>kid</code>
|
|
647
|
+
* are set from <code>opts</code> when supplied (callers minting a typed
|
|
648
|
+
* object such as a JAR request object pass <code>typ</code>). Extra
|
|
649
|
+
* <code>opts.header</code> members pass through with two refusals:
|
|
650
|
+
* <code>b64</code> (RFC 7797 unencoded payload — it changes the signing
|
|
651
|
+
* input, which this signer always base64url-encodes) and <code>crit</code>
|
|
652
|
+
* (RFC 7515 §4.1.11 — it promises extension semantics the signer does not
|
|
653
|
+
* implement). Emitting either would produce a JWS whose header claims
|
|
654
|
+
* semantics its signature was not computed under.
|
|
655
|
+
*
|
|
656
|
+
* @opts
|
|
657
|
+
* {
|
|
658
|
+
* privateKey: KeyObject|PEM|JWK, // required — classical signing key
|
|
659
|
+
* alg?: string, // override; default inferred from the key (RS256 / ES256/384/512 / PS256 / EdDSA)
|
|
660
|
+
* typ?: string, // protected-header typ (e.g. "oauth-authz-req+jwt")
|
|
661
|
+
* kid?: string, // protected-header kid (JWKS key selection)
|
|
662
|
+
* header?: object, // extra protected-header members (alg/typ/kid reserved; b64/crit refused)
|
|
663
|
+
* }
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* var jws = b.auth.jws.sign(
|
|
667
|
+
* { iss: "client", aud: "https://as.example.com", response_type: "code" },
|
|
668
|
+
* { privateKey: clientKey, typ: "oauth-authz-req+jwt", kid: "c1" });
|
|
669
|
+
* // → "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ..."
|
|
670
|
+
*/
|
|
671
|
+
function signExternal(claims, opts) {
|
|
672
|
+
if (claims === null || typeof claims !== "object" || Array.isArray(claims)) {
|
|
673
|
+
throw new AuthError("auth-jwt-external/sign-bad-claims",
|
|
674
|
+
"jws.sign: claims must be a plain object");
|
|
675
|
+
}
|
|
676
|
+
validateOpts.requireObject(opts, "jws.sign", AuthError, "auth-jwt-external/sign-bad-opts");
|
|
677
|
+
validateOpts(opts, ["privateKey", "alg", "typ", "kid", "header"], "auth.jws.sign");
|
|
678
|
+
if (opts.alg !== undefined && opts.alg !== null) {
|
|
679
|
+
validateOpts.requireNonEmptyString(opts.alg, "jws.sign: alg", AuthError, "auth-jwt-external/sign-bad-alg");
|
|
680
|
+
}
|
|
681
|
+
if (opts.typ !== undefined && opts.typ !== null) {
|
|
682
|
+
validateOpts.requireNonEmptyString(opts.typ, "jws.sign: typ", AuthError, "auth-jwt-external/sign-bad-typ");
|
|
683
|
+
}
|
|
684
|
+
if (opts.kid !== undefined && opts.kid !== null) {
|
|
685
|
+
validateOpts.requireNonEmptyString(opts.kid, "jws.sign: kid", AuthError, "auth-jwt-external/sign-bad-kid");
|
|
686
|
+
}
|
|
687
|
+
validateOpts.optionalPlainObject(opts.header, "jws.sign: header", AuthError, "auth-jwt-external/sign-bad-header",
|
|
688
|
+
"must be a plain object of extra protected-header members");
|
|
689
|
+
// RFC 7797 `b64: false` changes the JWS signing input (the payload is
|
|
690
|
+
// signed raw, not base64url-encoded) and RFC 7515 §4.1.11 `crit`
|
|
691
|
+
// promises the producer implements every extension it names.
|
|
692
|
+
// _signCompactJws always base64url-encodes the payload and implements
|
|
693
|
+
// no header extensions, so passing either member through would mint a
|
|
694
|
+
// JWS whose header advertises semantics its signature was not computed
|
|
695
|
+
// under — a compliant verifier derives a different signing input (or
|
|
696
|
+
// refuses the critical header). Refused until those semantics are
|
|
697
|
+
// actually implemented.
|
|
698
|
+
if (opts.header !== undefined && opts.header !== null &&
|
|
699
|
+
(Object.prototype.hasOwnProperty.call(opts.header, "b64") ||
|
|
700
|
+
Object.prototype.hasOwnProperty.call(opts.header, "crit"))) {
|
|
701
|
+
throw new AuthError("auth-jwt-external/sign-unsupported-header",
|
|
702
|
+
"jws.sign: header members 'b64' (RFC 7797 unencoded payload) and 'crit' " +
|
|
703
|
+
"(RFC 7515 §4.1.11 critical extensions) are not supported — the signer " +
|
|
704
|
+
"always base64url-encodes the payload and implements no critical extensions");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
var key = _toPrivateKey(opts.privateKey, "jws.sign");
|
|
708
|
+
var alg = _resolveSignAlg(opts.alg, key, "jws.sign");
|
|
709
|
+
|
|
710
|
+
// Extra protected-header members first (alg/typ/kid reserved so a
|
|
711
|
+
// caller-supplied header object can never override the signer-set alg —
|
|
712
|
+
// the canonical alg-substitution shape), then the reserved members.
|
|
713
|
+
var header = validateOpts.assignOwnEnumerable({}, opts.header, ["alg", "typ", "kid"]);
|
|
714
|
+
header.alg = alg;
|
|
715
|
+
if (opts.typ !== undefined && opts.typ !== null) header.typ = opts.typ;
|
|
716
|
+
if (opts.kid !== undefined && opts.kid !== null) header.kid = opts.kid;
|
|
717
|
+
|
|
718
|
+
return _signCompactJws(header, claims, key, alg);
|
|
719
|
+
}
|
|
720
|
+
|
|
515
721
|
module.exports = {
|
|
516
722
|
verifyExternal: verifyExternal,
|
|
723
|
+
signExternal: signExternal,
|
|
517
724
|
SUPPORTED_CLASSICAL_ALGS: SUPPORTED_CLASSICAL_ALGS,
|
|
518
725
|
REFUSED_ALGS: REFUSED_ALGS,
|
|
519
726
|
// Shared JOSE defenses — routed from oauth.verifyIdToken /
|
|
520
727
|
// oid4vci proof verify / sd-jwt-vc.verify / openid-federation.
|
|
521
728
|
_assertAlgKtyMatch: _assertAlgKtyMatch,
|
|
522
729
|
_issuerMatches: _issuerMatches,
|
|
730
|
+
// Classical-JWS signer internals — composed by oauth.js's attestation
|
|
731
|
+
// builders so the alg-from-key + compact-JWS bodies live in exactly one
|
|
732
|
+
// place (the classical-JOSE domain owner).
|
|
733
|
+
_toPrivateKey: _toPrivateKey,
|
|
734
|
+
_resolveSignAlg: _resolveSignAlg,
|
|
735
|
+
_signCompactJws: _signCompactJws,
|
|
523
736
|
};
|
package/lib/auth/oauth.js
CHANGED
|
@@ -122,6 +122,11 @@ var lazyRequire = require("../lazy-require");
|
|
|
122
122
|
// convention §3; no circular load — jwt-external requires nothing from
|
|
123
123
|
// oauth.
|
|
124
124
|
var jwtExternal = require("./jwt-external");
|
|
125
|
+
// RFC 9101 request-object builder — composed by pushAuthorizationRequest
|
|
126
|
+
// when the operator opts into sending a signed request object. Top-of-file
|
|
127
|
+
// per convention §3; no circular load — jar requires jwt-external +
|
|
128
|
+
// validate-opts only, nothing from oauth.
|
|
129
|
+
var jar = require("./jar");
|
|
125
130
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
126
131
|
|
|
127
132
|
// Cap on responses parsed from upstream OAuth providers. Token /
|
|
@@ -517,97 +522,62 @@ var MAX_ATTESTATION_JWT_BYTES = C.BYTES.kib(16);
|
|
|
517
522
|
var DEFAULT_POP_MAX_AGE_SEC = C.TIME.minutes(5) / C.TIME.seconds(1);
|
|
518
523
|
|
|
519
524
|
// Sign/verify params keyed by alg — superset of _verifyParamsForAlg that
|
|
520
|
-
// also covers EdDSA (used only on the attestation path; the
|
|
521
|
-
// verifier keeps its own narrower table untouched).
|
|
525
|
+
// also covers EdDSA (used only on the attestation verify path; the
|
|
526
|
+
// ID-token verifier keeps its own narrower table untouched).
|
|
522
527
|
function _attestationCryptoParams(alg) {
|
|
523
528
|
if (alg === "EdDSA") return { hash: null };
|
|
524
529
|
return _verifyParamsForAlg(alg);
|
|
525
530
|
}
|
|
526
531
|
|
|
532
|
+
// _toAttestationPrivateKey / _resolveAttestationAlg / _signAttestationJws —
|
|
533
|
+
// thin wrappers over the classical-JWS signer that the jwt-external module
|
|
534
|
+
// owns (b.auth.jws.sign internals). The attestation path keeps its own
|
|
535
|
+
// `auth-oauth/attestation-*` error codes so operators routing alerts on
|
|
536
|
+
// that class see no change; the signer BODIES (alg-from-key derivation,
|
|
537
|
+
// compact-JWS assembly) live in exactly one place — the classical-JOSE
|
|
538
|
+
// domain owner — rather than duplicated here. RFC 7518 §3.1 alg↔key
|
|
539
|
+
// binding and the self-invalid-alg defenses are enforced by the composed
|
|
540
|
+
// primitive.
|
|
541
|
+
|
|
527
542
|
function _toAttestationPrivateKey(value, label) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (typeof value === "string" || Buffer.isBuffer(value)) {
|
|
534
|
-
return nodeCrypto.createPrivateKey({ key: value, format: "pem" });
|
|
535
|
-
}
|
|
536
|
-
if (typeof value === "object" && value.kty) {
|
|
537
|
-
return nodeCrypto.createPrivateKey({ key: value, format: "jwk" });
|
|
538
|
-
}
|
|
539
|
-
} catch (e) {
|
|
540
|
-
throw new OAuthError("auth-oauth/attestation-bad-key",
|
|
541
|
-
label + ": private key parse failed: " + ((e && e.message) || String(e)));
|
|
543
|
+
try { return jwtExternal._toPrivateKey(value, label); }
|
|
544
|
+
catch (e) {
|
|
545
|
+
var code = (e && e.code) === "auth-jwt-external/sign-no-key"
|
|
546
|
+
? "auth-oauth/attestation-no-key" : "auth-oauth/attestation-bad-key";
|
|
547
|
+
throw new OAuthError(code, (e && e.message) || String(e));
|
|
542
548
|
}
|
|
543
|
-
throw new OAuthError("auth-oauth/attestation-bad-key",
|
|
544
|
-
label + ": privateKey must be a PEM string/Buffer, JWK object, or KeyObject");
|
|
545
549
|
}
|
|
546
550
|
|
|
547
|
-
// EC curve → the one ES* alg whose hash matches it (RFC 7518 §3.4).
|
|
548
|
-
var _EC_CURVE_ALG = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
|
|
549
|
-
|
|
550
551
|
// Resolve the JWS alg for an attestation / PoP signature. When the caller
|
|
551
|
-
// gives no `algorithm`,
|
|
552
|
-
// non-EC attester key (RSA, Ed25519) yields a
|
|
553
|
-
// alg ⇄ signature key — instead of a fixed
|
|
554
|
-
// real key, which `verifyClientAttestation`'s
|
|
555
|
-
// then reject. An explicit alg incompatible with
|
|
556
|
-
// signing
|
|
552
|
+
// gives no `algorithm`, the composed signer infers the default that matches
|
|
553
|
+
// the key type so a non-EC attester key (RSA, Ed25519) yields a
|
|
554
|
+
// self-consistent JWS — header alg ⇄ signature key — instead of a fixed
|
|
555
|
+
// `ES256` header signed with the real key, which `verifyClientAttestation`'s
|
|
556
|
+
// alg/kty cross-check would then reject. An explicit alg incompatible with
|
|
557
|
+
// the key is refused BEFORE signing. The draft additionally floors the
|
|
558
|
+
// accepted set to ATTESTATION_ALGS (no HMAC / none); the composed resolver
|
|
559
|
+
// already refuses those, surfaced here as the attestation-specific code.
|
|
557
560
|
function _resolveAttestationAlg(explicitAlg, privateKey, label) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
var
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
compatible = ["PS256", "PS384", "PS512"]; // an RSA-PSS key cannot produce an RS* signature
|
|
574
|
-
} else if (kty === "ed25519" || kty === "ed448") {
|
|
575
|
-
defaultAlg = "EdDSA";
|
|
576
|
-
compatible = ["EdDSA"];
|
|
577
|
-
} else {
|
|
578
|
-
throw new OAuthError("auth-oauth/attestation-key-unsupported",
|
|
579
|
-
label + ": key type '" + String(kty) + "' is not a supported attestation key (EC / RSA / Ed25519 / Ed448)");
|
|
580
|
-
}
|
|
581
|
-
if (explicitAlg === undefined || explicitAlg === null) return defaultAlg;
|
|
582
|
-
if (ATTESTATION_ALGS.indexOf(explicitAlg) === -1) {
|
|
583
|
-
throw new OAuthError("auth-oauth/attestation-alg-not-accepted",
|
|
584
|
-
label + ": alg '" + explicitAlg + "' is not an accepted attestation algorithm");
|
|
585
|
-
}
|
|
586
|
-
if (compatible.indexOf(explicitAlg) === -1) {
|
|
587
|
-
throw new OAuthError("auth-oauth/attestation-alg-key-mismatch",
|
|
588
|
-
label + ": alg '" + explicitAlg + "' is incompatible with the " + kty +
|
|
589
|
-
" key (compatible: " + compatible.join(", ") + ")");
|
|
561
|
+
try {
|
|
562
|
+
return jwtExternal._resolveSignAlg(explicitAlg, privateKey, label);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
var ec = (e && e.code) || "";
|
|
565
|
+
if (ec === "auth-jwt-external/sign-alg-key-mismatch") {
|
|
566
|
+
throw new OAuthError("auth-oauth/attestation-alg-key-mismatch", (e && e.message) || String(e));
|
|
567
|
+
}
|
|
568
|
+
if (ec === "auth-jwt-external/sign-alg-refused" || ec === "auth-jwt-external/sign-alg-unsupported") {
|
|
569
|
+
throw new OAuthError("auth-oauth/attestation-alg-not-accepted",
|
|
570
|
+
label + ": alg '" + explicitAlg + "' is not an accepted attestation algorithm");
|
|
571
|
+
}
|
|
572
|
+
if (ec === "auth-jwt-external/sign-key-unsupported") {
|
|
573
|
+
throw new OAuthError("auth-oauth/attestation-key-unsupported", (e && e.message) || String(e));
|
|
574
|
+
}
|
|
575
|
+
throw new OAuthError("auth-oauth/attestation-bad-key", (e && e.message) || String(e));
|
|
590
576
|
}
|
|
591
|
-
return explicitAlg;
|
|
592
577
|
}
|
|
593
578
|
|
|
594
579
|
function _signAttestationJws(header, payload, privateKey, alg) {
|
|
595
|
-
|
|
596
|
-
var headerB64 = _b64urlEncode(Buffer.from(JSON.stringify(header), "utf8"));
|
|
597
|
-
var payloadB64 = _b64urlEncode(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
598
|
-
var signingInput = headerB64 + "." + payloadB64;
|
|
599
|
-
var sig;
|
|
600
|
-
var input = Buffer.from(signingInput, "ascii");
|
|
601
|
-
if (params.hash === null) {
|
|
602
|
-
sig = nodeCrypto.sign(null, input, privateKey); // EdDSA — no prehash
|
|
603
|
-
} else {
|
|
604
|
-
var keyParam = { key: privateKey };
|
|
605
|
-
if (params.padding !== undefined) keyParam.padding = params.padding;
|
|
606
|
-
if (params.saltLength !== undefined) keyParam.saltLength = params.saltLength;
|
|
607
|
-
if (params.dsaEncoding !== undefined) keyParam.dsaEncoding = params.dsaEncoding;
|
|
608
|
-
sig = nodeCrypto.sign(params.hash, input, keyParam);
|
|
609
|
-
}
|
|
610
|
-
return signingInput + "." + _b64urlEncode(sig);
|
|
580
|
+
return jwtExternal._signCompactJws(header, payload, privateKey, alg);
|
|
611
581
|
}
|
|
612
582
|
|
|
613
583
|
// Verify a compact JWS against an already-imported public KeyObject. The
|
|
@@ -751,16 +721,9 @@ function buildClientAttestation(aopts) {
|
|
|
751
721
|
};
|
|
752
722
|
if (typeof aopts.nbf === "number") payload.nbf = aopts.nbf;
|
|
753
723
|
// Operator extra claims merged WITHOUT overriding the spec-required
|
|
754
|
-
// fields (
|
|
755
|
-
// names rejected).
|
|
724
|
+
// fields (proto-pollution sentinels skipped, the spec keys reserved).
|
|
756
725
|
if (aopts.extraClaims && typeof aopts.extraClaims === "object" && !Array.isArray(aopts.extraClaims)) {
|
|
757
|
-
|
|
758
|
-
for (var i = 0; i < ck.length; i += 1) {
|
|
759
|
-
var k = ck[i];
|
|
760
|
-
if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
|
|
761
|
-
if (Object.prototype.hasOwnProperty.call(payload, k)) continue; // never override spec fields
|
|
762
|
-
payload[k] = aopts.extraClaims[k];
|
|
763
|
-
}
|
|
726
|
+
validateOpts.assignOwnEnumerable(payload, aopts.extraClaims, Object.keys(payload));
|
|
764
727
|
}
|
|
765
728
|
return _signAttestationJws(
|
|
766
729
|
{ typ: "oauth-client-attestation+jwt", alg: alg }, payload, key, alg);
|
|
@@ -1996,6 +1959,13 @@ function create(opts) {
|
|
|
1996
1959
|
// browser-side redirect to /authorize. Defends against parameter
|
|
1997
1960
|
// tampering by an MITM at the user-agent + against URL-length
|
|
1998
1961
|
// overflow on long authorization requests.
|
|
1962
|
+
//
|
|
1963
|
+
// RFC 9101 signed request object: pass `signedRequestObject: { key,
|
|
1964
|
+
// alg?, kid?, audience?, expiresInMs? }` to push a JAR request object
|
|
1965
|
+
// instead of plain form params. The authorization parameters then
|
|
1966
|
+
// travel as signed claims (RFC 9126 §3 — form body carries only
|
|
1967
|
+
// `request` + client auth), so the PAR endpoint can verify they
|
|
1968
|
+
// arrived exactly as the client signed them. Absent → plain-form PAR.
|
|
1999
1969
|
async function pushAuthorizationRequest(uopts) {
|
|
2000
1970
|
uopts = uopts || {};
|
|
2001
1971
|
var endpoint;
|
|
@@ -2006,6 +1976,20 @@ function create(opts) {
|
|
|
2006
1976
|
"pushed_authorization_request_endpoint (set opts.pushedAuthorizationRequestEndpoint " +
|
|
2007
1977
|
"on create() if the IdP doesn't publish it)");
|
|
2008
1978
|
}
|
|
1979
|
+
// RFC 9101 signed-request-object opt: when the operator supplies
|
|
1980
|
+
// `signedRequestObject` (a config object carrying the client's signing
|
|
1981
|
+
// key), the authorization parameters travel as claims of a JAR request
|
|
1982
|
+
// object rather than as bare form params. Validated config-time; absent
|
|
1983
|
+
// → the existing plain-form path sends the same key/value set
|
|
1984
|
+
// (form-encoded params are unordered per the media type).
|
|
1985
|
+
var sro = uopts.signedRequestObject || null;
|
|
1986
|
+
if (sro) {
|
|
1987
|
+
validateOpts.optionalPlainObject(sro, "pushAuthorizationRequest: signedRequestObject",
|
|
1988
|
+
OAuthError, "auth-oauth/par-bad-request-object-opt",
|
|
1989
|
+
"must be an object { key, alg?, kid?, audience?, expiresInMs? }");
|
|
1990
|
+
validateOpts(sro, ["key", "alg", "kid", "audience", "expiresInMs"],
|
|
1991
|
+
"pushAuthorizationRequest.signedRequestObject");
|
|
1992
|
+
}
|
|
2009
1993
|
// Same PKCE-downgrade gate as authorizationUrl (RFC 9700 §4.13):
|
|
2010
1994
|
// PAR pushes the identical S256 challenge, so an OP advertising
|
|
2011
1995
|
// code_challenge_methods_supported without S256 is refused here too.
|
|
@@ -2015,31 +1999,60 @@ function create(opts) {
|
|
|
2015
1999
|
var state = uopts.state || _generateRandomToken(STATE_NONCE_BYTES);
|
|
2016
2000
|
var nonce = uopts.nonce || (isOidc ? _generateRandomToken(STATE_NONCE_BYTES) : null);
|
|
2017
2001
|
var pkceVals = _generatePkce();
|
|
2018
|
-
|
|
2019
|
-
body
|
|
2020
|
-
body.
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
if (
|
|
2031
|
-
if (
|
|
2002
|
+
// The authorization-request parameters. On the plain path these are set
|
|
2003
|
+
// on the form body directly; on the JAR path they become request-object
|
|
2004
|
+
// claims and the form body carries only `request` + client auth.
|
|
2005
|
+
var authzParams = {
|
|
2006
|
+
response_type: "code",
|
|
2007
|
+
client_id: clientId,
|
|
2008
|
+
redirect_uri: redirectUri,
|
|
2009
|
+
scope: scope.join(" "),
|
|
2010
|
+
state: state,
|
|
2011
|
+
code_challenge: pkceVals.challenge,
|
|
2012
|
+
code_challenge_method: "S256",
|
|
2013
|
+
};
|
|
2014
|
+
if (nonce) authzParams.nonce = nonce;
|
|
2015
|
+
if (responseMode) authzParams.response_mode = responseMode;
|
|
2016
|
+
if (uopts.prompt) authzParams.prompt = uopts.prompt;
|
|
2017
|
+
if (uopts.loginHint) authzParams.login_hint = uopts.loginHint;
|
|
2018
|
+
if (uopts.maxAge != null) authzParams.max_age = String(uopts.maxAge);
|
|
2032
2019
|
// RFC 9396 — push the fine-grained authorization request through PAR
|
|
2033
2020
|
// identically to the redirect path (validated, then JSON-serialized).
|
|
2034
2021
|
var requestedAuthzDetails = null;
|
|
2035
2022
|
if (uopts.authorizationDetails !== undefined) {
|
|
2036
2023
|
requestedAuthzDetails = _validateAuthorizationDetailsArray(
|
|
2037
2024
|
uopts.authorizationDetails, "pushAuthorizationRequest");
|
|
2038
|
-
|
|
2025
|
+
authzParams.authorization_details = JSON.stringify(requestedAuthzDetails);
|
|
2039
2026
|
}
|
|
2040
2027
|
if (uopts.extraParams && typeof uopts.extraParams === "object") {
|
|
2041
2028
|
var ek = Object.keys(uopts.extraParams);
|
|
2042
|
-
for (var i = 0; i < ek.length; i++)
|
|
2029
|
+
for (var i = 0; i < ek.length; i++) authzParams[ek[i]] = String(uopts.extraParams[ek[i]]);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
var body = new URLSearchParams();
|
|
2033
|
+
if (sro) {
|
|
2034
|
+
// RFC 9126 §3 — when a signed request object is pushed, the
|
|
2035
|
+
// authorization parameters MUST appear ONLY as claims of the JWT;
|
|
2036
|
+
// the form body carries `request` plus the parameters a client
|
|
2037
|
+
// authentication method requires (client_id, and client_secret for
|
|
2038
|
+
// the secret-based methods) and nothing else. The JAR `aud` is the
|
|
2039
|
+
// AS issuer identifier (RFC 9101 §5) — the operator may override but
|
|
2040
|
+
// it defaults to the configured `issuer`.
|
|
2041
|
+
var requestJwt = jar.build(authzParams, {
|
|
2042
|
+
clientId: clientId,
|
|
2043
|
+
audience: sro.audience || issuer,
|
|
2044
|
+
key: sro.key,
|
|
2045
|
+
alg: sro.alg,
|
|
2046
|
+
kid: sro.kid,
|
|
2047
|
+
expiresInMs: sro.expiresInMs,
|
|
2048
|
+
});
|
|
2049
|
+
body.set("request", requestJwt);
|
|
2050
|
+
body.set("client_id", clientId); // RFC 9126 §3 — client identification
|
|
2051
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
2052
|
+
} else {
|
|
2053
|
+
var ak = Object.keys(authzParams);
|
|
2054
|
+
for (var ap = 0; ap < ak.length; ap++) body.set(ak[ap], authzParams[ak[ap]]);
|
|
2055
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
2043
2056
|
}
|
|
2044
2057
|
var rv = await _postForm(endpoint, body);
|
|
2045
2058
|
if (!rv || typeof rv.request_uri !== "string" || rv.request_uri.length === 0) {
|
|
@@ -2062,6 +2075,7 @@ function create(opts) {
|
|
|
2062
2075
|
requestUri: rv.request_uri,
|
|
2063
2076
|
expiresIn: typeof rv.expires_in === "number" ? rv.expires_in : null,
|
|
2064
2077
|
authorizationDetails: requestedAuthzDetails,
|
|
2078
|
+
requestObjectSent: !!sro,
|
|
2065
2079
|
};
|
|
2066
2080
|
}
|
|
2067
2081
|
|
package/lib/compliance.js
CHANGED
|
@@ -1580,9 +1580,46 @@ function fipsMode(enable) {
|
|
|
1580
1580
|
return STATE.fipsMode;
|
|
1581
1581
|
}
|
|
1582
1582
|
|
|
1583
|
+
// Postures whose jurisdictions restrict cross-border data transfer
|
|
1584
|
+
// (GDPR Art 44-46 / UK-GDPR / DPDP §16 / PIPL Art 38 / LGPD Art 33 /
|
|
1585
|
+
// APPI Art 28 / PDPA §26). The residency write gates (db-query local,
|
|
1586
|
+
// external-db backend/replica) refuse mismatched writes under these;
|
|
1587
|
+
// other postures observe-and-audit only.
|
|
1588
|
+
var CROSS_BORDER_REGULATED_POSTURES = Object.freeze([
|
|
1589
|
+
"gdpr", "uk-gdpr", "dpdp", "pipl-cn", "lgpd-br", "appi-jp", "pdpa-sg",
|
|
1590
|
+
]);
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* @primitive b.compliance.isCrossBorderRegulated
|
|
1594
|
+
* @signature b.compliance.isCrossBorderRegulated(posture)
|
|
1595
|
+
* @since 0.14.24
|
|
1596
|
+
* @compliance gdpr
|
|
1597
|
+
* @related b.compliance.current, b.cryptoField.declarePerRowResidency
|
|
1598
|
+
*
|
|
1599
|
+
* Returns true when `posture` is one of the cross-border regulated
|
|
1600
|
+
* postures (gdpr / uk-gdpr / dpdp / pipl-cn / lgpd-br / appi-jp /
|
|
1601
|
+
* pdpa-sg) — the jurisdictions whose transfer restrictions flip the
|
|
1602
|
+
* data-residency write gates from advisory to refusing. The set
|
|
1603
|
+
* itself is exported as `CROSS_BORDER_REGULATED_POSTURES`; this
|
|
1604
|
+
* helper is the membership test the local (`b.db.from`) and external
|
|
1605
|
+
* (`b.externalDb.query`) gates share. Non-string and unknown postures
|
|
1606
|
+
* return false.
|
|
1607
|
+
*
|
|
1608
|
+
* @example
|
|
1609
|
+
* b.compliance.isCrossBorderRegulated("gdpr"); // → true
|
|
1610
|
+
* b.compliance.isCrossBorderRegulated("soc2"); // → false
|
|
1611
|
+
* b.compliance.isCrossBorderRegulated(null); // → false
|
|
1612
|
+
*/
|
|
1613
|
+
function isCrossBorderRegulated(posture) {
|
|
1614
|
+
if (typeof posture !== "string" || posture.length === 0) return false;
|
|
1615
|
+
return CROSS_BORDER_REGULATED_POSTURES.indexOf(posture) !== -1;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1583
1618
|
module.exports = {
|
|
1584
1619
|
set: set,
|
|
1585
1620
|
current: current,
|
|
1621
|
+
isCrossBorderRegulated: isCrossBorderRegulated,
|
|
1622
|
+
CROSS_BORDER_REGULATED_POSTURES: CROSS_BORDER_REGULATED_POSTURES,
|
|
1586
1623
|
assert: assert,
|
|
1587
1624
|
clear: clear,
|
|
1588
1625
|
describe: describe,
|