@blamejs/pki 0.5.17 → 0.5.18
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 +41 -0
- package/lib/acme.js +6 -4
- package/lib/cmp-build.js +15 -1
- package/lib/cms-encrypt.js +155 -14
- package/lib/cms-sign.js +15 -1
- package/lib/crl-sign.js +61 -2
- package/lib/jose.js +30 -3
- package/lib/pkcs12-build.js +201 -11
- package/lib/tsp-sign.js +64 -0
- package/lib/x509-sign.js +37 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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.5.18 — 2026-08-20
|
|
8
|
+
|
|
9
|
+
Every producing verb refuses an authoring field it does not read, so a misspelling is a refusal at the call instead of a signed artifact that quietly omits what was asked for.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The issuer argument of `pki.x509.sign` and `pki.crl.sign` accepts only the fields the form it selects reads: `{ key, cert }` when an issuing certificate is supplied, `{ key, name, publicKey }` otherwise. A caller who hands over a key-store object carrying anything else must narrow it to one of the two. The check is there because a misspelled `cert` is not otherwise refused on this argument -- the issuing certificate is optional, so a dropped one falls through to self-signing, and a cross-certificate issued that way names its own subject as its issuer. Supplying both forms is refused for the same reason: the certificate branch wins unconditionally and reads neither `name` nor `publicKey`, so a caller who named a different issuer got one issued under the certificate's own distinguished name.
|
|
14
|
+
- The TSA argument of `pki.tsp.sign` accepts only the certificate and key it reads. A misspelling of either was always refused for what it left missing, but that is half the class: a name belonging on the third argument, written on the TSA instead, is read by nothing there. `ordering` placed on the TSA emitted a token the same size as one that never asked for it, with the flag unset, while the caller believed they had set it. A caller passing a key-store object whole must narrow it to `{ cert, key }`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `pki.x509.sign` refuses an unrecognized field on the certificate spec, the issuer and the options. `extension` in place of `extensions` signed a certificate carrying NONE of them: a caller asking for `basicConstraints: { cA: true }` and `keyUsage` got neither, and nothing in the returned certificate said so. An `issuer` nested inside the spec is refused by name as well -- the issuer is the second argument, and nesting it produced a self-signed certificate with no error, which reads as a broken signer rather than a mis-shaped call.
|
|
19
|
+
- `pki.crl.sign` refuses an unrecognized field on the CRL spec, the issuer and the options. The revoked list is `revoked`; the parser reports the same list as `revokedCertificates`, so a caller round-tripping a parsed CRL back into signing reached for that name and got a correctly signed, structurally valid CRL asserting that nothing is revoked. The refusal names the field the producer reads.
|
|
20
|
+
- `pki.pkcs12.build` refuses an unrecognized field on the store spec and the options, and no longer accepts an option belonging to `open` or `verifyMac`. A store that silently omits a certificate or a key looks well formed and opens cleanly; the omission surfaces wherever it is later imported.
|
|
21
|
+
- `pki.pkcs12.build` refuses an unrecognized field on a `safeContents` entry, on a bag, and on the PBE descriptor either of them carries. The safeContents entry is where the privacy directive lives, so a misspelling there was the costliest in the format: `encrypt` misspelled is neither present nor falsy, so it passed the guard that rejects a present-but-falsy directive, no privacy was selected, and that safe went out as plaintext `id-data` -- an unshrouded private-key bag in the clear, inside a PFX whose MAC still verified and which opened without complaint. A misspelled PBE parameter, on either the safe or the bag, silently reverted to the built-in default, so a caller who asked for a stronger KDF or a different cipher got neither. `opts.mac` is checked the same way, and every one of its fields has a default too: a misspelled `iterations` produced a store MAC'd at the shipped iteration count while the caller believed they had raised it, and an array passed as `opts.mac` carried no field any table lists, so it reached the builder as an empty descriptor and produced the default MAC.
|
|
22
|
+
- `pki.pkcs12.build` refuses a spec that mixes its two forms. `safeContents` and the `{ key, cert, ca }` shorthand are alternatives, and the builder returns `safeContents` the moment it is present without ever reading the shorthand fields -- so `{ safeContents: [...], key }` produced a store with the key silently absent.
|
|
23
|
+
- A `safeContents` entry is checked against the privacy branch it selects. `contentEncryptionAlgorithm` is the public-key branch's content cipher and is read by nothing on a password safe, whose cipher comes from `encrypt.cipher`, so an explicit `aes-128-cbc` request on a password safe was accepted and the default used anyway.
|
|
24
|
+
- A certificate recipient of `pki.cms.encrypt` is checked against the fields ITS CERTIFICATE'S KEY ALGORITHM reads. The arms differ: RSA reads `oaepHash` and never `ukm`; ECDH, X25519, X448 and ML-KEM read `ukm` and never `oaepHash`. One table across all three accepted `oaepHash` on an elliptic-curve recipient, where nothing reads it, and wrapped the content key under the arm's own defaults.
|
|
25
|
+
- A bag is checked against the fields ITS OWN TYPE reads, rather than against one table covering every type. `encrypt` is real on a `shroudedKey` bag and read by nothing on a plaintext `key` bag, so a union accepted the combination and emitted the private key as an unencrypted key bag while the caller had supplied an explicit encryption directive and the surrounding MAC gave integrity only.
|
|
26
|
+
- `pki.crl.sign` refuses an unrecognized field on a revoked-certificate entry. Every optional field on an entry is encoded only when present, so `reson` in place of `reason` listed the certificate as revoked carrying no reason code at all -- in a CRL that verifies, where a relying party cannot distinguish it from a revocation published without a reason.
|
|
27
|
+
- `pki.cms.encrypt` refuses an unrecognized field on a recipient descriptor, against a table per recipient type. The fields beyond the selector all have defaults, so `oaepHsh` encrypted the content key under the default SHA-256 OAEP, and a misspelled `iterations`, `salt` or `prf` on a password recipient derived the key at the default work factor. The single descriptor an `EncryptedData` takes is checked per arm as well: its `cek` branch derives nothing, so a work factor handed to it was discarded exactly as a misspelling would be.
|
|
28
|
+
- `pki.tsp.sign` refuses an unrecognized field on the `accuracy` descriptor. Its three fields are all optional, so `milis` emitted an empty Accuracy that reads back as zero seconds, millis and micros, and the token understated the precision the caller asked it to claim.
|
|
29
|
+
- `pki.cms.encrypt` and `pki.cms.authenticate` each refuse an unrecognized option, against separate tables. The two verbs have different option sets, so a single shared table would have accepted either verb's keys and defeated the check.
|
|
30
|
+
- `pki.tsp.sign` refuses an unrecognized field on the message imprint and the options. A one-letter slip -- `odering` for `ordering` -- emitted a token with the ordering flag unset, which is a claim about the timestamp the caller believed they had made.
|
|
31
|
+
- `pki.pkcs12.open` and `pki.pkcs12.verifyMac` each refuse an unrecognized option, against their own tables. `maxIterations` caps the work a hostile store can demand of the key derivation, so a misspelling restored the built-in ceiling and the tighter bound the caller set was never applied.
|
|
32
|
+
- `pki.tsp.request` refuses an unrecognized field on the message imprint as well as on the options -- the same table `pki.tsp.sign` holds its imprint to. The request's own options are the second argument, so `nonce` written on the imprint reached nothing and the request went out with no nonce.
|
|
33
|
+
- `pki.tsp.request` refuses an unrecognized option. Every field it encodes is optional in the request structure, so a misspelled one was dropped without complaint: `nonce` misspelled put a request on the wire carrying no nonce at all, and the client then matched the reply against a replay defense it had never actually asked for.
|
|
34
|
+
- `pki.tsp.response` refuses an unrecognized option. `status` carries the verdict and defaults to granted, so a misspelling did not lose a detail -- it inverted the answer, and a TSA declining a request emitted a granted response over a token it meant to withhold.
|
|
35
|
+
- The options of `pki.pkcs12.build` are checked against the spec form in force. `recipientCerts` selects public-key privacy only where the builder assembles the safes itself, so under the `safeContents` form it selected nothing: a plaintext key bag went into an `id-data` safe with the private key in the clear, while the caller had asked for recipient-enveloped privacy. The full form carries privacy per entry, as `safeContents[i].recipients`.
|
|
36
|
+
- `pki.cms.encrypt` and `pki.cms.authenticate` refuse a per-recipient default that no recipient of the message reads. `oaepHash` is read only by an RSA certificate recipient, `ukm` only by a key-agreement or KEM certificate recipient, and `keyIdentifier` only by a certificate recipient -- so a password-only message given an `oaepHash` described a wrapping it never performed. A recipient that supplies its own value reads that one and never the default, so a default every recipient overrides is refused too; one recipient that does read it is enough. An `EncryptedData` carries no recipients at all and so reads none of the three.
|
|
37
|
+
- `pki.cmp.build` refuses a signature parameter under MAC protection. `pss` and `digestAlgorithm` are read only by the `{ key, cert }` form; supplied alongside `mac`, they emitted a message MAC'd under the default PRF, byte for byte identical to the call that never named them.
|
|
38
|
+
- `pki.jose.sign` refuses `opts.jwk` when the protected header already embeds one. The embedded key wins, so the option is read in kid mode only, and a caller who named a different public JWK beside an embedded one had it discarded.
|
|
39
|
+
- `pki.jose.sign` serializes the protected header once and every check reads back those exact bytes, through the same bounded reader a received JWS goes through. The header is a caller-owned object, so a `toJSON` or an accessor could answer the public-only check and the serializer differently: a JWK whose own members carried no private half but which serialized `d` passed the check and shipped the private key in the header. Serializing runs that code, so the option check now runs before it -- a `toJSON` reaching into the options bag could otherwise delete a misspelled option before the door looked at it. A header that cannot be serialized is refused rather than reaching the signer.
|
|
40
|
+
- `pki.pkcs12.build` decides which of the two spec forms it has once, and the field check and the builder both act on that decision. Held separately the two tests disagreed on a `safeContents` that is present but not a list, so the refusal named a field that was correct instead of the one that was wrong.
|
|
41
|
+
- `pki.cms.encrypt` refuses `authAttrs` under a CBC content algorithm. Authenticated attributes are carried by an `AuthEnvelopedData`, which an AEAD algorithm produces; a CBC algorithm selects `EnvelopedData`, which has no field for them, so the attributes a caller asked to authenticate were dropped and the message went out without them.
|
|
42
|
+
- `pki.cms.authenticate` refuses `digestAlgorithm` and `authAttrs` when `authenticatedAttributes` is `false`. That branch MACs the content octets directly and builds no attribute set, so the digest algorithm that names its hash, and any attribute added to it, are read by nothing.
|
|
43
|
+
- `pem` on an `EncryptedData` descriptor is refused; it is an option, the third argument, as on every other verb here. A second spelling on the descriptor was read by the password arm alone, so a `{ cek, pem: true }` descriptor was accepted and returned DER.
|
|
44
|
+
- `pki.crl.sign` refuses `spec.issuer` when the issuer argument already names one. The distinguished name has three possible sources and `issuer.cert` and `issuer.name` both win outright, so a spec naming one issuer beside a certificate for another produced a CRL, which verifies, issued under the certificate's name. `spec.issuer` is still the name when the issuer argument supplies none.
|
|
45
|
+
- `opts.mac.keyLength` on a `pki.pkcs12.build` store is refused under the classic `hmac` algorithm, which is the default. The Appendix B derivation produces a key at the hash's own output length and reads no key length, so an explicit one was discarded and the store carried the same MAC as a call that never named it. `keyLength` remains a PBMAC1 parameter (RFC 9579).
|
|
46
|
+
- A `pki.pkcs12.build` public-key integrity signer is checked against the form it selects, `{ cert, key }` or `{ spki, keyIdentifier, key }`, plus the signature parameters. Every field beyond the identity has a default, so a misspelled `pss` or `digestAlgorithm` signed the store under PKCS#1 with SHA-256 while the caller had asked for something else, and a store records only what was used. `opts.integrity.signer` and `opts.integrity.signers` are the same setting spelled two ways and the list won outright, so supplying both signed the store under the list alone; exactly one is now required.
|
|
47
|
+
|
|
7
48
|
## v0.5.17 — 2026-08-20
|
|
8
49
|
|
|
9
50
|
A failed integrity check destroys the plaintext it recovered, AuthEnvelopedData validates the attributes it is asked to authenticate, and every refusal names the rule it is actually applying.
|
package/lib/acme.js
CHANGED
|
@@ -694,7 +694,9 @@ var _promised = guard.async.deferred;
|
|
|
694
694
|
function _signOuter(o, payloadObj) {
|
|
695
695
|
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
696
696
|
if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
|
|
697
|
-
|
|
697
|
+
// The header carries the account jwk in jwk mode and the kid in kid mode, so the public JWK is
|
|
698
|
+
// already there whenever there is one to name; jose.sign reads its own opts.jwk in kid mode only.
|
|
699
|
+
return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, profile: "acme-outer" });
|
|
698
700
|
}
|
|
699
701
|
|
|
700
702
|
/**
|
|
@@ -778,7 +780,7 @@ function newAccount(o) {
|
|
|
778
780
|
if (!_isObject(o.externalAccountBinding)) throw E("acme/bad-input", "externalAccountBinding must be an EAB inner JWS object");
|
|
779
781
|
payload.externalAccountBinding = o.externalAccountBinding;
|
|
780
782
|
}
|
|
781
|
-
return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key,
|
|
783
|
+
return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key, profile: "acme-outer" });
|
|
782
784
|
});
|
|
783
785
|
}
|
|
784
786
|
|
|
@@ -1123,7 +1125,7 @@ function revokeCert(o) {
|
|
|
1123
1125
|
}
|
|
1124
1126
|
var header = { alg: o.alg, nonce: o.nonce, url: o.url };
|
|
1125
1127
|
if (hasKid) header.kid = o.kid; else header.jwk = o.jwk;
|
|
1126
|
-
return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key,
|
|
1128
|
+
return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key, profile: "acme-outer" });
|
|
1127
1129
|
});
|
|
1128
1130
|
}
|
|
1129
1131
|
|
|
@@ -1164,7 +1166,7 @@ async function keyChange(o) {
|
|
|
1164
1166
|
var inner = await jose.sign({
|
|
1165
1167
|
protected: { alg: o.newAlg, url: o.url, jwk: o.newJwk },
|
|
1166
1168
|
payload: _payloadBuf({ account: o.account, oldKey: o.oldKey }),
|
|
1167
|
-
key: o.newKey,
|
|
1169
|
+
key: o.newKey, profile: "keychange-inner",
|
|
1168
1170
|
});
|
|
1169
1171
|
return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload: _payloadBuf(inner), key: o.key, profile: "acme-outer" });
|
|
1170
1172
|
}
|
package/lib/cmp-build.js
CHANGED
|
@@ -75,7 +75,13 @@ var BODY_TAG = {
|
|
|
75
75
|
};
|
|
76
76
|
var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
|
|
77
77
|
var CERT_REP_ARM = { ip: 1, cp: 1, kup: 1, ccp: 1 }; // arms carrying a CertRepMessage
|
|
78
|
+
// Protection has two mutually exclusive forms, and the signature parameters are read only by the
|
|
79
|
+
// signature one: under PBMAC1 a requested pss or digestAlgorithm selects nothing and the message
|
|
80
|
+
// goes out MAC'd under the default PRF, byte-identical to the call that never named them. The union
|
|
81
|
+
// stays in force where the form is ambiguous, so _resolveProtection keeps its both/neither verdicts.
|
|
78
82
|
var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
|
|
83
|
+
var KNOWN_OPTS_SIG_KEYS = { key: 1, cert: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
|
|
84
|
+
var KNOWN_OPTS_MAC_KEYS = { mac: 1, extraCerts: 1, pem: 1 };
|
|
79
85
|
var KNOWN_MESSAGE_KEYS = { header: 1, body: 1 };
|
|
80
86
|
var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
|
|
81
87
|
|
|
@@ -565,7 +571,15 @@ function _build(message, opts) {
|
|
|
565
571
|
guard.identifier.assertKnownKeys(message, KNOWN_MESSAGE_KEYS, _err, "cmp/bad-input", function (k) {
|
|
566
572
|
return "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)";
|
|
567
573
|
});
|
|
568
|
-
|
|
574
|
+
var sigForm = opts.key != null || opts.cert != null, macForm = opts.mac != null;
|
|
575
|
+
guard.identifier.assertKnownKeys(opts,
|
|
576
|
+
sigForm === macForm ? KNOWN_OPTS_KEYS : sigForm ? KNOWN_OPTS_SIG_KEYS : KNOWN_OPTS_MAC_KEYS,
|
|
577
|
+
_err, "cmp/bad-input", function (k) {
|
|
578
|
+
return "unknown opts field " + JSON.stringify(k) +
|
|
579
|
+
(macForm && (k === "pss" || k === "digestAlgorithm")
|
|
580
|
+
? " for MAC protection; the signature parameters are read only by the { key, cert } form"
|
|
581
|
+
: "");
|
|
582
|
+
});
|
|
569
583
|
if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
|
|
570
584
|
if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
|
|
571
585
|
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -364,22 +364,91 @@ var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA38
|
|
|
364
364
|
function _prfHash(prf) { if (!PRF_HASH[prf]) throw _err("cms/bad-input", "unsupported pwri prf " + JSON.stringify(prf)); return PRF_HASH[prf]; }
|
|
365
365
|
|
|
366
366
|
// ---- recipient dispatch ----------------------------------------------------
|
|
367
|
-
|
|
367
|
+
// The fields a recipient descriptor carries, PER ARM. Each arm reads a disjoint set, and every
|
|
368
|
+
// field beyond the selector is optional with a default, so an unrecognized name is missed by
|
|
369
|
+
// nothing and the recipient is built with the shipped parameters instead: `oaepHsh` for `oaepHash`
|
|
370
|
+
// encrypts the content key under the default SHA-256 OAEP, and a misspelled `iterations`, `salt`
|
|
371
|
+
// or `prf` on a password recipient derives the key at the default work factor. A union table would
|
|
372
|
+
// admit another arm's fields, which the chosen arm never reads.
|
|
373
|
+
var _PWRI_KEYS = { password: 1, iterations: 1, prf: 1, salt: 1 };
|
|
374
|
+
var _KEKRI_KEYS = { kek: 1, kekId: 1 };
|
|
375
|
+
// A certificate recipient dispatches on the certificate's KEY ALGORITHM, and the arms read
|
|
376
|
+
// different fields: ktri (RSA) reads oaepHash and never ukm; kari (ECDH / X25519 / X448) and kemri
|
|
377
|
+
// (ML-KEM) read ukm and never oaepHash. One table across all three accepted `oaepHash` on an EC
|
|
378
|
+
// certificate, where nothing reads it, so the content key was wrapped under the arm's own defaults
|
|
379
|
+
// while the caller had named a hash. Checked after the key algorithm resolves, since that is what
|
|
380
|
+
// selects the arm.
|
|
381
|
+
var _KTRI_RECIPIENT_KEYS = { cert: 1, oaepHash: 1, keyIdentifier: 1 };
|
|
382
|
+
var _AGREE_RECIPIENT_KEYS = { cert: 1, keyIdentifier: 1, ukm: 1 };
|
|
383
|
+
|
|
384
|
+
// The three top-level options that are per-recipient DEFAULTS rather than message-wide settings.
|
|
385
|
+
// Which of them any given message actually consumes depends on the recipient arms it selects, so
|
|
386
|
+
// they are checked against the arms after those resolve, not against a fixed list.
|
|
387
|
+
var _RECIPIENT_DEFAULTS = ["oaepHash", "keyIdentifier", "ukm"];
|
|
388
|
+
|
|
389
|
+
function _assertRecipientKeys(desc, known, arm) {
|
|
390
|
+
guard.identifier.assertKnownKeys(desc, known, _err, "cms/bad-input", function (k) {
|
|
391
|
+
return "unknown field " + JSON.stringify(k) + " on a " + arm +
|
|
392
|
+
" recipient; that field is not read for this recipient type";
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// A default no selected recipient reads is discarded exactly as a misspelling would be: `oaepHash`
|
|
397
|
+
// on a message whose only recipient is a password recipient names a wrapping the message does not
|
|
398
|
+
// perform. Run after every recipient's arm has resolved and BEFORE anything is emitted.
|
|
399
|
+
function _assertDefaultsConsumed(opts, consumed) {
|
|
400
|
+
for (var i = 0; i < _RECIPIENT_DEFAULTS.length; i++) {
|
|
401
|
+
var name = _RECIPIENT_DEFAULTS[i];
|
|
402
|
+
if (opts[name] != null && !consumed[name]) {
|
|
403
|
+
throw _err("cms/bad-input", "opts." + name + " is a per-recipient default and no recipient of this message reads it" +
|
|
404
|
+
(name === "oaepHash" ? " (only an RSA certificate recipient does)"
|
|
405
|
+
: name === "ukm" ? " (only a key-agreement or KEM certificate recipient does)"
|
|
406
|
+
: " (only a certificate recipient does)"));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function _buildRecipient(cek, desc, opts, consumed) {
|
|
368
412
|
if (desc == null || typeof desc !== "object") throw _err("cms/bad-input", "each recipient must be a descriptor object");
|
|
369
|
-
|
|
370
|
-
|
|
413
|
+
// Checked per arm, on the same selector the dispatch below uses, so the table that runs is the
|
|
414
|
+
// one belonging to the arm that will build. `consumed` records which top-level defaults the
|
|
415
|
+
// selected arms read, so a default no recipient consumes can be refused once they all resolve.
|
|
416
|
+
if (desc.password != null) { _assertRecipientKeys(desc, _PWRI_KEYS, "password"); return _buildPwri(cek, desc); }
|
|
417
|
+
if (desc.kek != null) { _assertRecipientKeys(desc, _KEKRI_KEYS, "KEK"); return _buildKekri(cek, desc); }
|
|
371
418
|
if (desc.cert != null) {
|
|
372
419
|
var cert = x509.parse(_normCertDer(desc.cert, "a recipient certificate"));
|
|
373
420
|
var keyOid = cert.subjectPublicKeyInfo.algorithm.oid;
|
|
374
|
-
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss"))
|
|
375
|
-
|
|
376
|
-
|
|
421
|
+
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
422
|
+
_assertRecipientKeys(desc, _KTRI_RECIPIENT_KEYS, "RSA certificate");
|
|
423
|
+
return _buildKtri(cek, cert, mergeOpts(opts, desc, _KTRI_RECIPIENT_KEYS, consumed));
|
|
424
|
+
}
|
|
425
|
+
if (keyOid === O("ecPublicKey") || MONT_KA[keyOid]) {
|
|
426
|
+
_assertRecipientKeys(desc, _AGREE_RECIPIENT_KEYS, "key-agreement certificate");
|
|
427
|
+
return _buildKari(cek, cert, mergeOpts(opts, desc, _AGREE_RECIPIENT_KEYS, consumed));
|
|
428
|
+
}
|
|
429
|
+
if (KEM_WC[keyOid]) {
|
|
430
|
+
_assertRecipientKeys(desc, _AGREE_RECIPIENT_KEYS, "KEM certificate");
|
|
431
|
+
return _buildKemri(cek, cert, mergeOpts(opts, desc, _AGREE_RECIPIENT_KEYS, consumed));
|
|
432
|
+
}
|
|
377
433
|
throw _err("cms/unsupported-algorithm", "unsupported recipient certificate key algorithm " + keyOid);
|
|
378
434
|
}
|
|
379
435
|
throw _err("cms/bad-input", "a recipient needs { cert }, { password }, or { kek, kekId }");
|
|
380
436
|
}
|
|
381
|
-
|
|
382
|
-
|
|
437
|
+
// Each default is taken from the descriptor when it supplies one and from the top-level options
|
|
438
|
+
// otherwise. That one choice is also the record of which top-level defaults the recipient CONSUMED:
|
|
439
|
+
// an arm that does not read the name never consumes it, and neither does one whose descriptor
|
|
440
|
+
// overrode it, so recording it from the arm's table alone would accept a value nothing read.
|
|
441
|
+
function _pickDefault(desc, opts, name, known, consumed, nullish) {
|
|
442
|
+
var supplied = desc[name], fromDesc = nullish ? supplied != null : !!supplied;
|
|
443
|
+
if (!fromDesc && known[name] && consumed && opts[name] != null) consumed[name] = 1;
|
|
444
|
+
return fromDesc ? supplied : opts[name];
|
|
445
|
+
}
|
|
446
|
+
function mergeOpts(opts, desc, known, consumed) {
|
|
447
|
+
return {
|
|
448
|
+
oaepHash: _pickDefault(desc, opts, "oaepHash", known, consumed, false),
|
|
449
|
+
keyIdentifier: _pickDefault(desc, opts, "keyIdentifier", known, consumed, false),
|
|
450
|
+
ukm: _pickDefault(desc, opts, "ukm", known, consumed, true),
|
|
451
|
+
};
|
|
383
452
|
}
|
|
384
453
|
// RecipientInfo CHOICE: ktri untagged; kari [1], kekri [2], pwri [3], ori [4], all IMPLICIT,
|
|
385
454
|
// so the arm's SEQUENCE tag is replaced by the context tag.
|
|
@@ -397,12 +466,46 @@ function _envelopedVersion(recips, hasUnprotected) {
|
|
|
397
466
|
return forcesTwo ? 2 : 0;
|
|
398
467
|
}
|
|
399
468
|
|
|
469
|
+
// The two exported verbs have DISJOINT option sets, so they get separate tables: one shared table
|
|
470
|
+
// would admit every other verb's keys, which is the hole rather than the door. Each is the union of
|
|
471
|
+
// what the verb reads THROUGH ITS DELEGATIONS -- encrypt never names oaepHash, keyIdentifier or ukm
|
|
472
|
+
// itself, but hands opts to the per-recipient builders that do, and to _emit for pem.
|
|
473
|
+
// Authenticated attributes live in an AuthEnvelopedData, which only an AEAD content algorithm
|
|
474
|
+
// produces: a CBC algorithm selects EnvelopedData, which has no field for them and is not handed
|
|
475
|
+
// opts at all. Requested there, they named a protection the message does not carry.
|
|
476
|
+
var _ENCRYPT_OPTS_AEAD = {
|
|
477
|
+
contentEncryptionAlgorithm: 1, contentType: 1, authAttrs: 1, oaepHash: 1, keyIdentifier: 1,
|
|
478
|
+
ukm: 1, pem: 1,
|
|
479
|
+
};
|
|
480
|
+
var _ENCRYPT_OPTS_CBC = {
|
|
481
|
+
contentEncryptionAlgorithm: 1, contentType: 1, oaepHash: 1, keyIdentifier: 1, ukm: 1, pem: 1,
|
|
482
|
+
};
|
|
483
|
+
// authenticatedAttributes: false MACs the content octets directly, and the attribute set is what
|
|
484
|
+
// carries the digest: neither the digest algorithm nor any extra attribute is read on that branch.
|
|
485
|
+
var _AUTHENTICATE_OPTS_ATTRS = {
|
|
486
|
+
macAlgorithm: 1, digestAlgorithm: 1, authenticatedAttributes: 1, authAttrs: 1, contentType: 1,
|
|
487
|
+
keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
|
|
488
|
+
};
|
|
489
|
+
var _AUTHENTICATE_OPTS_BARE = {
|
|
490
|
+
macAlgorithm: 1, authenticatedAttributes: 1, contentType: 1,
|
|
491
|
+
keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
|
|
492
|
+
};
|
|
493
|
+
|
|
400
494
|
async function encrypt(content, recipients, opts) {
|
|
401
495
|
opts = opts || {};
|
|
402
|
-
|
|
496
|
+
// The content algorithm selects the structure, so it is resolved before the door and the table
|
|
497
|
+
// that runs is the one belonging to the structure this call will emit.
|
|
403
498
|
var algName = opts.contentEncryptionAlgorithm || "aes-256-gcm";
|
|
404
499
|
var ca = CONTENT_ALGS[algName];
|
|
405
500
|
if (!ca) throw _err("cms/bad-input", "unsupported contentEncryptionAlgorithm " + JSON.stringify(algName));
|
|
501
|
+
guard.identifier.assertKnownKeys(opts, ca.aead ? _ENCRYPT_OPTS_AEAD : _ENCRYPT_OPTS_CBC, _err, "cms/bad-input",
|
|
502
|
+
function (k) {
|
|
503
|
+
return "pki.cms.encrypt has an unknown option " + JSON.stringify(k) +
|
|
504
|
+
(!ca.aead && k === "authAttrs"
|
|
505
|
+
? " for a CBC content algorithm; authenticated attributes are carried only by an AuthEnvelopedData, which an AEAD algorithm produces"
|
|
506
|
+
: "");
|
|
507
|
+
});
|
|
508
|
+
var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
|
|
406
509
|
var contentType = opts.contentType || "data";
|
|
407
510
|
var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
|
|
408
511
|
|
|
@@ -415,7 +518,9 @@ async function encrypt(content, recipients, opts) {
|
|
|
415
518
|
|
|
416
519
|
if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
|
|
417
520
|
var recips = [];
|
|
418
|
-
|
|
521
|
+
var consumed = {};
|
|
522
|
+
for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(cek, recipients[i], opts, consumed));
|
|
523
|
+
_assertDefaultsConsumed(opts, consumed);
|
|
419
524
|
var riNodes = recips.map(_taggedRecipient);
|
|
420
525
|
|
|
421
526
|
if (ca.aead) return _emit(_authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, recips), "authEnvelopedData", opts);
|
|
@@ -500,8 +605,31 @@ function _authEnvelopedData(contentBytes, cek, ca, contentType, opts, riNodes, r
|
|
|
500
605
|
return b.sequence(kids);
|
|
501
606
|
}
|
|
502
607
|
|
|
608
|
+
// The single descriptor an EncryptedData takes, PER ARM. `cek` and `password` select the arm and
|
|
609
|
+
// the arms read disjoint sets, so a union would admit the other arm's fields: `{ cek, iterations }`
|
|
610
|
+
// supplies a work factor to a branch that derives nothing, and the caller's parameter is discarded
|
|
611
|
+
// exactly as a misspelling would be. `pem` is read after both arms, so it belongs to each.
|
|
612
|
+
// pem selects the output encoding of the whole call and is an OPTION, the third argument, on every
|
|
613
|
+
// other verb here. A second spelling on the descriptor was read by the password arm alone, so the
|
|
614
|
+
// cek arm accepted it and returned DER; there is now one place to ask for PEM.
|
|
615
|
+
var _ENCRYPTED_DATA_CEK_KEYS = { cek: 1 };
|
|
616
|
+
var _ENCRYPTED_DATA_PW_KEYS = { password: 1, iterations: 1, salt: 1, prf: 1 };
|
|
617
|
+
|
|
503
618
|
function _encryptedData(contentBytes, desc, ca, contentType, opts, cek) {
|
|
504
619
|
if (ca.aead) throw _err("cms/bad-input", "EncryptedData supports only CBC content encryption");
|
|
620
|
+
// An EncryptedData carries no RecipientInfos at all, so nothing here reads a per-recipient
|
|
621
|
+
// default. This branch returns before the recipient loop, so it answers for them itself.
|
|
622
|
+
_assertDefaultsConsumed(opts, {});
|
|
623
|
+
// Checked per arm, on the same selector the dispatch below uses, so the table that runs belongs
|
|
624
|
+
// to the branch that will encrypt.
|
|
625
|
+
if (desc != null && typeof desc === "object") {
|
|
626
|
+
var edKeys = desc.cek != null ? _ENCRYPTED_DATA_CEK_KEYS : _ENCRYPTED_DATA_PW_KEYS;
|
|
627
|
+
var edArm = desc.cek != null ? "cek" : "password";
|
|
628
|
+
guard.identifier.assertKnownKeys(desc, edKeys, _err, "cms/bad-input", function (k) {
|
|
629
|
+
return "unknown field " + JSON.stringify(k) + " on a " + edArm +
|
|
630
|
+
" EncryptedData descriptor; that field is not read for this arm";
|
|
631
|
+
});
|
|
632
|
+
}
|
|
505
633
|
var iv = nodeCrypto.randomBytes(16);
|
|
506
634
|
var contentAlgNode, encKey;
|
|
507
635
|
if (desc && desc.cek != null) {
|
|
@@ -537,7 +665,7 @@ function _encryptedDataPbes2(contentBytes, desc, ca, contentType, iv, opts) {
|
|
|
537
665
|
var contentAlg = pbes2.pbes2AlgId(salt, iterations, prf, ca.oid, iv);
|
|
538
666
|
var eci = b.sequence([b.oid(O(contentType)), contentAlg, b.contextPrimitive(0, enc)]);
|
|
539
667
|
var inner = b.sequence([b.integer(0n), eci]);
|
|
540
|
-
return _emit(inner, "encryptedData",
|
|
668
|
+
return _emit(inner, "encryptedData", opts);
|
|
541
669
|
} finally {
|
|
542
670
|
guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key");
|
|
543
671
|
}
|
|
@@ -581,13 +709,22 @@ var SUPPORTED_DIGEST = { sha256: 1, sha384: 1, sha512: 1 };
|
|
|
581
709
|
|
|
582
710
|
async function authenticate(content, recipients, opts) {
|
|
583
711
|
opts = opts || {};
|
|
712
|
+
// The attribute branch is selected once, and the door and the MAC below both act on that one
|
|
713
|
+
// decision, so the table that runs belongs to the branch that will compute the MAC.
|
|
714
|
+
var withAttrs = opts.authenticatedAttributes !== false;
|
|
715
|
+
guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
|
|
716
|
+
_err, "cms/bad-input", function (k) {
|
|
717
|
+
return "pki.cms.authenticate has an unknown option " + JSON.stringify(k) +
|
|
718
|
+
(!withAttrs && (k === "digestAlgorithm" || k === "authAttrs")
|
|
719
|
+
? " with authenticatedAttributes: false; the MAC then covers the content octets directly and no attribute set is built"
|
|
720
|
+
: "");
|
|
721
|
+
});
|
|
584
722
|
var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
|
|
585
723
|
var macName = opts.macAlgorithm || "hmac-sha256";
|
|
586
724
|
var mac = MAC_ALGS[macName];
|
|
587
725
|
if (!mac) throw _err("cms/bad-input", "unsupported macAlgorithm " + JSON.stringify(macName) + " (hmac-sha256/384/512)");
|
|
588
726
|
if (!Array.isArray(recipients) || !recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 9.1)");
|
|
589
727
|
var contentType = opts.contentType || "data";
|
|
590
|
-
var withAttrs = opts.authenticatedAttributes !== false;
|
|
591
728
|
// RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
|
|
592
729
|
if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
|
|
593
730
|
|
|
@@ -596,12 +733,16 @@ async function authenticate(content, recipients, opts) {
|
|
|
596
733
|
var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
|
|
597
734
|
try {
|
|
598
735
|
var recips = [];
|
|
599
|
-
|
|
736
|
+
var consumedAuth = {};
|
|
737
|
+
for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts, consumedAuth));
|
|
738
|
+
_assertDefaultsConsumed(opts, consumedAuth);
|
|
600
739
|
var riNodes = recips.map(_taggedRecipient);
|
|
601
740
|
|
|
602
|
-
var digestName = opts.digestAlgorithm || mac.node;
|
|
603
741
|
var digestAlgTagged = null, authAttrsDer = null, preimage;
|
|
604
742
|
if (withAttrs) {
|
|
743
|
+
// Inside the branch that uses it: the attribute set is what carries the digest, so on the
|
|
744
|
+
// other branch there is no digest to name and opts.digestAlgorithm is refused at the door.
|
|
745
|
+
var digestName = opts.digestAlgorithm || mac.node;
|
|
605
746
|
if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
|
|
606
747
|
// sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
|
|
607
748
|
// SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
|
package/lib/cms-sign.js
CHANGED
|
@@ -43,6 +43,14 @@ var KNOWN_SIGN_OPTS = {
|
|
|
43
43
|
signedAttributes: 1, signingTime: 1, additionalSignedAttributes: 1, unsignedAttributes: 1,
|
|
44
44
|
sid: 1, eContentType: 1, detached: 1, certificates: 1, pem: 1,
|
|
45
45
|
};
|
|
46
|
+
// The SIGNER descriptor has two forms, and every field it reads is named here so a caller that
|
|
47
|
+
// authors one can be held to the form it selects: a certificate signer, and the key-only signer
|
|
48
|
+
// RFC 5272 sec. 3.2 requires for a Full PKI Request, whose identity comes from `spki` +
|
|
49
|
+
// `keyIdentifier` instead. The signature parameters are read by both. Exported for the verbs that
|
|
50
|
+
// take signer descriptors as AUTHORING input and gate them at their own door -- pki.cms.sign takes
|
|
51
|
+
// them straight from its caller, where a bag carrying extra handles is an ordinary shape.
|
|
52
|
+
var KNOWN_SIGNER_CERT_KEYS = { cert: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 };
|
|
53
|
+
var KNOWN_SIGNER_KEY_ONLY_KEYS = { spki: 1, keyIdentifier: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 };
|
|
46
54
|
// Countersign has its own set: it takes signerIndex and countersignatureOf to SELECT which
|
|
47
55
|
// signature it attaches to, and has no content of its own, so eContentType, detached and
|
|
48
56
|
// unsignedAttributes are not among the options it reads.
|
|
@@ -658,4 +666,10 @@ function _countersign(cmsInput, signers, opts) {
|
|
|
658
666
|
// belt-and-suspenders against a future caller reordering the two.
|
|
659
667
|
// * the `crls [1]` preservation branches in `countersign`. pki.cms.sign never emits a crls field,
|
|
660
668
|
// so a store this producer countersigns never carries one to preserve.
|
|
661
|
-
module.exports = {
|
|
669
|
+
module.exports = {
|
|
670
|
+
sign: sign, countersign: countersign,
|
|
671
|
+
// @internal -- the signer-descriptor field tables, for a verb that AUTHORS signer descriptors
|
|
672
|
+
// from its own caller's options and gates them at its own door.
|
|
673
|
+
KNOWN_SIGNER_CERT_KEYS: KNOWN_SIGNER_CERT_KEYS,
|
|
674
|
+
KNOWN_SIGNER_KEY_ONLY_KEYS: KNOWN_SIGNER_KEY_ONLY_KEYS,
|
|
675
|
+
};
|
package/lib/crl-sign.js
CHANGED
|
@@ -72,8 +72,38 @@ var REASON_BY_NAME = {};
|
|
|
72
72
|
Object.keys(CRL_REASON).forEach(function (v) { REASON_BY_NAME[CRL_REASON[v]] = Number(v); });
|
|
73
73
|
|
|
74
74
|
var KNOWN_CRL_EXT_KEYS = { authorityKeyIdentifier: 1, issuingDistributionPoint: 1, deltaCRLIndicator: 1, freshestCRL: 1, authorityInfoAccess: 1 };
|
|
75
|
+
// The three caller-owned objects pki.crl.sign reads. `crlNumber` and `extensions` are read by
|
|
76
|
+
// _buildCrlExtensions, which _sign hands the whole spec to, so they belong here even though _sign
|
|
77
|
+
// never names them -- the table is derived from what the call ACTUALLY reads, including through
|
|
78
|
+
// its delegations, not from the doc block.
|
|
79
|
+
var KNOWN_SPEC_KEYS = {
|
|
80
|
+
issuer: 1, thisUpdate: 1, nextUpdate: 1, revoked: 1, crlNumber: 1, extensions: 1,
|
|
81
|
+
};
|
|
82
|
+
// spec.issuer is the third place an issuer distinguished name can come from, and it is read only
|
|
83
|
+
// when neither issuer.cert nor issuer.name supplies one: both of those win outright. A spec naming
|
|
84
|
+
// one issuer beside a certificate for another was signed under the certificate's.
|
|
85
|
+
var KNOWN_SPEC_KEYS_ISSUER_NAMED = {
|
|
86
|
+
thisUpdate: 1, nextUpdate: 1, revoked: 1, crlNumber: 1, extensions: 1,
|
|
87
|
+
};
|
|
88
|
+
// Two FORMS, exactly as x509-sign has: when `issuer.cert` is present the builder takes the DN and
|
|
89
|
+
// the public key from it and never reads `name` or `publicKey`, so a union signed under the
|
|
90
|
+
// certificate's identity while discarding the one the caller wrote out.
|
|
91
|
+
var KNOWN_ISSUER_CERT_KEYS = { key: 1, cert: 1 };
|
|
92
|
+
var KNOWN_ISSUER_EXPLICIT_KEYS = { key: 1, name: 1, publicKey: 1 };
|
|
93
|
+
var KNOWN_SIGN_OPTS = { digestAlgorithm: 1, pem: 1, pss: 1 };
|
|
94
|
+
|
|
75
95
|
var KNOWN_IDP_KEYS = { fullName: 1, onlyContainsUserCerts: 1, onlyContainsCACerts: 1, onlyContainsAttributeCerts: 1, indirectCRL: 1 };
|
|
76
96
|
|
|
97
|
+
// The fields ONE revoked-certificate entry carries. Every optional one is encoded only when
|
|
98
|
+
// present, so a misspelling is missed by nothing and the entry is signed without it: `reson` for
|
|
99
|
+
// `reason` lists the certificate as revoked with no reasonCode at all, and a relying party reading
|
|
100
|
+
// the CRL cannot tell a caller who meant keyCompromise from one who gave no reason. The entry is
|
|
101
|
+
// where this bites hardest, because the CRL as a whole still verifies. `certificateIssuer` is
|
|
102
|
+
// listed because the builder DOES read it -- to refuse it with its own message.
|
|
103
|
+
var KNOWN_REVOKED_KEYS = {
|
|
104
|
+
serialNumber: 1, revocationDate: 1, reason: 1, invalidityDate: 1, certificateIssuer: 1, extensions: 1,
|
|
105
|
+
};
|
|
106
|
+
|
|
77
107
|
// RFC 5280 sec. 5.2 -- the profile-fixed criticality of each recognized CRL extension. cRLNumber (5.2.3),
|
|
78
108
|
// authorityKeyIdentifier (5.2.1), freshestCRL (5.2.6), and authorityInfoAccess (5.2.7) MUST be non-critical;
|
|
79
109
|
// issuingDistributionPoint (5.2.5) and deltaCRLIndicator (5.2.4) MUST be critical. Enforced on the object
|
|
@@ -312,6 +342,12 @@ function _buildRevoked(entryList, isDelta) {
|
|
|
312
342
|
var anyExt = false, seenSerials = {};
|
|
313
343
|
var entries = entryList.map(function (e, idx) {
|
|
314
344
|
if (!e || typeof e !== "object" || Buffer.isBuffer(e)) throw _err("crl/bad-input", "each revoked entry must be an object");
|
|
345
|
+
// Before any field of the entry is encoded, so an unrecognized name is a refusal rather than an
|
|
346
|
+
// entry signed without what it asked for.
|
|
347
|
+
guard.identifier.assertKnownKeys(e, KNOWN_REVOKED_KEYS, _err, "crl/bad-input", function (k) {
|
|
348
|
+
return "unknown field " + JSON.stringify(k) + " in revoked entry [" + idx +
|
|
349
|
+
"]; an entry is { serialNumber, revocationDate, reason?, invalidityDate?, extensions? }";
|
|
350
|
+
});
|
|
315
351
|
if (e.serialNumber == null) throw _err("crl/bad-input", "revoked entry [" + idx + "] requires a serialNumber");
|
|
316
352
|
var serialTlv = _serialInteger(e.serialNumber);
|
|
317
353
|
// RFC 5280 sec. 5.1.2.6 -- a serial number identifies a certificate uniquely, so a CRL MUST NOT list the
|
|
@@ -399,11 +435,34 @@ function _sign(spec, issuer, opts) {
|
|
|
399
435
|
opts = opts || {};
|
|
400
436
|
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crl/bad-input", "the CRL spec must be an object");
|
|
401
437
|
issuer = issuer || {};
|
|
438
|
+
// Before any field is read. A dropped key here is worse than elsewhere in the toolkit: a CRL
|
|
439
|
+
// asserts what is revoked, so the failure is a correctly signed, structurally valid CRL saying
|
|
440
|
+
// NOTHING is revoked. `revokedCertificates` earns its own hint because it is the name the parser
|
|
441
|
+
// uses for the same list -- a caller round-tripping a parsed CRL back into sign reaches for it
|
|
442
|
+
// first, and got a signed empty list.
|
|
443
|
+
// The issuer form is settled once, before either door, so the spec table matches the source the
|
|
444
|
+
// distinguished name will actually come from.
|
|
445
|
+
var byCert = issuer.cert != null, byName = issuer.name != null;
|
|
446
|
+
guard.identifier.assertKnownKeys(spec, byCert || byName ? KNOWN_SPEC_KEYS_ISSUER_NAMED : KNOWN_SPEC_KEYS,
|
|
447
|
+
_err, "crl/bad-input", function (k) {
|
|
448
|
+
return "unknown CRL spec field " + JSON.stringify(k) +
|
|
449
|
+
(k === "revokedCertificates" ? "; the producer reads `revoked` (the parser reports the same list as `revokedCertificates`)"
|
|
450
|
+
: k === "issuer" ? "; the issuer argument already names one, through " + (byCert ? "issuer.cert" : "issuer.name") +
|
|
451
|
+
", and that is the name the CRL is signed under"
|
|
452
|
+
: "");
|
|
453
|
+
});
|
|
454
|
+
guard.identifier.assertKnownKeys(issuer, byCert ? KNOWN_ISSUER_CERT_KEYS : KNOWN_ISSUER_EXPLICIT_KEYS,
|
|
455
|
+
_err, "crl/bad-input", function (k) {
|
|
456
|
+
return "unknown issuer field " + JSON.stringify(k) + " for the " + (byCert
|
|
457
|
+
? "certificate form; issuer.cert supplies the name and the public key, so they are not read here"
|
|
458
|
+
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
459
|
+
});
|
|
460
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "crl/bad-input", "pki.crl.sign has an unknown option ");
|
|
402
461
|
if (issuer.key == null) throw _err("crl/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
|
|
403
462
|
|
|
404
463
|
// Resolve the issuer name + signing-key SPKI. `{ cert }` supplies both; else `{ name|spec.issuer, publicKey }`.
|
|
405
464
|
var issuerDer, issuerSpki, issuerCert = null;
|
|
406
|
-
if (
|
|
465
|
+
if (byCert) {
|
|
407
466
|
issuerCert = _parseIssuerCert(issuer.cert);
|
|
408
467
|
_assertIssuerCanSignCrl(issuerCert);
|
|
409
468
|
issuerDer = pkiBuild.tbsNameField(issuerCert, "subject");
|
|
@@ -411,7 +470,7 @@ function _sign(spec, issuer, opts) {
|
|
|
411
470
|
} else {
|
|
412
471
|
issuerSpki = _reqDer(issuer.publicKey, "issuer.publicKey (the issuer SPKI DER)");
|
|
413
472
|
_assertValidSpki(issuerSpki, "issuer.publicKey");
|
|
414
|
-
var dnSource =
|
|
473
|
+
var dnSource = byName ? issuer.name : spec.issuer;
|
|
415
474
|
if (dnSource == null) throw _err("crl/bad-issuer", "an issuer distinguished name is required (issuer.name or spec.issuer) when no issuer.cert is given");
|
|
416
475
|
issuerDer = _encodeName(dnSource);
|
|
417
476
|
}
|
package/lib/jose.js
CHANGED
|
@@ -457,7 +457,7 @@ async function verify(jws, opts) {
|
|
|
457
457
|
* payload: Buffer // the raw payload octets ("" for POST-as-GET)
|
|
458
458
|
* key: CryptoKey // the private signing key
|
|
459
459
|
* profile: string // default "acme-outer"
|
|
460
|
-
* jwk: object // the public JWK,
|
|
460
|
+
* jwk: object // the public JWK, in kid mode (a header that embeds jwk supplies its own)
|
|
461
461
|
*
|
|
462
462
|
* @example
|
|
463
463
|
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
@@ -466,11 +466,38 @@ async function verify(jws, opts) {
|
|
|
466
466
|
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
|
|
467
467
|
* var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
|
|
468
468
|
*/
|
|
469
|
+
// The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
|
|
470
|
+
// Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
|
|
471
|
+
// came from the header's key while the caller had named a different one.
|
|
469
472
|
var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
|
|
473
|
+
var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
|
|
470
474
|
async function sign(opts) {
|
|
471
475
|
opts = opts || {};
|
|
476
|
+
// The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
|
|
477
|
+
// -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
|
|
478
|
+
// so a misspelling reached the door already removed and the message went out signed.
|
|
472
479
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
|
|
473
|
-
|
|
480
|
+
// The header is then serialized ONCE, and every read below comes from a snapshot of those exact
|
|
481
|
+
// bytes: the header validated, the header embedded in the JWS and the key confirmed against the
|
|
482
|
+
// produced signature are then all the same values. Read live, a caller getter answers each of
|
|
483
|
+
// those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
|
|
484
|
+
// the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
|
|
485
|
+
var headerJson;
|
|
486
|
+
try { headerJson = JSON.stringify(opts.protected); }
|
|
487
|
+
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
488
|
+
// Read back through the same bounded reader a received JWS goes through, so a header this side
|
|
489
|
+
// produces is held to the caps and the duplicate-member rule the other side enforces.
|
|
490
|
+
var headerBytes = headerJson === undefined ? null : Buffer.from(headerJson, "utf8");
|
|
491
|
+
var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
|
|
492
|
+
// Which of the two key sources is in force is known only once the header has been read, so the
|
|
493
|
+
// narrower table runs here. It re-walks the bag, which also catches a name that code added.
|
|
494
|
+
var embedsJwk = !!(header && typeof header === "object" && header.jwk);
|
|
495
|
+
if (embedsJwk) {
|
|
496
|
+
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
|
|
497
|
+
return "unknown pki.jose.sign option " + JSON.stringify(k) +
|
|
498
|
+
(k === "jwk" ? "; the protected header already embeds the public JWK" : "");
|
|
499
|
+
});
|
|
500
|
+
}
|
|
474
501
|
var payload = opts.payload;
|
|
475
502
|
if (!Buffer.isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
476
503
|
// Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
|
|
@@ -494,7 +521,7 @@ async function sign(opts) {
|
|
|
494
521
|
// also PUBLISHED to the server, so it must be public-only: an accidentally exported
|
|
495
522
|
// private JWK must never be serialized into the protected header.
|
|
496
523
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
497
|
-
var protectedB64 = b64uEncode(
|
|
524
|
+
var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
|
|
498
525
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
499
526
|
var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
|
|
500
527
|
var jwk = header.jwk || opts.jwk || {};
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -43,6 +43,7 @@ var x509 = require("./schema-x509");
|
|
|
43
43
|
var schemaCrl = require("./schema-crl");
|
|
44
44
|
var key = require("./key");
|
|
45
45
|
var cms = require("./cms-verify"); // pki.cms.sign (build the id-signedData authSafe) + pki.cms.verify (the open integrity gate) + cms.encrypt (the privacy envelope)
|
|
46
|
+
var cmsSign = require("./cms-sign"); // the signer-descriptor field tables, for the integrity signers this verb authors
|
|
46
47
|
var cmsDecrypt = require("./cms-decrypt"); // the @internal decryptEnvelopedData seam -- opens a privacy safe off the pkcs12 parser's already-walked (BER-capable) EnvelopedData node
|
|
47
48
|
var schemaPkcs12 = require("./schema-pkcs12");
|
|
48
49
|
var pkix = require("./schema-pkix");
|
|
@@ -116,6 +117,91 @@ var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- a
|
|
|
116
117
|
// Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
|
|
117
118
|
// capability cannot arrive with its option silently ignored at this boundary.
|
|
118
119
|
var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
|
|
120
|
+
// The two caller-owned objects pki.pkcs12.build reads. The spec keys are what _normalizeSpec
|
|
121
|
+
// consumes -- the explicit `safeContents` form and the `{ key, cert, ca }` shorthand -- so the table
|
|
122
|
+
// is derived from that function rather than from the doc block. opts.integrity already had its own
|
|
123
|
+
// door; the objects containing it did not, so a misspelled `certificates` at the top level was
|
|
124
|
+
// dropped and the store built without them.
|
|
125
|
+
// The spec comes in two mutually exclusive FORMS, so it is checked against the one it selects.
|
|
126
|
+
// `_normalizeSpec` returns `spec.safeContents` the moment it is an array and never looks at the
|
|
127
|
+
// shorthand fields, so a union accepted `{ safeContents: [...], key }` and built a store with the
|
|
128
|
+
// key silently absent -- the same omission the door exists to refuse, reached by mixing the forms
|
|
129
|
+
// rather than by misspelling one.
|
|
130
|
+
var _BUILD_SPEC_FULL_KEYS = { safeContents: 1 };
|
|
131
|
+
var _BUILD_SPEC_SHORTHAND_KEYS = { key: 1, cert: 1, ca: 1, friendlyName: 1, localKeyId: 1 };
|
|
132
|
+
// `recipientCerts` selects the privacy recipients and is read by _normalizeSpec, which _build hands
|
|
133
|
+
// opts to -- it belongs here even though _build never names it. The three verbs this module exports
|
|
134
|
+
// have disjoint option sets (verifyMac reads maxIterations / recipientKey / allowUnauthenticated,
|
|
135
|
+
// open reads signerCerts / keys / importAlgorithm), so build gets its own table rather than a
|
|
136
|
+
// shared one that would admit theirs.
|
|
137
|
+
// Per spec FORM, because `recipientCerts` is read only by the shorthand branch of _normalizeSpec.
|
|
138
|
+
// Under the full form it is read by nothing: a caller who put a plaintext key bag in safeContents
|
|
139
|
+
// and supplied recipientCerts expecting public-key privacy got an id-data safe holding the private
|
|
140
|
+
// key in cleartext. The full form builds its own safes, so privacy there is `recipients` on the
|
|
141
|
+
// entry.
|
|
142
|
+
var _BUILD_OPTS_FULL = { integrity: 1, mac: 1, password: 1, pem: 1 };
|
|
143
|
+
var _BUILD_OPTS_SHORTHAND = { integrity: 1, mac: 1, password: 1, pem: 1, recipientCerts: 1 };
|
|
144
|
+
// The fields a safeContents entry carries. This entry is where the PRIVACY directive lives, which
|
|
145
|
+
// makes an unrecognized name here the costliest misspelling in the format: a misspelled `encrypt`
|
|
146
|
+
// is neither present nor falsy, so it passes the present-but-falsy guard in _buildAuthSafeElement,
|
|
147
|
+
// no privacy is selected, and the safe goes out as plaintext id-data -- an unshrouded keyBag in the
|
|
148
|
+
// clear, inside a PFX whose MAC still verifies and which opens without complaint.
|
|
149
|
+
// Per privacy branch, because the branches read different fields:
|
|
150
|
+
// `contentEncryptionAlgorithm` is the public-key branch's content cipher and is read by NOTHING on
|
|
151
|
+
// a password safe, whose cipher comes from `encrypt.cipher`. A union accepted an explicit cipher
|
|
152
|
+
// request on a password safe and encrypted under the default anyway.
|
|
153
|
+
var _SAFE_RECIPIENTS_KEYS = { bags: 1, recipients: 1, contentEncryptionAlgorithm: 1 };
|
|
154
|
+
var _SAFE_PASSWORD_KEYS = { bags: 1, encrypt: 1 };
|
|
155
|
+
var _SAFE_PLAINTEXT_KEYS = { bags: 1 };
|
|
156
|
+
// The fields a bag carries, PER TYPE rather than as one union. A union would admit a field that
|
|
157
|
+
// this bag's own arm never reads, which is worse here than a dropped attribute: `encrypt` on a
|
|
158
|
+
// `key` bag is read by nothing, so the private key is emitted as a plaintext keyBag while the
|
|
159
|
+
// caller supplied an explicit encryption directive and the outer MAC gives integrity only. Naming
|
|
160
|
+
// the fields per type makes that combination a refusal instead of a silent downgrade. Written out
|
|
161
|
+
// per type rather than merged at load, so each row reads as the whole contract for that bag.
|
|
162
|
+
var _BAG_KEYS_BY_TYPE = {
|
|
163
|
+
key: { type: 1, friendlyName: 1, localKeyId: 1, key: 1 },
|
|
164
|
+
shroudedKey: { type: 1, friendlyName: 1, localKeyId: 1, key: 1, encrypt: 1 },
|
|
165
|
+
cert: { type: 1, friendlyName: 1, localKeyId: 1, cert: 1 },
|
|
166
|
+
crl: { type: 1, friendlyName: 1, localKeyId: 1, crl: 1 },
|
|
167
|
+
secret: { type: 1, friendlyName: 1, localKeyId: 1, secretTypeId: 1, secretValue: 1 },
|
|
168
|
+
safeContents: { type: 1, friendlyName: 1, localKeyId: 1, nested: 1 },
|
|
169
|
+
};
|
|
170
|
+
// The PBE descriptor both privacy levels share (_pbeOpts is the one place it is read, so one door
|
|
171
|
+
// covers the safe-level and bag-level spellings). A misspelled parameter reverts to the built-in
|
|
172
|
+
// default, so a caller who asked for a stronger KDF or a different cipher silently gets neither.
|
|
173
|
+
var _ENCRYPT_KEYS = { password: 1, cipher: 1, salt: 1, iterations: 1, prf: 1 };
|
|
174
|
+
// The MAC descriptor. Every field has a default, so a misspelled one reverts to the shipped value:
|
|
175
|
+
// a caller who asked for a higher iteration count, a different hash or a longer key gets the
|
|
176
|
+
// default instead, and the store carries no record of which was used.
|
|
177
|
+
// Per MAC algorithm: the classic Appendix B key derivation produces a key at the hash's own output
|
|
178
|
+
// length and has no keyLength to set, so admitting one there took an explicit security parameter and
|
|
179
|
+
// emitted the same MAC as a call that never named it. Only PBMAC1 (RFC 9579) derives to a length.
|
|
180
|
+
var _MAC_KEYS = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
|
|
181
|
+
var _MAC_KEYS_HMAC = { algorithm: 1, hash: 1, salt: 1, iterations: 1, password: 1 };
|
|
182
|
+
var _MAC_KEYS_PBMAC1 = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
|
|
183
|
+
// The reading verbs take their own option sets, disjoint from build's. `maxIterations` is the one
|
|
184
|
+
// that matters most: it caps the work a hostile store can demand, so a misspelling restores the
|
|
185
|
+
// built-in ceiling and the caller's tighter bound is never applied.
|
|
186
|
+
// The table is what the CALL reads, including through its delegations -- and those cross a module
|
|
187
|
+
// boundary here. recipientKey / recipientCert are read by _openEncryptedSafe; `recipientIndex` is
|
|
188
|
+
// read by cms-decrypt's decryptEnvelopedData, which _openEncryptedSafe hands this whole options
|
|
189
|
+
// object to, so a table built from this file's own `opts.` reads alone would refuse a documented
|
|
190
|
+
// option and break selecting a recipient by index in a multi-recipient store.
|
|
191
|
+
//
|
|
192
|
+
// One table here, not one per branch, and the distinction is who selects the branch. A builder's
|
|
193
|
+
// options are checked against the form the CALLER chose, because a field the chosen form never
|
|
194
|
+
// reads is a request that silently did not happen. Reading a store, the branch is chosen by the
|
|
195
|
+
// BYTES: which integrity mode it carries, and whether a safe is password- or recipient-encrypted,
|
|
196
|
+
// are not known until it is parsed. These options are the policy a caller declares in advance for
|
|
197
|
+
// whatever the store turns out to be, so one that goes unread means the store did not need it --
|
|
198
|
+
// signerCerts on a MAC'd store, recipientKey on one with no enveloped safe. Refusing those would
|
|
199
|
+
// make a correct, store-agnostic call fail on the shape of its input.
|
|
200
|
+
var _OPEN_OPTS = {
|
|
201
|
+
allowUnauthenticated: 1, importAlgorithm: 1, keys: 1, maxIterations: 1, signerCerts: 1,
|
|
202
|
+
recipientKey: 1, recipientCert: 1, recipientIndex: 1,
|
|
203
|
+
};
|
|
204
|
+
var _VERIFY_MAC_OPTS = { maxIterations: 1 };
|
|
119
205
|
|
|
120
206
|
// The same encoding, reporting ownership, mirroring pbes2.passwordBytesOwned. A caller-supplied
|
|
121
207
|
// Buffer is returned as-is and is borrowed: clearing it would destroy the caller's own credential,
|
|
@@ -280,6 +366,14 @@ function _safeBag(bagName, bagValueDer, bag) {
|
|
|
280
366
|
|
|
281
367
|
function _buildBag(bag, opts, depth) {
|
|
282
368
|
if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
|
|
369
|
+
// Checked against THIS type's table. An unrecognized type falls through to the switch below,
|
|
370
|
+
// which is the one place that names the permitted set.
|
|
371
|
+
if (guard.intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
|
|
372
|
+
guard.identifier.assertKnownKeys(bag, _BAG_KEYS_BY_TYPE[bag.type], _err, "pkcs12/bad-input", function (k) {
|
|
373
|
+
return "unknown field " + JSON.stringify(k) + " on a " + JSON.stringify(bag.type) +
|
|
374
|
+
" bag; that field is not read for this bag type";
|
|
375
|
+
});
|
|
376
|
+
}
|
|
283
377
|
switch (bag.type) {
|
|
284
378
|
case "key": {
|
|
285
379
|
var keyDer = _coerceDer(bag.key, "keyBag key");
|
|
@@ -290,9 +384,12 @@ function _buildBag(bag, opts, depth) {
|
|
|
290
384
|
var kDer = _coerceDer(bag.key, "shroudedKey key");
|
|
291
385
|
try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
|
|
292
386
|
var enc = bag.encrypt || {};
|
|
387
|
+
// The descriptor is checked BEFORE its password is read, so no field of an object carrying an
|
|
388
|
+
// unrecognized name reaches the KDF -- the same order the safe-level path uses below.
|
|
389
|
+
var bagPbe = _pbeOpts(enc);
|
|
293
390
|
var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
|
|
294
391
|
var r;
|
|
295
|
-
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer,
|
|
392
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, bagPbe, _err, "pkcs12"); }
|
|
296
393
|
finally { _wipePw(pw); }
|
|
297
394
|
return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
|
|
298
395
|
}
|
|
@@ -349,6 +446,10 @@ function _reqDer(input, label) {
|
|
|
349
446
|
}
|
|
350
447
|
|
|
351
448
|
function _pbeOpts(enc) {
|
|
449
|
+
guard.identifier.assertKnownKeys(enc, _ENCRYPT_KEYS, _err, "pkcs12/bad-input", function (k) {
|
|
450
|
+
return "unknown encrypt field " + JSON.stringify(k) +
|
|
451
|
+
"; the PBE descriptor is { password?, cipher?, salt?, iterations?, prf? }";
|
|
452
|
+
});
|
|
352
453
|
var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
|
|
353
454
|
if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
|
|
354
455
|
return {
|
|
@@ -379,6 +480,18 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
379
480
|
var hasRecipients = sc.recipients !== undefined;
|
|
380
481
|
var hasEncrypt = sc.encrypt !== undefined;
|
|
381
482
|
if (hasEncrypt && hasRecipients) throw _err("pkcs12/bad-input", "a safeContents cannot combine encrypt (password) and recipients (public-key) -- one ContentInfo is one privacy type (RFC 7292 sec. 4.1)");
|
|
483
|
+
// Checked against the branch the directive above selects, and BEFORE that branch reads anything.
|
|
484
|
+
// The branches read different fields: `contentEncryptionAlgorithm` is the public-key branch's
|
|
485
|
+
// cipher and is read by nothing on a password safe, whose cipher comes from `encrypt.cipher` --
|
|
486
|
+
// so a union accepted an explicit cipher request on a password safe and encrypted under the
|
|
487
|
+
// default anyway.
|
|
488
|
+
guard.identifier.assertKnownKeys(sc, hasRecipients ? _SAFE_RECIPIENTS_KEYS : (hasEncrypt ? _SAFE_PASSWORD_KEYS : _SAFE_PLAINTEXT_KEYS),
|
|
489
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
490
|
+
return "unknown safeContents field " + JSON.stringify(k) + " for a " +
|
|
491
|
+
(hasRecipients ? "public-key safe; it takes { bags, recipients, contentEncryptionAlgorithm? }"
|
|
492
|
+
: hasEncrypt ? "password safe; it takes { bags, encrypt } and its cipher comes from encrypt.cipher"
|
|
493
|
+
: "plaintext safe; it takes { bags }, and a privacy directive must be encrypt or recipients");
|
|
494
|
+
});
|
|
382
495
|
var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
|
|
383
496
|
if (hasRecipients) {
|
|
384
497
|
if (!Array.isArray(sc.recipients) || !sc.recipients.length) throw _err("pkcs12/bad-input", "safeContents.recipients must be a non-empty array of recipient descriptors (RFC 5652 sec. 6.1)");
|
|
@@ -401,9 +514,12 @@ async function _buildAuthSafeElement(sc, opts) {
|
|
|
401
514
|
return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
|
|
402
515
|
}
|
|
403
516
|
if (!sc.encrypt || typeof sc.encrypt !== "object") throw _err("pkcs12/bad-input", "safeContents.encrypt must be an object { password? } (RFC 7292 sec. 5.1) -- omit it entirely for a plaintext safe");
|
|
517
|
+
// Checked before the password is read, so no field of a descriptor carrying an unrecognized name
|
|
518
|
+
// reaches the KDF.
|
|
519
|
+
var safePbe = _pbeOpts(sc.encrypt);
|
|
404
520
|
var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
|
|
405
521
|
var r;
|
|
406
|
-
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer,
|
|
522
|
+
try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, safePbe, _err, "pkcs12"); }
|
|
407
523
|
finally { _wipePw(pw); }
|
|
408
524
|
var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
|
|
409
525
|
var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
|
|
@@ -464,11 +580,26 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
464
580
|
// Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
|
|
465
581
|
// inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
|
|
466
582
|
// PBES2 cert bags + one shrouded-key safe.
|
|
467
|
-
|
|
468
|
-
|
|
583
|
+
// Which of the two forms a spec is gets decided ONCE, and that one decision is what both the door
|
|
584
|
+
// and this normalizer act on. Decided separately they can disagree: two spellings of "is this the
|
|
585
|
+
// full form" drift apart on their own, and a caller getter returning an array to one and something
|
|
586
|
+
// else to the other makes them disagree on demand -- the full form checked while the shorthand form
|
|
587
|
+
// is the one assembled.
|
|
588
|
+
function _specForm(spec) {
|
|
589
|
+
var isObject = !!spec && typeof spec === "object" && !Buffer.isBuffer(spec);
|
|
590
|
+
var safeContents = isObject ? spec.safeContents : null;
|
|
591
|
+
var full = guard.intrinsic.isArray(safeContents);
|
|
592
|
+
return { isObject: isObject, full: full, safeContents: full ? safeContents : null };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function _normalizeSpec(spec, opts, form) {
|
|
596
|
+
if (form.full) return form.safeContents;
|
|
469
597
|
if (spec && (spec.key != null || spec.cert != null)) {
|
|
470
|
-
|
|
471
|
-
|
|
598
|
+
// One read of the caller's array, so the emptiness check and the recipients built from it cannot
|
|
599
|
+
// be handed different values.
|
|
600
|
+
var recipientCerts = opts.recipientCerts;
|
|
601
|
+
var enveloped = recipientCerts != null;
|
|
602
|
+
if (enveloped && (!guard.intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
|
|
472
603
|
var certBags = [];
|
|
473
604
|
if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
474
605
|
if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
|
|
@@ -480,7 +611,7 @@ function _normalizeSpec(spec, opts) {
|
|
|
480
611
|
// caller password, if any, remains the MAC (integrity) password; privacy and integrity are independent.
|
|
481
612
|
var bags = certBags.slice();
|
|
482
613
|
if (spec.key != null) bags.push({ type: "key", key: spec.key, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
483
|
-
sc.push({ recipients:
|
|
614
|
+
sc.push({ recipients: recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
|
|
484
615
|
return sc;
|
|
485
616
|
}
|
|
486
617
|
if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
|
|
@@ -512,7 +643,7 @@ function _normalizeSpec(spec, opts) {
|
|
|
512
643
|
*
|
|
513
644
|
* @opts
|
|
514
645
|
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
515
|
-
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations
|
|
646
|
+
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations? }`. `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.
|
|
516
647
|
* - `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.
|
|
517
648
|
* - `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.
|
|
518
649
|
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
@@ -541,6 +672,26 @@ function build(spec, opts) {
|
|
|
541
672
|
|
|
542
673
|
async function _build(spec, opts) {
|
|
543
674
|
opts = opts || {};
|
|
675
|
+
// Before the spec is normalized, so a misspelled bag field is a refusal rather than a store built
|
|
676
|
+
// without it. A PKCS#12 file that silently omits the certificate or the key a caller asked for
|
|
677
|
+
// looks well-formed and opens cleanly; the omission surfaces wherever it is later imported.
|
|
678
|
+
var form = _specForm(spec);
|
|
679
|
+
if (form.isObject) {
|
|
680
|
+
guard.identifier.assertKnownKeys(spec, form.full ? _BUILD_SPEC_FULL_KEYS : _BUILD_SPEC_SHORTHAND_KEYS,
|
|
681
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
682
|
+
return "unknown PKCS#12 spec field " + JSON.stringify(k) + " for the " +
|
|
683
|
+
(form.full ? "safeContents form; the shorthand fields belong to { key, cert, ca } and are not read here"
|
|
684
|
+
: "shorthand form; pass safeContents instead to build the safes yourself");
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// The options answer to the same form: recipientCerts is read only by the shorthand branch.
|
|
688
|
+
guard.identifier.assertKnownKeys(opts, form.full ? _BUILD_OPTS_FULL : _BUILD_OPTS_SHORTHAND, _err, "pkcs12/bad-input",
|
|
689
|
+
function (k) {
|
|
690
|
+
return "pki.pkcs12.build has an unknown option " + JSON.stringify(k) +
|
|
691
|
+
(form.full && k === "recipientCerts"
|
|
692
|
+
? " for the safeContents form; the full form carries privacy per entry, as safeContents[i].recipients"
|
|
693
|
+
: "");
|
|
694
|
+
});
|
|
544
695
|
// The integrity mode is checked against the permitted set rather than compared to one literal.
|
|
545
696
|
// Compared, any other spelling reads as "not public-key" and silently selects password integrity,
|
|
546
697
|
// dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
|
|
@@ -556,7 +707,7 @@ async function _build(spec, opts) {
|
|
|
556
707
|
// RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
|
|
557
708
|
// config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
|
|
558
709
|
if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
|
|
559
|
-
var safeContentsSpecs = _normalizeSpec(spec, opts);
|
|
710
|
+
var safeContentsSpecs = _normalizeSpec(spec, opts, form);
|
|
560
711
|
if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
|
|
561
712
|
var elements = [];
|
|
562
713
|
for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(await _buildAuthSafeElement(safeContentsSpecs[i], opts));
|
|
@@ -569,13 +720,48 @@ async function _build(spec, opts) {
|
|
|
569
720
|
// password) provides the integrity, and there is NO MacData. Compose pki.cms.sign -- so every cms.sign
|
|
570
721
|
// signer algorithm (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) ships for free.
|
|
571
722
|
var ig = opts.integrity;
|
|
723
|
+
// `signer` is the one-signer spelling of `signers`, and the list wins outright, so supplying
|
|
724
|
+
// both signed the store under the list alone and dropped the single signer without a word.
|
|
725
|
+
if (ig.signers != null && ig.signer != null) throw _err("pkcs12/bad-input", "supply exactly one of opts.integrity.signer or opts.integrity.signers, not both");
|
|
572
726
|
var signers = ig.signers != null ? ig.signers : (ig.signer != null ? [ig.signer] : null);
|
|
573
727
|
if (!Array.isArray(signers) || !signers.length) throw _err("pkcs12/bad-input", "public-key integrity requires opts.integrity.signer or opts.integrity.signers (a cms.sign signer descriptor)");
|
|
728
|
+
// The signer descriptors are authoring input for this store, so they answer to the same rule as
|
|
729
|
+
// every other field the caller writes here, against the form each one selects. Every field
|
|
730
|
+
// beyond the identity has a default: without the door a misspelled `pss` or `digestAlgorithm`
|
|
731
|
+
// signed the store under PKCS#1 with SHA-256 while the caller had asked for something else, and
|
|
732
|
+
// the store records only what was used, never what was requested.
|
|
733
|
+
for (var sx = 0; sx < signers.length; sx++) {
|
|
734
|
+
var sd = signers[sx];
|
|
735
|
+
if (sd == null || typeof sd !== "object" || Buffer.isBuffer(sd)) throw _err("pkcs12/bad-input", "each opts.integrity signer must be a descriptor object");
|
|
736
|
+
var keyOnly = sd.cert == null && sd.spki != null;
|
|
737
|
+
guard.identifier.assertKnownKeys(sd, keyOnly ? cmsSign.KNOWN_SIGNER_KEY_ONLY_KEYS : cmsSign.KNOWN_SIGNER_CERT_KEYS,
|
|
738
|
+
_err, "pkcs12/bad-input", function (k) {
|
|
739
|
+
return "unknown opts.integrity signer field " + JSON.stringify(k) + " for the " + (keyOnly
|
|
740
|
+
? "key-only form; it takes { key, spki, keyIdentifier } plus the signature parameters"
|
|
741
|
+
: "certificate form; it takes { key, cert } plus the signature parameters");
|
|
742
|
+
});
|
|
743
|
+
}
|
|
574
744
|
var authSafeCi = await cms.sign(authSafeDer, signers, { eContentType: "data", detached: false, certificates: ig.certificates !== false, sid: ig.sid, signingTime: ig.signingTime });
|
|
575
745
|
pfx = b.sequence([b.integer(3n), b.raw(authSafeCi)]); // PFX ::= SEQUENCE { version v3, authSafe (id-signedData) } -- NO MacData
|
|
576
746
|
} else {
|
|
577
747
|
var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
|
|
578
|
-
|
|
748
|
+
// An array passes `typeof x === "object"`, and its own names are indices no table lists, so an
|
|
749
|
+
// array reached _buildMacData as an empty descriptor and produced the DEFAULT MAC. Excluded by
|
|
750
|
+
// the same test opts.integrity already applies, read through the load-time capture so a
|
|
751
|
+
// replaced Array.isArray cannot reopen it.
|
|
752
|
+
if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || guard.intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
|
|
753
|
+
if (opts.mac != null && opts.mac !== false) {
|
|
754
|
+
// The algorithm selects the derivation, so the table that runs is the one belonging to it.
|
|
755
|
+
// An unrecognized spelling keeps the union; _buildMacData refuses it by name a moment later.
|
|
756
|
+
var macAlg = opts.mac.algorithm == null ? "hmac" : opts.mac.algorithm;
|
|
757
|
+
var macKeys = macAlg === "hmac" ? _MAC_KEYS_HMAC : macAlg === "pbmac1" ? _MAC_KEYS_PBMAC1 : _MAC_KEYS;
|
|
758
|
+
guard.identifier.assertKnownKeys(opts.mac, macKeys, _err, "pkcs12/bad-input", function (k) {
|
|
759
|
+
return "opts.mac has an unknown option " + JSON.stringify(k) +
|
|
760
|
+
(macAlg === "hmac" && k === "keyLength"
|
|
761
|
+
? " for the classic HMAC algorithm; its key is derived at the hash's own output length, and only pbmac1 takes a keyLength"
|
|
762
|
+
: "");
|
|
763
|
+
});
|
|
764
|
+
}
|
|
579
765
|
if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
|
|
580
766
|
pfx = b.sequence(pfxChildren);
|
|
581
767
|
}
|
|
@@ -612,6 +798,9 @@ async function _build(spec, opts) {
|
|
|
612
798
|
* var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
|
|
613
799
|
*/
|
|
614
800
|
async function verifyMac(pfx, password, opts) {
|
|
801
|
+
if (opts != null && typeof opts === "object" && !Buffer.isBuffer(opts)) {
|
|
802
|
+
guard.identifier.assertKnownKeys(opts, _VERIFY_MAC_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.verifyMac has an unknown option ");
|
|
803
|
+
}
|
|
615
804
|
return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
|
|
616
805
|
}
|
|
617
806
|
|
|
@@ -715,7 +904,8 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
715
904
|
*/
|
|
716
905
|
async function open(pfx, password, opts) {
|
|
717
906
|
opts = opts || {};
|
|
718
|
-
|
|
907
|
+
guard.identifier.assertKnownKeys(opts, _OPEN_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.open has an unknown option ");
|
|
908
|
+
if (opts.maxIterations != null &&(typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
|
|
719
909
|
throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
720
910
|
}
|
|
721
911
|
var m = _storeFromBytes(pfx);
|
package/lib/tsp-sign.js
CHANGED
|
@@ -142,9 +142,38 @@ function sign(messageImprint, tsa, opts) {
|
|
|
142
142
|
], _sign);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// The three caller-owned objects pki.tsp.sign reads. A dropped option here is a token that quietly
|
|
146
|
+
// lacks what was asked for -- `odering` for `ordering` emitted a token with the ordering flag unset,
|
|
147
|
+
// which is a claim about the timestamp's orderability that the caller believes they made.
|
|
148
|
+
// The TSA is gated too, and the reason it needs to be is not the one it looks like. Both fields it
|
|
149
|
+
// carries are REQUIRED, so a MISSPELLING of either is already refused for what it leaves missing --
|
|
150
|
+
// but that is only half the class. A name belonging on the OPTIONS, written on the TSA instead, is
|
|
151
|
+
// read by nothing here: `ordering` placed on the TSA emits a token the same size as one that never
|
|
152
|
+
// asked for it, with the flag unset, while the caller believes they set it. Requiring the argument
|
|
153
|
+
// to carry only what it reads is what closes that, so all three caller-owned arguments answer the
|
|
154
|
+
// same question -- is this a name I read? -- rather than two of them answering it.
|
|
155
|
+
var _TSA_KEYS = { cert: 1, key: 1 };
|
|
156
|
+
var _IMPRINT_KEYS = { hashAlgorithm: 1, hashedMessage: 1 };
|
|
157
|
+
var _SIGN_OPTS = {
|
|
158
|
+
policy: 1, serialNumber: 1, genTime: 1, nonce: 1, accuracy: 1, ordering: 1, sid: 1,
|
|
159
|
+
digestAlgorithm: 1, certHashAlgorithm: 1, additionalSignedAttributes: 1, pss: 1, pem: 1,
|
|
160
|
+
};
|
|
161
|
+
|
|
145
162
|
function _sign(messageImprint, tsa, opts) {
|
|
146
163
|
opts = opts || {};
|
|
164
|
+
if (messageImprint && typeof messageImprint === "object" && !Buffer.isBuffer(messageImprint)) {
|
|
165
|
+
guard.identifier.assertKnownKeys(messageImprint, _IMPRINT_KEYS, _err, "tsp/bad-input", function (k) {
|
|
166
|
+
return "unknown messageImprint field " + JSON.stringify(k) + " (the imprint is { hashAlgorithm, hashedMessage })";
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
|
|
147
170
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
|
|
171
|
+
if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
|
|
172
|
+
guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
|
|
173
|
+
return "unknown TSA field " + JSON.stringify(k) +
|
|
174
|
+
" (the TSA is { cert, key }; a signing option belongs in the third argument)";
|
|
175
|
+
});
|
|
176
|
+
}
|
|
148
177
|
var mi = messageImprint || {};
|
|
149
178
|
if (!mi.hashAlgorithm || !NODE_DIGEST[mi.hashAlgorithm]) throw _err("tsp/unsupported-algorithm", "messageImprint.hashAlgorithm must be a supported hash name");
|
|
150
179
|
if (!Buffer.isBuffer(mi.hashedMessage) && !(mi.hashedMessage instanceof Uint8Array)) throw _err("tsp/bad-input", "messageImprint.hashedMessage must be a Buffer");
|
|
@@ -187,7 +216,17 @@ function _sign(messageImprint, tsa, opts) {
|
|
|
187
216
|
// Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] INTEGER (1..999) OPTIONAL,
|
|
188
217
|
// micros [1] INTEGER (1..999) OPTIONAL } (RFC 3161 sec. 2.4.2). seconds is a non-negative
|
|
189
218
|
// integer; millis and micros MUST each be in 1..999 -- enforced before encoding.
|
|
219
|
+
// Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] OPTIONAL, micros [1] OPTIONAL }
|
|
220
|
+
// (RFC 3161 sec. 2.4.2). All three are OPTIONAL and encoded only when present, so a misspelling is
|
|
221
|
+
// missed by nothing: `milis` emitted an EMPTY Accuracy that parses back as zero seconds, millis and
|
|
222
|
+
// micros, and the token then understates its own precision -- a claim about the timestamp that the
|
|
223
|
+
// caller believed they had made.
|
|
224
|
+
var _ACCURACY_KEYS = { seconds: 1, millis: 1, micros: 1 };
|
|
225
|
+
|
|
190
226
|
function _accuracy(a) {
|
|
227
|
+
guard.identifier.assertKnownKeys(a, _ACCURACY_KEYS, _err, "tsp/bad-input", function (k) {
|
|
228
|
+
return "unknown accuracy field " + JSON.stringify(k) + "; Accuracy is { seconds?, millis?, micros? }";
|
|
229
|
+
});
|
|
191
230
|
var f = [];
|
|
192
231
|
if (a.seconds != null) {
|
|
193
232
|
var s = Number(a.seconds);
|
|
@@ -264,10 +303,29 @@ function _assertImprint(mi) {
|
|
|
264
303
|
* var sha256Digest = Buffer.from(await pki.webcrypto.subtle.digest("SHA-256", Buffer.from("hello")));
|
|
265
304
|
* var req = pki.tsp.request({ hashAlgorithm: "sha256", hashedMessage: sha256Digest }, { nonce: 0x0102030405060708n, certReq: true });
|
|
266
305
|
*/
|
|
306
|
+
// The options pki.tsp.request encodes into the TimeStampReq. Every one is OPTIONAL in the
|
|
307
|
+
// structure, so a misspelled name is missed by nothing and the request goes out claiming less than
|
|
308
|
+
// was asked for. `nonce` is the one that costs most: misspelled, the request carries no nonce at
|
|
309
|
+
// all, and the caller matches a reply against a replay defense that was never asked for.
|
|
310
|
+
var _REQUEST_OPTS = { reqPolicy: 1, nonce: 1, certReq: 1, extensions: 1, pem: 1 };
|
|
311
|
+
|
|
267
312
|
function request(messageImprint, opts) {
|
|
268
313
|
opts = opts || {};
|
|
269
314
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.request options must be an object");
|
|
315
|
+
guard.identifier.assertKnownKeys(opts, _REQUEST_OPTS, _err, "tsp/bad-input", "pki.tsp.request has an unknown option ");
|
|
270
316
|
var mi = messageImprint || {};
|
|
317
|
+
// The same table pki.tsp.sign holds its imprint to. The request's own options live in the second
|
|
318
|
+
// argument, so `nonce` written on the imprint reaches nothing and the request goes out without
|
|
319
|
+
// the replay defense the caller believes they asked for.
|
|
320
|
+
// The same table pki.tsp.sign holds its imprint to. The request's own options live in the second
|
|
321
|
+
// argument, so `nonce` written on the imprint reaches nothing and the request goes out without
|
|
322
|
+
// the replay defense the caller believes they asked for.
|
|
323
|
+
if (mi && typeof mi === "object" && !Buffer.isBuffer(mi)) {
|
|
324
|
+
guard.identifier.assertKnownKeys(mi, _IMPRINT_KEYS, _err, "tsp/bad-input", function (k) {
|
|
325
|
+
return "unknown messageImprint field " + JSON.stringify(k) +
|
|
326
|
+
" (the imprint is { hashAlgorithm, hashedMessage }; a request option belongs in the second argument)";
|
|
327
|
+
});
|
|
328
|
+
}
|
|
271
329
|
_assertImprint(mi);
|
|
272
330
|
var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
|
|
273
331
|
// TimeStampReq: version(1), messageImprint, then OPTIONAL reqPolicy / nonce / certReq /
|
|
@@ -349,9 +407,15 @@ function parseRequest(input) { return schemaTsp.parseRequest(input); }
|
|
|
349
407
|
* var resp = pki.tsp.response(token, {}); // granted
|
|
350
408
|
* var rej = pki.tsp.response(null, { status: 2, failInfo: ["badAlg"] }); // rejection
|
|
351
409
|
*/
|
|
410
|
+
// The options pki.tsp.response encodes into the TimeStampResp. `status` carries the verdict and
|
|
411
|
+
// defaults to granted(0), so a misspelled name does not lose a detail -- it INVERTS the answer: a
|
|
412
|
+
// TSA refusing a request emits a granted response instead, over a token it did mean to withhold.
|
|
413
|
+
var _RESPONSE_OPTS = { status: 1, statusString: 1, failInfo: 1, pem: 1 };
|
|
414
|
+
|
|
352
415
|
function response(token, opts) {
|
|
353
416
|
opts = opts || {};
|
|
354
417
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.response options must be an object");
|
|
418
|
+
guard.identifier.assertKnownKeys(opts, _RESPONSE_OPTS, _err, "tsp/bad-input", "pki.tsp.response has an unknown option ");
|
|
355
419
|
var status = opts.status == null ? 0 : Number(opts.status);
|
|
356
420
|
if (!Number.isInteger(status) || status < 0 || status > 5) throw _err("tsp/bad-input", "PKIStatus must be an integer in 0..5 (RFC 3161 sec. 2.4.2)");
|
|
357
421
|
var granted = status === 0 || status === 1;
|
package/lib/x509-sign.js
CHANGED
|
@@ -53,6 +53,27 @@ var OID_SKI = O("subjectKeyIdentifier");
|
|
|
53
53
|
|
|
54
54
|
// The recognized keys of the `extensions` spec object; an unknown key is a typo and throws at
|
|
55
55
|
// config-time (a custom extension is passed as pre-encoded DER via the array form).
|
|
56
|
+
// The three caller-owned objects pki.x509.sign reads, each with its own door. A key nothing here
|
|
57
|
+
// names is a typo or a misplaced argument, and the failure it causes is invisible: the certificate
|
|
58
|
+
// builds, signs, and simply does not carry what was asked for. `spec.extension` (one letter short
|
|
59
|
+
// of `extensions`) emitted a certificate with NO extensions at all -- no basicConstraints on an
|
|
60
|
+
// intended CA, no keyUsage on an intended constrained leaf. `spec.issuer` is the other shape: the
|
|
61
|
+
// issuer is argument TWO, and nesting it in the spec left the certificate SELF-SIGNED with nothing
|
|
62
|
+
// to read as a failure.
|
|
63
|
+
//
|
|
64
|
+
// Every table is derived from what the function actually reads, not from the doc block, because a
|
|
65
|
+
// table naming a key nothing reads reopens the hole it was added to close.
|
|
66
|
+
var KNOWN_SPEC_KEYS = {
|
|
67
|
+
subject: 1, subjectPublicKey: 1, notBefore: 1, notAfter: 1, serialNumber: 1, extensions: 1,
|
|
68
|
+
};
|
|
69
|
+
// The issuer names itself in one of two FORMS, and the cert form wins unconditionally: when
|
|
70
|
+
// `issuer.cert` is present the builder takes the DN and the public key from it and never reads
|
|
71
|
+
// `name` or `publicKey`. A union accepted `{ key, cert, name: "Different CA", publicKey }` and
|
|
72
|
+
// signed under the certificate's identity while discarding the identity the caller wrote out.
|
|
73
|
+
var KNOWN_ISSUER_CERT_KEYS = { key: 1, cert: 1 };
|
|
74
|
+
var KNOWN_ISSUER_EXPLICIT_KEYS = { key: 1, name: 1, publicKey: 1 };
|
|
75
|
+
var KNOWN_SIGN_OPTS = { digestAlgorithm: 1, pem: 1, pss: 1 };
|
|
76
|
+
|
|
56
77
|
var KNOWN_EXT_KEYS = {
|
|
57
78
|
subjectKeyIdentifier: 1, authorityKeyIdentifier: 1, keyUsage: 1, keyUsageCritical: 1,
|
|
58
79
|
extendedKeyUsage: 1, extendedKeyUsageCritical: 1, basicConstraints: 1, subjectAltName: 1,
|
|
@@ -282,6 +303,22 @@ function _sign(spec, issuer, opts) {
|
|
|
282
303
|
opts = opts || {};
|
|
283
304
|
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("x509/bad-input", "the certificate spec must be an object");
|
|
284
305
|
issuer = issuer || {};
|
|
306
|
+
// Before a single field is read, so a misspelling is a refusal rather than an omission carried
|
|
307
|
+
// into a signed certificate. The issuer message names its own argument because the mistake this
|
|
308
|
+
// catches most often is putting the issuer inside the spec, where it self-signed in silence.
|
|
309
|
+
guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "x509/bad-input", function (k) {
|
|
310
|
+
return "unknown certificate spec field " + JSON.stringify(k) +
|
|
311
|
+
(k === "issuer" ? "; the issuer is the SECOND argument to pki.x509.sign, not a spec field" : "") +
|
|
312
|
+
(k === "extension" ? "; the extensions spec field is `extensions`" : "");
|
|
313
|
+
});
|
|
314
|
+
var byCert = issuer.cert != null;
|
|
315
|
+
guard.identifier.assertKnownKeys(issuer, byCert ? KNOWN_ISSUER_CERT_KEYS : KNOWN_ISSUER_EXPLICIT_KEYS,
|
|
316
|
+
_err, "x509/bad-input", function (k) {
|
|
317
|
+
return "unknown issuer field " + JSON.stringify(k) + " for the " + (byCert
|
|
318
|
+
? "certificate form; issuer.cert supplies the name and the public key, so they are not read here"
|
|
319
|
+
: "explicit form; the issuer takes { key, name, publicKey }, or { key, cert } to take both from a certificate");
|
|
320
|
+
});
|
|
321
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "x509/bad-input", "pki.x509.sign has an unknown option ");
|
|
285
322
|
if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
|
|
286
323
|
|
|
287
324
|
var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:123ddb5e-05ea-4e65-9d7d-7a667c46e3a3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-20T20:31:47.390Z",
|
|
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.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.18",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.18",
|
|
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.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.18",
|
|
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.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|