@blamejs/pki 0.9.4 → 0.9.5
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 +23 -0
- package/lib/cms-digest.js +7 -2
- package/lib/cms-encrypt.js +6 -1
- package/lib/cms-sign.js +16 -0
- package/lib/cms-verify.js +14 -6
- package/lib/csr-sign.js +5 -1
- package/lib/est.js +23 -2
- package/lib/guard-identifier.js +27 -1
- package/lib/ocsp.js +8 -0
- package/lib/path-validate.js +7 -2
- package/lib/pkcs12-build.js +6 -1
- package/lib/pki-build.js +38 -6
- package/lib/revocation-fetch.js +8 -2
- package/lib/scep.js +11 -1
- package/lib/smime.js +14 -3
- package/lib/tsp-sign.js +8 -1
- package/lib/x509-sign.js +18 -6
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.9.5 — 2026-10-12
|
|
8
|
+
|
|
9
|
+
Refuse a non-boolean switch option instead of reading it as the state the caller did not ask for.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- For every one of these switches, `true` and `false` pass, `null` and an omitted option select the documented default, and any other value is refused with that module's own `domain/reason` code naming the option. For every value that was already valid the emitted bytes are unchanged. A call that passes a non-boolean to one of these options now throws instead of building a message the other way: pass the boolean, or omit the option to take its default.
|
|
14
|
+
- `opts.pem` is unchanged and still takes `true` or a PEM label string where a verb documents one, as do `signingTime` (a time or `false`) and `pki.pkcs12.build`'s `mac` (a descriptor or `false`). These carry a value as well as an off form, and each is held to the type its own value needs.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `pki.cms.sign` refuses a non-boolean `detached`. The option decided the message shape in one place and whether the content was carried in another, so `detached: "false"` took the attached branch and then omitted the content from it, emitting an attached SignedData with nothing in it.
|
|
19
|
+
- `pki.cms.authenticate` refuses a non-boolean `authenticatedAttributes`. With `false` the MAC covers the content octets directly (RFC 6211 sec. 2), so the two values are structurally different messages and a mistyped one produced the message the caller had declined.
|
|
20
|
+
- `pki.cms.digest` refuses a non-boolean `detached`; `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `signedAttributes`; `pki.ocsp.sign` refuses a non-boolean `embedCert` or `extendedRevoke`; `pki.tsp.sign` refuses a non-boolean `ordering`; `pki.pkcs12.build` refuses a non-boolean `integrity.certificates`; `pki.path.crlChecker` and `pki.path.fetchingChecker` refuse a non-boolean `useDeltas`.
|
|
21
|
+
- `pki.est.serverkeygen` reads a `null` `requestedEncryption` as the absent option it documents rather than as a stated `false`. It tested whether the caller had stated an override by presence alone, so a `null` counted as stated and coerced to `false`, and a request against a CSR that does advertise key encryption was refused for contradicting a CSR the caller never disagreed with.
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
- `pki.smime.sign` and `pki.smime.encrypt` refuse a non-boolean `protectHeaders`. Read by comparison to `true`, a value of any other type left RFC 9788 header protection off and the message went out unprotected with no error, so a caller that asked for the protection did not get it and was not told.
|
|
26
|
+
- The `pki.est` and `pki.scep` verbs refuse a non-boolean `allowCrossOriginRedirect`. Read by truthiness, `"false"` allowed the cross-origin redirect on an unsafe method that the check exists to refuse (RFC 7030 sec. 3.2.1), which is the opposite of what that value states.
|
|
27
|
+
- `pki.cms.sign` and `pki.cms.countersign` refuse a non-boolean `certificates`, the switch that withholds the signer chain. A value of any other type embedded the certificates the caller asked to leave out.
|
|
28
|
+
- `pki.x509.sign` and `pki.csr.sign` refuse a non-boolean criticality flag (`keyUsageCritical`, `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`, `tlsFeatureCritical`). A value of any other type could emit an extension critical that the caller asked to be non-critical, and RFC 5280 sec. 6.1 requires a relying party that does not recognize a critical extension to reject the certificate.
|
|
29
|
+
|
|
7
30
|
## v0.9.4 — 2026-10-12
|
|
8
31
|
|
|
9
32
|
Refuse to issue an elliptic curve certificate asserting a key usage its own algorithm forbids.
|
package/lib/cms-digest.js
CHANGED
|
@@ -78,7 +78,8 @@ function _digestName(opts) {
|
|
|
78
78
|
* @opts
|
|
79
79
|
* - `digestAlgorithm` (string) -- `sha256` (default), `sha384` or `sha512`.
|
|
80
80
|
* - `contentType` (string) -- the encapsulated content type by OID name, default `data`.
|
|
81
|
-
* - `detached` (boolean) -- omit the content, leaving the digest to travel alone.
|
|
81
|
+
* - `detached` (boolean) -- omit the content, leaving the digest to travel alone. A value other
|
|
82
|
+
* than `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
|
|
82
83
|
* - `pem` (boolean) -- return a PEM `CMS` block instead of DER.
|
|
83
84
|
* @example
|
|
84
85
|
* var der = await pki.cms.digest(Buffer.from("hello"));
|
|
@@ -94,6 +95,10 @@ async function _digest(content, opts) {
|
|
|
94
95
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.digest options");
|
|
95
96
|
guard.identifier.assertKnownKeys(opts, _DIGEST_OPTS, _err, "cms/bad-input",
|
|
96
97
|
"pki.cms.digest has an unknown option ");
|
|
98
|
+
/** @internal Whether the content rides in the message is decided below by comparing this option
|
|
99
|
+
* to `true`, so a value of any other type attached a content the caller asked to leave out. */
|
|
100
|
+
var detached = guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input",
|
|
101
|
+
"pki.cms.digest opts.detached");
|
|
97
102
|
var raw = guard.bytes.source(content, CmsError, "cms/bad-input", "content");
|
|
98
103
|
var alg = _digestName(opts);
|
|
99
104
|
var ctName = opts.contentType === undefined ? "data" : opts.contentType;
|
|
@@ -101,7 +106,7 @@ async function _digest(content, opts) {
|
|
|
101
106
|
if (!ctOid) throw _err("cms/bad-input", "opts.contentType is not a known OID name: " + guard.text.showValue(ctName));
|
|
102
107
|
|
|
103
108
|
var value = guard.crypto.digest(NODE_DIGEST[alg], raw);
|
|
104
|
-
var eciChildren =
|
|
109
|
+
var eciChildren = detached
|
|
105
110
|
? [b.oid(ctOid)]
|
|
106
111
|
: [b.oid(ctOid), b.explicit(0, b.octetString(raw))];
|
|
107
112
|
var dd = b.sequence([
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -649,7 +649,12 @@ function authenticate(content, recipients, opts) {
|
|
|
649
649
|
|
|
650
650
|
async function _authenticateImpl(content, recipients, opts) {
|
|
651
651
|
opts = opts || intrinsic.create(null);
|
|
652
|
-
|
|
652
|
+
/** @internal Typed before it is used, because this switch picks the option set the door below
|
|
653
|
+
* checks against as well as the message that gets built: with `authenticatedAttributes: false`
|
|
654
|
+
* the MAC covers the content octets directly, so a value of any other type selected a
|
|
655
|
+
* structurally different message than the caller asked for and said nothing. */
|
|
656
|
+
var withAttrs = guard.identifier.booleanOptionDefaultOn(opts.authenticatedAttributes, _err,
|
|
657
|
+
"cms/bad-input", "pki.cms.authenticate opts.authenticatedAttributes");
|
|
653
658
|
guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
|
|
654
659
|
_err, "cms/bad-input", function (k) {
|
|
655
660
|
return "pki.cms.authenticate has an unknown option " + JSON.stringify(k) +
|
package/lib/cms-sign.js
CHANGED
|
@@ -186,6 +186,19 @@ function _algorithmProtectionPair(attrOid, digestAlgId, armAlgId, armTag) {
|
|
|
186
186
|
* The emitter below reads the same option again, which is sound because the options door refuses
|
|
187
187
|
* one supplied through an accessor, own or inherited, before any value is read: two reads of a
|
|
188
188
|
* plain field are one answer. */
|
|
189
|
+
/** @internal The three switches these verbs document, typed where the options arrive. Each is read
|
|
190
|
+
* further down by comparing it to `false`, which answers for a value of any other type as the
|
|
191
|
+
* state the caller did not ask for: measured, `certificates: "false"` embedded the signer
|
|
192
|
+
* certificate the caller asked to withhold, and `detached: "false"` is read as "not detached" by
|
|
193
|
+
* the shape test and as "detached" by the content test, emitting an attached SignedData with no
|
|
194
|
+
* content in it. Typed here, those comparisons read a value already known to be a boolean.
|
|
195
|
+
* `detached` is not an option every one of these verbs accepts; an absent one types as absent. */
|
|
196
|
+
function _assertSignSwitches(opts, verb) {
|
|
197
|
+
guard.identifier.booleanOptionDefaultOn(opts.certificates, _err, "cms/bad-input", verb + " opts.certificates");
|
|
198
|
+
guard.identifier.booleanOptionDefaultOn(opts.signedAttributes, _err, "cms/bad-input", verb + " opts.signedAttributes");
|
|
199
|
+
guard.identifier.booleanOption(opts.detached, _err, "cms/bad-input", verb + " opts.detached");
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
function _assertProtectionPlaceable(opts) {
|
|
190
203
|
var wanted = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
|
|
191
204
|
if (opts.signedAttributes === false && wanted) {
|
|
@@ -373,6 +386,7 @@ function _signDispatch(content, signers, opts) {
|
|
|
373
386
|
async function _signStream(stream, signers, opts) {
|
|
374
387
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
375
388
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
389
|
+
_assertSignSwitches(opts, "pki.cms.sign");
|
|
376
390
|
_assertProtectionPlaceable(opts);
|
|
377
391
|
if (opts.detached !== true) {
|
|
378
392
|
throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
|
|
@@ -413,6 +427,7 @@ async function _signStream(stream, signers, opts) {
|
|
|
413
427
|
function _sign(content, signers, opts) {
|
|
414
428
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
415
429
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
430
|
+
_assertSignSwitches(opts, "pki.cms.sign");
|
|
416
431
|
_assertProtectionPlaceable(opts);
|
|
417
432
|
var contentBuf = _toBuf(content, "content");
|
|
418
433
|
var list = Array.isArray(signers) ? _b.reqDenseArray(signers, "the signer list") : [signers];
|
|
@@ -680,6 +695,7 @@ function countersign(cmsInput, signers, opts) {
|
|
|
680
695
|
function _countersign(cmsInput, signers, opts) {
|
|
681
696
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
|
|
682
697
|
guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
698
|
+
_assertSignSwitches(opts, "pki.cms.countersign");
|
|
683
699
|
_assertProtectionPlaceable(opts);
|
|
684
700
|
var list = Array.isArray(signers) ? signers : [signers];
|
|
685
701
|
if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
|
package/lib/cms-verify.js
CHANGED
|
@@ -1275,16 +1275,20 @@ function _addCert(out, der) {
|
|
|
1275
1275
|
* the same content as the buffered form.
|
|
1276
1276
|
*
|
|
1277
1277
|
* @opts detached Omit the encapsulated content (a detached signature; the verifier
|
|
1278
|
-
* supplies the content). Default false.
|
|
1278
|
+
* supplies the content). Default false. A value other than `true`,
|
|
1279
|
+
* `false`, `null` or an absent option is refused `cms/bad-input`.
|
|
1279
1280
|
* @opts eContentType The encapsulated content type (an OID name). Default `data`.
|
|
1280
1281
|
* @opts signedAttributes Include signed attributes (content-type, message-digest, signing-time);
|
|
1281
|
-
* false signs the content directly. Default true.
|
|
1282
|
+
* false signs the content directly. Default true. A value other than
|
|
1283
|
+
* `true`, `false`, `null` or an absent option is refused `cms/bad-input`.
|
|
1282
1284
|
* @opts additionalSignedAttributes Extra signed attributes to add to every SignerInfo, each
|
|
1283
1285
|
* `{ type, values }` (`type` an OID name or dotted string, `values` an
|
|
1284
1286
|
* array of DER value `Buffer`s). Covered by the signature.
|
|
1285
1287
|
* @opts signingTime A `Date` for the signing-time attribute, or false to omit it.
|
|
1286
1288
|
* @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
|
|
1287
|
-
* @opts certificates Embed the signer certificates in the output. Default true.
|
|
1289
|
+
* @opts certificates Embed the signer certificates in the output. Default true. A value other
|
|
1290
|
+
* than `true`, `false`, `null` or an absent option is refused
|
|
1291
|
+
* `cms/bad-input`.
|
|
1288
1292
|
* @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
|
|
1289
1293
|
* @opts unsignedAttributes Unsigned attributes for every SignerInfo, each `{ type, values }`
|
|
1290
1294
|
* (`type` an OID name or dotted string, `values` an array of DER value
|
|
@@ -1341,12 +1345,15 @@ var sign = cmsSign.sign;
|
|
|
1341
1345
|
* instead of the primary signature (a nested countersignature).
|
|
1342
1346
|
* @opts signedAttributes Include signed attributes (message-digest + signing-time; never a
|
|
1343
1347
|
* content-type). Default true; false signs the target signature octets
|
|
1344
|
-
* directly.
|
|
1348
|
+
* directly. A value other than `true`, `false`, `null` or an absent
|
|
1349
|
+
* option is refused `cms/bad-input`.
|
|
1345
1350
|
* @opts signingTime A `Date` for the countersignature's signing-time, or false to omit it.
|
|
1346
1351
|
* @opts additionalSignedAttributes Extra signed attributes on the countersignature (a content-type
|
|
1347
1352
|
* is rejected, RFC 5652 sec. 11.4).
|
|
1348
1353
|
* @opts sid `"issuerAndSerial"` (default) or `"ski"`.
|
|
1349
|
-
* @opts certificates Embed the countersigner certificate(s) in the SignedData. Default
|
|
1354
|
+
* @opts certificates Embed the countersigner certificate(s) in the SignedData. Default
|
|
1355
|
+
* true. A value other than `true`, `false`, `null` or an absent option
|
|
1356
|
+
* is refused `cms/bad-input`.
|
|
1350
1357
|
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` attribute on the
|
|
1351
1358
|
* countersignature, copying ITS own digest and signature algorithm
|
|
1352
1359
|
* identifiers rather than the countersigned signature's. Off by
|
|
@@ -1440,7 +1447,8 @@ var encrypt = cmsEncrypt.encrypt;
|
|
|
1440
1447
|
* @opts digestAlgorithm The authenticated-attributes message-digest hash (defaults to the MAC hash).
|
|
1441
1448
|
* @opts authenticatedAttributes Include the content-type + message-digest authenticated attributes
|
|
1442
1449
|
* (the default, and required for a non-`data` content type); false MACs the
|
|
1443
|
-
* content directly.
|
|
1450
|
+
* content directly. A value other than `true`, `false`, `null` or an absent
|
|
1451
|
+
* option is refused `cms/bad-input`.
|
|
1444
1452
|
* @opts authAttrs Extra authenticated attributes appended to the content-type + message-digest pair.
|
|
1445
1453
|
* @opts contentType The encapsulated content type (an OID name). Default `data`.
|
|
1446
1454
|
* @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
package/lib/csr-sign.js
CHANGED
|
@@ -172,7 +172,11 @@ function _relatedCertRequest(spec) {
|
|
|
172
172
|
* subject-owned set `pki.x509.sign` takes, or an array of pre-encoded Extension DER) and
|
|
173
173
|
* `challengePassword`. An extension the issuing CA assigns (authorityKeyIdentifier, the
|
|
174
174
|
* certificate-transparency pair, the CA version and previous-certificate hash, ocspNoCheck) is not a
|
|
175
|
-
* request and is refused by name. `
|
|
175
|
+
* request and is refused by name. The per-extension criticality flags `pki.x509.sign` takes
|
|
176
|
+
* (`keyUsageCritical`, default true, and `extendedKeyUsageCritical`,
|
|
177
|
+
* `certificatePoliciesCritical`, `policyMappingsCritical`, `qcStatementsCritical`,
|
|
178
|
+
* `tlsFeatureCritical`, each default false) apply here too; each is a boolean, and a value other
|
|
179
|
+
* than `true`, `false`, `null` or an absent flag is refused `csr/bad-input`. `key` (or
|
|
176
180
|
* `{ key }`) is the subject's own PKCS#8 private key, WebCrypto CryptoKey, or signer
|
|
177
181
|
* `{ algorithm, publicKey, sign }`, so the request is self-signed
|
|
178
182
|
* to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
|
package/lib/est.js
CHANGED
|
@@ -173,6 +173,17 @@ function _knownOpts(opts, known, verb) {
|
|
|
173
173
|
Object.keys(known).sort().join(", ");
|
|
174
174
|
});
|
|
175
175
|
_assertTransport(settled);
|
|
176
|
+
/** @internal The two switches these verbs document, typed here because this is the one door every
|
|
177
|
+
* one of them passes through and it runs before any request goes out. Both are read further down
|
|
178
|
+
* by truthiness: `allowCrossOriginRedirect: "false"` is truthy, so it ALLOWED the cross-origin
|
|
179
|
+
* redirect on an unsafe method that the check exists to refuse (sec. 3.2.1), and
|
|
180
|
+
* `requestedEncryption: "false"` is read as "encryption was requested", which skips the
|
|
181
|
+
* sec. 4.4.2 check that an encrypted key part was advertised. A verb that does not accept one of
|
|
182
|
+
* them never sees a value for it. */
|
|
183
|
+
guard.identifier.booleanOption(settled.allowCrossOriginRedirect, E, "est/bad-input",
|
|
184
|
+
"pki.est." + verb + " opts.allowCrossOriginRedirect");
|
|
185
|
+
guard.identifier.booleanOption(settled.requestedEncryption, E, "est/bad-input",
|
|
186
|
+
"pki.est." + verb + " opts.requestedEncryption");
|
|
176
187
|
return settled;
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -1643,6 +1654,8 @@ function _enroll(op, baseUrl, csrInput, opts, proxySnap) {
|
|
|
1643
1654
|
* There is no `"auto"`: the scheme is chosen here, not by whatever a server offers. `username` / `password`
|
|
1644
1655
|
* at the top level are the older form and mean Basic. Answered only after the transport authenticated the server.
|
|
1645
1656
|
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on an unsafe method.
|
|
1657
|
+
* A value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
|
|
1658
|
+
* before the request goes out.
|
|
1646
1659
|
* @example
|
|
1647
1660
|
* // a live CA uses the default pki.transport.https; here an injected transport returns a canned bag
|
|
1648
1661
|
* var r = await pki.est.cacerts("https://ca.example",
|
|
@@ -1695,7 +1708,9 @@ function cacerts(baseUrl, opts) {
|
|
|
1695
1708
|
* - `transport` / `tls` / `label` / `timeout` / `maxResponseBytes` / `maxRedirects` / `proxy` / `now`, as in pki.est.cacerts.
|
|
1696
1709
|
* - `strict`: reject an enroll response that carries more than the single issued certificate.
|
|
1697
1710
|
* - `username` / `password` / `auth` -- HTTP credentials: top-level `username` / `password` mean Basic; `auth` (`{ scheme: "basic" | "digest", username, password, ... }`, as in pki.est.cacerts) selects Basic or Digest. Answered only after server authorization (empty username allowed).
|
|
1698
|
-
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST.
|
|
1711
|
+
* - `allowCrossOriginRedirect` -- opt in to following a cross-origin redirect on this POST. A
|
|
1712
|
+
* value other than `true`, `false`, `null` or an absent option is refused `est/bad-input`
|
|
1713
|
+
* before the request goes out.
|
|
1699
1714
|
* @example
|
|
1700
1715
|
* var req = await pki.csr.sign({ subject: "device.example", subjectPublicKey: signerSpki }, { key: signerKeyPkcs8 });
|
|
1701
1716
|
* // a 202 means the CA queued the request -- the verb surfaces the delay, never sleeps
|
|
@@ -1919,6 +1934,8 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
1919
1934
|
* @opts
|
|
1920
1935
|
* - `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
|
|
1921
1936
|
* overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
|
|
1937
|
+
* `requestedEncryption` is a boolean: a value other than `true`, `false`, `null` or an absent
|
|
1938
|
+
* option is refused `est/bad-input`.
|
|
1922
1939
|
* - every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
|
|
1923
1940
|
* @example
|
|
1924
1941
|
* var r = await pki.est.serverkeygen("https://ca.example", csrDer,
|
|
@@ -1933,7 +1950,11 @@ function serverkeygen(baseUrl, csrInput, opts) {
|
|
|
1933
1950
|
var checkedTransport = opts.transport;
|
|
1934
1951
|
var csrDer = _csrDer(csrInput);
|
|
1935
1952
|
var derived = _serverkeygenEncryptionFromCsr(csrDer);
|
|
1936
|
-
|
|
1953
|
+
/** @internal An override is a value the caller stated, and `null` states nothing: it is the
|
|
1954
|
+
* absent form the switch door accepts, so the CSR's own answer stands. Read as present it
|
|
1955
|
+
* coerced to `false` and contradicted a CSR the caller never disagreed with. The value is a
|
|
1956
|
+
* boolean by the time it reaches here, so it is compared rather than coerced. */
|
|
1957
|
+
if (opts.requestedEncryption != null && opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
|
|
1937
1958
|
if (opts.expectedRecipientKeyId !== undefined) {
|
|
1938
1959
|
if (!Buffer.isBuffer(opts.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId must be a Buffer");
|
|
1939
1960
|
if (derived.expectedRecipientKeyId && !opts.expectedRecipientKeyId.equals(derived.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId contradicts the key identifier the CSR advertised (RFC 7030 sec. 4.4.1)");
|
package/lib/guard-identifier.js
CHANGED
|
@@ -743,7 +743,32 @@ function assertCallableOption(opts, key, E, code, label, shape) {
|
|
|
743
743
|
// @enforced-by behavioral -- guard: each drives the shipped verb with a non-boolean and asserts the
|
|
744
744
|
// @enforced-by behavioral -- typed refusal, and with false and absence to pin the two off forms.
|
|
745
745
|
function booleanOption(value, E, code, label) {
|
|
746
|
-
|
|
746
|
+
return _switchValue(value, false, E, code, label);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** @internal The same read for a switch whose absent form is ON: an option the caller turns OFF,
|
|
750
|
+
* where the documented default is the state an omitted option selects. The two differ only in what
|
|
751
|
+
* absence means, so the type rule is written once and each boundary names the default it documents.
|
|
752
|
+
* A boundary comparing such an option to `false` answers for every other value as the default
|
|
753
|
+
* state, which is how `certificates: "false"` embedded the certificate the caller asked to
|
|
754
|
+
* withhold; once the value has been through here, that comparison reads a boolean.
|
|
755
|
+
*
|
|
756
|
+
* @enforced-by behavioral -- the per-verb vectors in coercion-safe-options.test.js drive each
|
|
757
|
+
* @enforced-by behavioral -- shipped verb with a non-boolean and assert the typed refusal names the
|
|
758
|
+
* @enforced-by behavioral -- option, and with true, false and absence to pin the three accepted
|
|
759
|
+
* @enforced-by behavioral -- forms. The comparison this replaces has its own codebase-wide
|
|
760
|
+
* @enforced-by behavioral -- detector, optout-switch-read-by-coercion.
|
|
761
|
+
*/
|
|
762
|
+
function booleanOptionDefaultOn(value, E, code, label) {
|
|
763
|
+
return _switchValue(value, true, E, code, label);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** @internal A switch is the boolean it documents, an absent one is the stated default, and every
|
|
767
|
+
* other value is refused. The value is rendered from its own type rather than coerced, so a caller
|
|
768
|
+
* value with a throwing `toString` reports the typed refusal. `null` is read as absence, the same
|
|
769
|
+
* reading the rest of this module gives it. */
|
|
770
|
+
function _switchValue(value, whenAbsent, E, code, label) {
|
|
771
|
+
if (value === undefined || value === null) return whenAbsent;
|
|
747
772
|
if (value === true || value === false) return value;
|
|
748
773
|
throw E(code, label + " must be true or false, got " + _showSwitch(value));
|
|
749
774
|
}
|
|
@@ -776,6 +801,7 @@ function declaresOwn(obj, name) {
|
|
|
776
801
|
module.exports = intrinsic.freeze({
|
|
777
802
|
assertCallable: assertCallable,
|
|
778
803
|
booleanOption: booleanOption,
|
|
804
|
+
booleanOptionDefaultOn: booleanOptionDefaultOn,
|
|
779
805
|
declaresOwn: declaresOwn,
|
|
780
806
|
assertCallableOption: assertCallableOption,
|
|
781
807
|
assertCanonicalOid: assertCanonicalOid,
|
package/lib/ocsp.js
CHANGED
|
@@ -316,6 +316,8 @@ function _normCertDer(cert, what) {
|
|
|
316
316
|
* extendedRevoke emit the id-pkix-ocsp-extended-revoke extension (RFC 6960 sec. 4.4.8); implied
|
|
317
317
|
* by a non-issued response, where `false` is refused.
|
|
318
318
|
* embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
|
|
319
|
+
* For both switches, a value other than `true`, `false`, `null` or an absent
|
|
320
|
+
* option is refused `ocsp/bad-input`.
|
|
319
321
|
* pem emit a PEM `OCSP RESPONSE` string instead of DER.
|
|
320
322
|
* @example
|
|
321
323
|
* var ca = await pki.key.generate("Ed25519");
|
|
@@ -350,6 +352,12 @@ function _sign(responseData, responder, opts) {
|
|
|
350
352
|
opts = guard.identifier.optionsObject(opts, _err, "ocsp/bad-input", "pki.ocsp.sign options");
|
|
351
353
|
guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "ocsp/bad-input",
|
|
352
354
|
"pki.ocsp.sign has an unknown option (the nonce to echo back is `nonce`): ");
|
|
355
|
+
/** @internal Both switches are read below against one boolean each, which answered for a value
|
|
356
|
+
* of any other type as the opposite of what the caller asked: `embedCert: "false"` carried the
|
|
357
|
+
* responder certificate a direct-CA response leaves out, and `extendedRevoke: "false"` placed
|
|
358
|
+
* the extension while escaping the sec. 2.2 refusal that reads the same option. */
|
|
359
|
+
guard.identifier.booleanOptionDefaultOn(opts.embedCert, _err, "ocsp/bad-input", "pki.ocsp.sign opts.embedCert");
|
|
360
|
+
guard.identifier.booleanOption(opts.extendedRevoke, _err, "ocsp/bad-input", "pki.ocsp.sign opts.extendedRevoke");
|
|
353
361
|
responseData = responseData || _ocspIntrinsic.create(null);
|
|
354
362
|
if (typeof responseData !== "object") throw _err("ocsp/bad-input", "the responseData must be an object { responderID, responses, producedAt? }");
|
|
355
363
|
guard.identifier.assertKnownKeys(responseData, _RESPONSE_DATA_KEYS, _err, "ocsp/bad-input", "unknown responseData field (accepted: responderID, responses, producedAt): ");
|
package/lib/path-validate.js
CHANGED
|
@@ -2147,7 +2147,9 @@ function selectDelta(candidates) {
|
|
|
2147
2147
|
* revocationDate.
|
|
2148
2148
|
*
|
|
2149
2149
|
* @opts
|
|
2150
|
-
* useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
|
|
2150
|
+
* useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)). Default `true`;
|
|
2151
|
+
* a value other than `true`, `false`, `null` or an absent option is refused
|
|
2152
|
+
* `path/bad-input`.
|
|
2151
2153
|
* Default true. When false a delta is never merged; it is
|
|
2152
2154
|
* still consulted for revocation.
|
|
2153
2155
|
*
|
|
@@ -2171,7 +2173,10 @@ function crlChecker(crls, opts) {
|
|
|
2171
2173
|
return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
|
|
2172
2174
|
"crlChecker: crls[" + ci + "]");
|
|
2173
2175
|
});
|
|
2174
|
-
|
|
2176
|
+
/** @internal The sec. 6.3.1(b) merge, read as the boolean it documents: the comparison this
|
|
2177
|
+
* replaces answered for a value of any other type by merging deltas the caller turned off. */
|
|
2178
|
+
var useDeltas = guard.identifier.booleanOptionDefaultOn(opts && opts.useDeltas, E,
|
|
2179
|
+
"path/bad-input", "crlChecker: opts.useDeltas");
|
|
2175
2180
|
return {
|
|
2176
2181
|
check: async function (cert, issuer, ctx) {
|
|
2177
2182
|
var time = ctx.time;
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -532,7 +532,7 @@ function _normalizeSpec(spec, opts, form) {
|
|
|
532
532
|
* @opts
|
|
533
533
|
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
534
534
|
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, password? }`. `password` overrides the shared integrity password for the MAC. `keyLength` is a `pbmac1` field: the classic Appendix B derivation produces a key at the hash's own output length, and naming one under `hmac` is refused.
|
|
535
|
-
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
|
|
535
|
+
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected. `certificates` embeds the signer certificates in the authSafe, default true; a value other than `true`, `false`, `null` or an absent option is refused `pkcs12/bad-input`.
|
|
536
536
|
* - `recipientCerts` -- (public-key privacy convenience) `[certDer|PEM, ...]`; the convenience-form cert + key are placed in one recipient-enveloped safe (a plaintext keyBag). For the full form, set per-`safeContents` `recipients: [{ cert }, ...]` -- CERTIFICATE recipients only (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM, dispatched off the cert key); a password or KEK recipient is not public-key privacy and is rejected -- with an optional `contentEncryptionAlgorithm` (`aes-128|192|256-cbc`, default 256; GCM/AEAD rejected). `encrypt` (password) and `recipients` (public-key) on the same safe is rejected; privacy is independent of integrity.
|
|
537
537
|
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
538
538
|
* @example
|
|
@@ -572,6 +572,11 @@ async function _build(spec, opts) {
|
|
|
572
572
|
if (opts.integrity != null) {
|
|
573
573
|
if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
|
|
574
574
|
guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
|
|
575
|
+
/** @internal Forwarded to `pki.cms.sign` below by comparing it to `false`, which answered for a
|
|
576
|
+
* value of any other type by embedding the signer certificates the caller asked to leave out
|
|
577
|
+
* of the authSafe. */
|
|
578
|
+
guard.identifier.booleanOptionDefaultOn(opts.integrity.certificates, _err, "pkcs12/bad-input",
|
|
579
|
+
"opts.integrity.certificates");
|
|
575
580
|
if (opts.integrity.mode !== "public-key") {
|
|
576
581
|
throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + guard.text.showValue(opts.integrity.mode));
|
|
577
582
|
}
|
package/lib/pki-build.js
CHANGED
|
@@ -648,6 +648,36 @@ function makeBuilder(ctx) {
|
|
|
648
648
|
if (bc.pathLen != null) pathLen(bc.pathLen);
|
|
649
649
|
guard.identifier.assertKnownKeys(bc, BC_KEYS, E, "bad-input", "unknown basicConstraints field ");
|
|
650
650
|
}
|
|
651
|
+
|
|
652
|
+
/** @internal The criticality a caller states for an extension, read as the boolean each flag
|
|
653
|
+
* documents and resolved to the default an omitted flag selects. RFC 5280 sec. 4.2 fixes
|
|
654
|
+
* keyUsage critical and leaves the other five non-critical, which is why the defaults differ.
|
|
655
|
+
* `basicConstraints` carries its own flag inside the value and is typed by `validateBcSpec`.
|
|
656
|
+
*
|
|
657
|
+
* The flags are RETURNED, so the emitters below read criticality from this record rather than
|
|
658
|
+
* from the caller's spec a second time. A flag read off the spec at the point of emission is a
|
|
659
|
+
* read of a value that was never typed: measured, `extendedKeyUsageCritical: "false"` emitted
|
|
660
|
+
* that extension critical, and an extension a relying party does not recognize as critical is
|
|
661
|
+
* one RFC 5280 sec. 6.1 requires it to reject. */
|
|
662
|
+
var CRITICALITY_FLAGS = [
|
|
663
|
+
["keyUsageCritical", true],
|
|
664
|
+
["extendedKeyUsageCritical", false],
|
|
665
|
+
["certificatePoliciesCritical", false],
|
|
666
|
+
["policyMappingsCritical", false],
|
|
667
|
+
["qcStatementsCritical", false],
|
|
668
|
+
["tlsFeatureCritical", false],
|
|
669
|
+
];
|
|
670
|
+
function criticality(extSpec, label) {
|
|
671
|
+
var out = intrinsic.create(null);
|
|
672
|
+
var where = label || "extensions";
|
|
673
|
+
for (var i = 0; i < CRITICALITY_FLAGS.length; i++) {
|
|
674
|
+
var k = CRITICALITY_FLAGS[i][0];
|
|
675
|
+
out[k] = CRITICALITY_FLAGS[i][1]
|
|
676
|
+
? guard.identifier.booleanOptionDefaultOn(extSpec[k], E, "bad-input", where + "." + k)
|
|
677
|
+
: guard.identifier.booleanOption(extSpec[k], E, "bad-input", where + "." + k);
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
651
681
|
function extBasicConstraints(spec) {
|
|
652
682
|
var children = [];
|
|
653
683
|
if (spec.cA === true) _push(children,b.boolean(true));
|
|
@@ -1368,16 +1398,17 @@ function makeBuilder(ctx) {
|
|
|
1368
1398
|
guard.identifier.assertKnownKeys(extSpec, REQ_EXT_KEYS, E, "bad-input", function (k) {
|
|
1369
1399
|
return "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension";
|
|
1370
1400
|
});
|
|
1401
|
+
var crit = criticality(extSpec, "requested extensions");
|
|
1371
1402
|
var out = [];
|
|
1372
1403
|
if (extSpec.subjectKeyIdentifier != null) {
|
|
1373
1404
|
if (extSpec.subjectKeyIdentifier === true && spki == null) throw E("bad-input", "subjectKeyIdentifier auto-derive (true) requires the public key -- supply a Buffer key id, or include the public key");
|
|
1374
1405
|
_push(out,ext(O("subjectKeyIdentifier"), false, extSki(skiKeyId(extSpec.subjectKeyIdentifier, spki))));
|
|
1375
1406
|
}
|
|
1376
|
-
if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"),
|
|
1377
|
-
if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"),
|
|
1407
|
+
if (extSpec.keyUsage != null) _push(out,ext(O("keyUsage"), crit.keyUsageCritical, extKeyUsage(extSpec.keyUsage)));
|
|
1408
|
+
if (extSpec.extendedKeyUsage != null) _push(out,ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
1378
1409
|
if (extSpec.basicConstraints != null) { validateBcSpec(extSpec.basicConstraints); _push(out,ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, extBasicConstraints(extSpec.basicConstraints))); }
|
|
1379
1410
|
if (extSpec.subjectAltName != null) _push(out,ext(O("subjectAltName"), false, extSan(extSpec.subjectAltName)));
|
|
1380
|
-
if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"),
|
|
1411
|
+
if (extSpec.certificatePolicies != null) _push(out,ext(O("certificatePolicies"), crit.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
|
|
1381
1412
|
/** @internal RFC 5280 sec. 4.2.1.10 fixes this extension critical, in a request as in the certificate. */
|
|
1382
1413
|
if (extSpec.nameConstraints != null) guard.list.append(out, ext(O("nameConstraints"), true, extNameConstraints(extSpec.nameConstraints)));
|
|
1383
1414
|
if (extSpec.authorityInfoAccess != null) guard.list.append(out, ext(O("authorityInfoAccess"), false, extAuthorityInfoAccess(extSpec.authorityInfoAccess)));
|
|
@@ -1385,14 +1416,14 @@ function makeBuilder(ctx) {
|
|
|
1385
1416
|
if (extSpec.freshestCRL != null) guard.list.append(out, ext(O("freshestCRL"), false, extCrlDistributionPoints(extSpec.freshestCRL, "freshestCRL")));
|
|
1386
1417
|
if (extSpec.policyConstraints != null) guard.list.append(out, ext(O("policyConstraints"), true, extPolicyConstraints(extSpec.policyConstraints)));
|
|
1387
1418
|
if (extSpec.inhibitAnyPolicy != null) guard.list.append(out, ext(O("inhibitAnyPolicy"), true, extInhibitAnyPolicy(extSpec.inhibitAnyPolicy)));
|
|
1388
|
-
if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"),
|
|
1419
|
+
if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), crit.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
|
|
1389
1420
|
if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
|
|
1390
|
-
if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"),
|
|
1421
|
+
if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), crit.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
|
|
1391
1422
|
/** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
|
|
1392
1423
|
* sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
|
|
1393
1424
|
* belongs in this table. The criticality knob is here too, because a request carries the
|
|
1394
1425
|
* Extension shape whole and the CA decides what it issues. */
|
|
1395
|
-
if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"),
|
|
1426
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), crit.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
|
|
1396
1427
|
if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
|
|
1397
1428
|
if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
|
|
1398
1429
|
if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
|
|
@@ -1457,6 +1488,7 @@ function makeBuilder(ctx) {
|
|
|
1457
1488
|
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
|
|
1458
1489
|
requestedExtensions: requestedExtensions,
|
|
1459
1490
|
extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
|
|
1491
|
+
criticality: criticality,
|
|
1460
1492
|
extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki, AKI_KEYS: AKI_KEYS,
|
|
1461
1493
|
extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
|
|
1462
1494
|
extSubjectInfoAccess: extSubjectInfoAccess, extSubjectDirectoryAttributes: extSubjectDirectoryAttributes,
|
package/lib/revocation-fetch.js
CHANGED
|
@@ -670,7 +670,9 @@ function _deltaUrlsFor(parsedCrl, state) {
|
|
|
670
670
|
* of the same shape. A transport that does not declare that it blocks
|
|
671
671
|
* private addresses is refused a hostname.
|
|
672
672
|
* @opts allowPlaintextHttp Whether an `http` URL is fetched. Default `true`.
|
|
673
|
-
* @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`.
|
|
673
|
+
* @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`. A
|
|
674
|
+
* value other than `true`, `false`, `null` or an absent option is refused
|
|
675
|
+
* `path/bad-input`.
|
|
674
676
|
* @opts nonce Whether an OCSP request carries a 32-octet nonce. `true` or `false`,
|
|
675
677
|
* and anything else is `path/bad-input` rather than the default, since a
|
|
676
678
|
* control read as off is one an operator asked for and did not get.
|
|
@@ -709,7 +711,11 @@ function fetchingChecker(opts) {
|
|
|
709
711
|
* door is the value the rule is applied on. */
|
|
710
712
|
net: intrinsic.freeze({ allowHttp: allowPlaintextHttp, guardsAddresses: _declaresAddressGuard(transport) }),
|
|
711
713
|
allowPlaintextHttp: allowPlaintextHttp,
|
|
712
|
-
|
|
714
|
+
/** @internal Read the way `nonce` on the next line has been read since it was written: the
|
|
715
|
+
* comparison this replaces answered for a value of any other type by fetching the deltas the
|
|
716
|
+
* caller declined. */
|
|
717
|
+
useDeltas: guard.identifier.booleanOptionDefaultOn(opts.useDeltas, E, "path/bad-input",
|
|
718
|
+
"fetchingChecker: opts.useDeltas"),
|
|
713
719
|
nonce: _booleanOption(opts.nonce, "fetchingChecker: opts.nonce"),
|
|
714
720
|
tls: opts.tls,
|
|
715
721
|
maxFetches: guard.limits.cap(opts.maxFetches, "fetchingChecker: opts.maxFetches", LIMITS.REVOCATION_MAX_FETCHES,
|
package/lib/scep.js
CHANGED
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
* the message layer, transport-agnostic; a live SCEP CA is driven over
|
|
29
29
|
* pki.transport separately.
|
|
30
30
|
*
|
|
31
|
+
* Every verb that reaches a CA accepts the same transport bounds, listed on
|
|
32
|
+
* each of them. One of those bounds is a switch: `allowCrossOriginRedirect`
|
|
33
|
+
* opts in to following a cross-origin redirect on an unsafe method. It is a
|
|
34
|
+
* boolean, and a value other than `true`, `false`, `null` or an absent option
|
|
35
|
+
* is refused `scep/bad-input` before the first request goes out.
|
|
36
|
+
*
|
|
31
37
|
* @card
|
|
32
38
|
* Build and read SCEP (RFC 8894) messages: a PKCSReq, RenewalReq, CertPoll,
|
|
33
39
|
* GetCert, or GetCRL request, or a CA-side CertRep response, wrapped in the
|
|
@@ -731,7 +737,11 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
731
737
|
var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
|
|
732
738
|
var proxySnap = _snapshotProxy(opts.proxy);
|
|
733
739
|
var tlsBase = _tlsForRequest(opts);
|
|
734
|
-
|
|
740
|
+
/** @internal Read as the boolean it documents, on the one path every network verb here takes and
|
|
741
|
+
* before the first request. Taken by truthiness, `allowCrossOriginRedirect: "false"` ALLOWED the
|
|
742
|
+
* cross-origin redirect on an unsafe method that the check below exists to refuse. */
|
|
743
|
+
var allowCrossOrigin = guard.identifier.booleanOption(opts.allowCrossOriginRedirect, _err,
|
|
744
|
+
"scep/bad-input", "opts.allowCrossOriginRedirect");
|
|
735
745
|
function _hopTls(current) {
|
|
736
746
|
var t = tlsBase;
|
|
737
747
|
if (_urlOrigin(current) !== initialOrigin) {
|
package/lib/smime.js
CHANGED
|
@@ -76,12 +76,23 @@ var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
|
|
|
76
76
|
/** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
|
|
77
77
|
* decide whether a rule applies and reads it again for the value the rule uses, and an option
|
|
78
78
|
* reached through an accessor answers each read separately. Callers assign the return value. */
|
|
79
|
+
/** @internal Header protection is turned on below by comparing this option to `true`, so a value
|
|
80
|
+
* of any other type sent the message with the RFC 9788 protection the caller asked for left off.
|
|
81
|
+
* Typed at the one door every verb here passes through, which is before the entity is built. */
|
|
82
|
+
function _assertSmimeSwitches(opts, verb) {
|
|
83
|
+
guard.identifier.booleanOption(opts.protectHeaders, _err, "smime/bad-input",
|
|
84
|
+
"pki.smime." + verb + " opts.protectHeaders");
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
function _knownOpts(opts, known, verb) {
|
|
80
88
|
guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
|
|
81
89
|
return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
|
|
82
90
|
Object.keys(known).sort().join(", ");
|
|
83
91
|
});
|
|
84
|
-
|
|
92
|
+
/** @internal Typed on the snapshot, which is the object the verb goes on to read. */
|
|
93
|
+
var settled = guard.identifier.snapshotOptions(opts, known);
|
|
94
|
+
_assertSmimeSwitches(settled, verb);
|
|
95
|
+
return settled;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
function _isPkcs7(type, kind) {
|
|
@@ -654,7 +665,7 @@ function _base64Body(der) {
|
|
|
654
665
|
* @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
|
|
655
666
|
* @opts contentType the wrapped entity's Content-Type (default `text/plain; charset=utf-8`).
|
|
656
667
|
* @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
|
|
657
|
-
* @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers.
|
|
668
|
+
* @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
|
|
658
669
|
* @opts headers the Non-Structural fields to protect + display: an object `{ Name: value }` or an array `[{ name, value }]` (Subject / From / To / Date / ...); used with `protectHeaders`.
|
|
659
670
|
* @opts hcp the Header Confidentiality Policy applied to the OUTER display copies: `"hcp_baseline"` (default) or `"hcp_no_confidentiality"`. A signed message's payload is not encrypted, so this governs presentation, not secrecy; used with `protectHeaders`.
|
|
660
671
|
* @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
@@ -875,7 +886,7 @@ function _cmsEncryptOpts(opts) {
|
|
|
875
886
|
*
|
|
876
887
|
* @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
|
|
877
888
|
* @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
|
|
878
|
-
* @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside.
|
|
889
|
+
* @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
|
|
879
890
|
* @opts headers the Non-Structural fields to protect (object `{ Name: value }` or array `[{ name, value }]`); the real values, hidden by the HCP.
|
|
880
891
|
* @opts hcp the Header Confidentiality Policy: `"hcp_baseline"` (the default, obscuring Subject and removing Comments/Keywords) or `"hcp_no_confidentiality"` (leave all outer values visible). Per RFC 9788 sec. 3.2.1 / sec. 11.4, `hcp_baseline` deliberately does not strip `Bcc` (removing it can break deliverability to a Bcc'd recipient); to keep a blind recipient out of the plaintext outer headers, omit `Bcc` from `opts.headers`.
|
|
881
892
|
* @opts contentEncryptionAlgorithm forwarded to cms.encrypt: `"aes-256-gcm"` (default) / `"aes-128-gcm"` / `"aes-256-cbc"` / `"aes-128-cbc"`.
|
package/lib/tsp-sign.js
CHANGED
|
@@ -123,7 +123,9 @@ function _signingCertV2(certDer, hashName) {
|
|
|
123
123
|
* @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
|
|
124
124
|
* @opts accuracy The genTime +/- accuracy, as `{ seconds?, millis?, micros? }`, naming at
|
|
125
125
|
* least one field; omit it to make no accuracy claim.
|
|
126
|
-
* @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
|
|
126
|
+
* @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean). A value
|
|
127
|
+
* other than `true`, `false`, `null` or an absent option is refused
|
|
128
|
+
* `tsp/bad-input`.
|
|
127
129
|
* @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
|
|
128
130
|
* @opts digestAlgorithm / pss Passed through to `pki.cms.sign` (the signer digest algorithm, RSA-PSS padding).
|
|
129
131
|
* @opts additionalSignedAttributes Extra signed attributes to carry on the token, as `[{ type, values }]` (forwarded to `pki.cms.sign`).
|
|
@@ -163,6 +165,11 @@ function _sign(messageImprint, tsa, opts) {
|
|
|
163
165
|
}
|
|
164
166
|
guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
|
|
165
167
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
|
|
168
|
+
/** @internal The ordering claim is emitted below only for the boolean `true`, so a value of any
|
|
169
|
+
* other type left the TSTInfo saying the opposite of what the caller asked, which is what RFC
|
|
170
|
+
* 3161 sec. 2.4.2 has a relying party read to compare two tokens from this TSA. `certReq` on the
|
|
171
|
+
* request verb has been typed since it was written; this is the same read. */
|
|
172
|
+
guard.identifier.booleanOption(opts.ordering, _err, "tsp/bad-input", "pki.tsp.sign opts.ordering");
|
|
166
173
|
if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
|
|
167
174
|
guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
|
|
168
175
|
return "unknown TSA field " + JSON.stringify(k) +
|
package/lib/x509-sign.js
CHANGED
|
@@ -228,6 +228,10 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
228
228
|
return "unknown extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form for a custom extension";
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
/** @internal The criticality flags, typed at this door and read from the record below: the
|
|
232
|
+
* emitters took them off the caller's spec, where a value that was never a boolean selected a
|
|
233
|
+
* criticality nobody asked for. */
|
|
234
|
+
var crit = _b.criticality(extSpec);
|
|
231
235
|
var bc = extSpec.basicConstraints;
|
|
232
236
|
if (bc != null) _validateBcSpec(bc);
|
|
233
237
|
var caTrue = !!(bc && bc.cA === true);
|
|
@@ -257,14 +261,14 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
257
261
|
var akiF = _akiFields(akiVal, ctx);
|
|
258
262
|
_push(out,_ext(O("authorityKeyIdentifier"), false, _extAki(akiF[0], akiF[1], akiF[2])));
|
|
259
263
|
}
|
|
260
|
-
if (ku != null) _push(out,_ext(O("keyUsage"),
|
|
261
|
-
if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"),
|
|
264
|
+
if (ku != null) _push(out,_ext(O("keyUsage"), crit.keyUsageCritical, _extKeyUsage(ku)));
|
|
265
|
+
if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
262
266
|
if (bc != null) {
|
|
263
267
|
if (bc.cA === true && bc.critical === false) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
|
|
264
268
|
_push(out,_ext(O("basicConstraints"), bc.critical !== false, _extBasicConstraints(bc)));
|
|
265
269
|
}
|
|
266
270
|
if (extSpec.subjectAltName != null) _push(out,_ext(O("subjectAltName"), ctx.subjectEmpty, _extSan(extSpec.subjectAltName)));
|
|
267
|
-
if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"),
|
|
271
|
+
if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), crit.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
|
|
268
272
|
/** @internal Sections 4.2.2.1 and 4.2.1.15 fix these non-critical; sec. 4.2.1.13 states a SHOULD
|
|
269
273
|
* and the toolkit issues the conforming form. */
|
|
270
274
|
if (extSpec.authorityInfoAccess != null) guard.list.append(out, _ext(O("authorityInfoAccess"), false, _extAia(extSpec.authorityInfoAccess)));
|
|
@@ -279,15 +283,15 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
279
283
|
* does not process the extension, so a critical one is a certificate this toolkit's own validator
|
|
280
284
|
* must reject and pki.lint.certificate grades `unknown-critical-extension`. The default is the
|
|
281
285
|
* usable form and the knob reaches the SHOULD-conforming one, as certificatePolicies does. */
|
|
282
|
-
if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"),
|
|
286
|
+
if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), crit.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
|
|
283
287
|
if (extSpec.issuerAltName != null) guard.list.append(out, _ext(O("issuerAltName"), false, _encodeGeneralNames(_reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
|
|
284
288
|
/** @internal pki.path.validate does not process qcStatements, so a critical one is rejected as an
|
|
285
289
|
* unrecognized critical extension; the default is the form that validates and the knob reaches
|
|
286
290
|
* the other, as policyMappings and certificatePolicies do. */
|
|
287
|
-
if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"),
|
|
291
|
+
if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), crit.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
|
|
288
292
|
/** @internal RFC 7633 sec. 4: SHOULD NOT be critical, and the clause names the case where critical
|
|
289
293
|
* is the desired behavior, so the knob exists and the default is the compatible form. */
|
|
290
|
-
if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"),
|
|
294
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), crit.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
|
|
291
295
|
if (extSpec.nameConstraints != null) {
|
|
292
296
|
if (!caTrue) throw _err("x509/bad-input", "nameConstraints appears only in a CA certificate, so it requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.10)");
|
|
293
297
|
guard.list.append(out, _ext(O("nameConstraints"), true, _extNameConstraints(extSpec.nameConstraints)));
|
|
@@ -565,6 +569,14 @@ function _hasCriticalSan(extSpec) {
|
|
|
565
569
|
* (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions
|
|
566
570
|
* (v1 tag, `critical=FALSE`, `cA=FALSE`), and the CA cross-field rules (keyCertSign and
|
|
567
571
|
* pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed `CertificateError`.
|
|
572
|
+
*
|
|
573
|
+
* Criticality is stated per extension: `keyUsageCritical` (default true), and
|
|
574
|
+
* `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`,
|
|
575
|
+
* `qcStatementsCritical` and `tlsFeatureCritical` (each default false). `basicConstraints` carries
|
|
576
|
+
* its own `critical` inside the value. Each is a boolean: a value other than `true`, `false`,
|
|
577
|
+
* `null` or an absent flag is refused `x509/bad-input`. The extensions RFC 5280 fixes one way
|
|
578
|
+
* (`nameConstraints` critical, `authorityInfoAccess` and `cRLDistributionPoints` non-critical) take
|
|
579
|
+
* no flag.
|
|
568
580
|
* Where the spec carries raw DER (a `Name` Buffer, a pre-encoded `Extension`, an issuer `publicKey`
|
|
569
581
|
* SPKI), a structural fault throws `CertificateError`, while a malformed leaf inside those bytes
|
|
570
582
|
* throws `Asn1Error`, the same two-error contract the parsers present.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -87,6 +87,6 @@
|
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"c8": "12.0.0",
|
|
89
89
|
"esbuild": "0.28.2",
|
|
90
|
-
"eslint": "10.
|
|
90
|
+
"eslint": "10.12.0"
|
|
91
91
|
}
|
|
92
92
|
}
|
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:e1f0a0ad-8621-4fc9-a62a-62df66fe4d21",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-12T15:58:58.900Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.9.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.9.5",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.9.
|
|
25
|
+
"version": "0.9.5",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.9.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.9.5",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.9.
|
|
57
|
+
"ref": "@blamejs/pki@0.9.5",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|