@blamejs/pki 0.5.4 → 0.5.6
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 +42 -0
- package/MIGRATING.md +72 -0
- package/lib/acme.js +47 -19
- package/lib/attrcert-sign.js +11 -7
- package/lib/cmp-session.js +23 -10
- package/lib/cmp-verify.js +12 -4
- package/lib/cms-decrypt.js +71 -30
- package/lib/cms-sign.js +11 -0
- package/lib/cms-verify.js +22 -8
- package/lib/crl-sign.js +19 -7
- package/lib/est.js +77 -1
- package/lib/guard-all.js +8 -0
- package/lib/guard-async.js +37 -0
- package/lib/guard-encoding.js +35 -6
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +44 -8
- package/lib/guard-name.js +31 -8
- package/lib/guard-parsed.js +482 -0
- package/lib/hpke.js +36 -3
- package/lib/jose.js +8 -0
- package/lib/lint.js +20 -4
- package/lib/merkle.js +9 -0
- package/lib/ocsp.js +44 -8
- package/lib/path-validate.js +88 -44
- package/lib/pbes2.js +11 -1
- package/lib/pkcs12-build.js +105 -30
- package/lib/pki-build.js +12 -1
- package/lib/schema-cms.js +17 -2
- package/lib/schema-crl.js +7 -1
- package/lib/schema-ocsp.js +6 -1
- package/lib/schema-pkcs12.js +7 -2
- package/lib/schema-pkix.js +14 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/sigstore.js +10 -0
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/tsp-sign.js +112 -19
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +10 -18
- package/lib/webauthn.js +40 -18
- package/lib/x509-sign.js +6 -2
- package/package.json +5 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.6 — 2026-08-16
|
|
8
|
+
|
|
9
|
+
A CMS SignedData is verified over the bytes it was parsed from, an omitted PKCS#12 password is refused rather than encoded as the empty one, and a verb documented as returning a Promise rejects instead of throwing past your .catch.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- A verb documented as returning a Promise now rejects; it never throws synchronously. An operator reads -> Promise<...> in the reference and writes the documented shape, pki.cms.sign(content, signers).catch(handleIt) -- and a validation that ran before the promise was created threw straight past that .catch, so a misspelled option or a malformed input became an uncaught exception in code that already handles errors. Nothing in the shape of the call said which verbs did it. Eleven did: pki.cms.verify, pki.cms.sign, pki.cms.countersign, pki.ocsp.sign, pki.tsp.sign, and six pki.acme verbs. Nine more on the client pki.acme.client(...) returns did the same, including getAuthorization, which an ordinary ACME loop reaches with a URL that came from the CA rather than from the caller. The checks still run synchronously, so a caller's mutable options are read before any turn passes; only the way a fault leaves has changed. If you wrapped one of these verbs in a synchronous try/catch without awaiting it -- which the documented signature never supported, but which worked by accident on exactly these verbs -- that catch no longer fires and the rejection surfaces as an unhandled one instead. Await the call, or attach .catch to it.
|
|
14
|
+
- pki.tsp.verify reports whether revocation was established, not only whether the timestamp authority chained. Revocation runs only when a revocationChecker is supplied, so a token whose TSA was never checked against a CRL or an OCSP responder read identically to one established un-revoked -- and that is the default. The verdict now carries revocationChecked and anchorConstraints from the path validation, in pki.path.validate's own vocabulary, so a timestamp archived to be re-read years later can still answer what was actually checked. The trustAnchor documentation no longer lists revocation unconditionally.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- pki.cms.verify computes its verdict over the bytes the parser read. A SignedData's meaning is a signature over a byte range, but a parsed one presents that range, the signature, the algorithms and the certificates as separate properties, and the verb read them as though the parser had produced them together. The forgery that follows is concrete: take a message a trusted signer really signed, keep its SignerInfo and signature untouched, set signedAttrsBytes to null, and present the signature's own preimage -- the signed attributes re-tagged as the SET OF the signature covers -- as the encapsulated content under any content type you like. With no signed attributes the signature is checked against the content directly, which is exactly those bytes, and neither the content-type nor the message-digest check runs, so the message verifies as valid content the signer never signed. pki.schema.cms.parse now records what it read, pki.cms.verify re-derives from that record, and a SignedData a caller assembled rather than parsed is refused with a typed error instead of dereferenced into a raw TypeError. Passing DER, PEM, or the parser's own unmodified result is unaffected.
|
|
19
|
+
- pki.pkcs12.build refuses a store with no password instead of building one under the empty password. An omitted password and the empty password are different credentials, and the difference was invisible at the call site: pki.pkcs12.build(spec) with no options at all, or with the option name misspelled, returned a well-formed store whose shrouded private key opened under "" -- a key protected by nothing, with no error anywhere. The empty password is still available; it has to be asked for, as "".
|
|
20
|
+
- pki.pkcs12.build validates opts.integrity.mode against the value it accepts. Compared against a single literal, any other spelling read as "not public-key" and silently selected password integrity, dropping the signer with it: a caller who wrote mode: 'publicKey' asked for a CMS signature over the AuthenticatedSafe and got a password MAC. Combined with the previous item, a caller who misspelled both options got a store MACed under the empty password while believing it was signature-protected. An unrecognized key on opts.integrity is refused too.
|
|
21
|
+
- pki.pkcs12.open bounds the key-derivation work of a modern store, not only a legacy one. The aggregate budget existed because a per-bag iteration cap resets on every bag, so a store that repeats a costly bag up to the parser's element limit multiplies the cap by that limit -- but it was charged only on the RFC 7292 Appendix C path, leaving the RFC 8018 PBES2 path -- the one OpenSSL and NSS emit, and the one this toolkit itself builds -- free to do exactly that. Measured before the fix: ten bags at a million iterations each ran ten million aggregate rounds of blocking PBKDF2 with no refusal. Both schemes now charge one budget, and it is charged before the derivation runs rather than after.
|
|
22
|
+
- A PKCS#12 password this toolkit encoded is cleared once the derivation has consumed it, on the PBES2 and PBMAC1 paths as well as the classic ones. The Appendix B.1 copy taken from a password argument was wiped while the UTF-8 copy taken from the same argument in the same call was not, leaving a plaintext password in the heap after every modern bag encryption, bag decryption and MAC. A Buffer you supply is never written to: it is yours, and clearing it would destroy your credential rather than protect it.
|
|
23
|
+
- pki.tsp.request validates a pre-encoded extension instead of splicing it in. Elements of opts.extensions were checked only for being byte-like and then concatenated straight into the request, so a caller relaying an extension blob it did not author put fully chosen bytes into the structure -- and the encoder emitted requests its own pki.tsp.parseRequest refuses: an undecodable value, a repeated extension identifier, an explicit critical=FALSE that DER requires be omitted. Each element now goes through the same pre-encoded-Extension gate every other request builder in the toolkit applies, including the duplicate-identifier check.
|
|
24
|
+
- pki.tsp.sign, pki.tsp.request and pki.tsp.verify report a bad serial number or nonce as a typed tsp/bad-input. A value that is not an integer reached BigInt() directly and raised a raw SyntaxError or RangeError out of a public verb -- an untyped fault a caller handling tsp/* codes cannot catch.
|
|
25
|
+
- pki.sigstore.verifyBundle refuses an option it does not recognize. The identity policy one level down already did, for the reason that applies just as much at the top: the signer pin goes by other names in other Sigstore tooling, and a spelling this verb swallowed checked nothing under a name the operator believed pinned the signer. At the top level the same slip also loses the SLSA predicate pin, and unlike the identity fields there is no report field to reveal it -- the verdict said verified: true with the signer and the predicate entirely unpinned.
|
|
26
|
+
|
|
27
|
+
## v0.5.5 — 2026-08-15
|
|
28
|
+
|
|
29
|
+
A verdict is computed over the bytes the parser read, an identity is derived from the bytes that carry it, and the guards match no patterns.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The package resolves one entry point. require("@blamejs/pki") is unchanged; a path INTO the package, such as require("@blamejs/pki/lib/schema-x509"), no longer resolves. Every module under lib/ carries @internal in its own header and none has ever appeared in the API snapshot that freezes the public surface -- they were reachable because the package declared no exports map, not because they were offered, and one of them mints the provenance record the integrity verbs above rely on. Everything the internals do is on pki.*: the decoders are pki.schema.<format>.parse, the codec is pki.asn1, the OID registry is pki.oid, the error classes are pki.errors. MIGRATING.md carries the recipe.
|
|
34
|
+
- pki.merkle, pki.jose, pki.hpke, pki.smime and pki.est refuse an option they do not recognize, which completes the toolkit -- every module that takes options now does. A misspelled option is the one input that reads as an omission rather than as a value, so the caller who asked for something stricter gets the looser default and is told nothing: a misspelled psk leaves an HPKE psk-mode setup with no pre-shared key, a misspelled key leaves pki.jose.verify accepting whichever key the message names, a misspelled leafIndex leaves a Merkle inclusion proof about a leaf the caller never chose, a misspelled strictMicalg accepts the S/MIME digest mismatch it was set to reject, and a misspelled expectedRecipientKeyId drops the recipient pin on an EST server-generated private key. The accepted set is per verb rather than per module, because the surfaces differ -- form means something to pki.smime.sign and nothing to pki.smime.encrypt, strict cannot run on pki.est.cacerts at all, and HPKE's two ends read the same object from opposite sides, so senderPublicKey does nothing at the sender and senderKey nothing at the recipient. A merged set would accept each verb's options everywhere and reproduce the silence in a wider form, which is why an option passed to the wrong end of an HPKE exchange is refused with a message saying where it belongs rather than ignored -- the caller who passed it usually believes they authenticated something. Four options pki.smime.sign has always forwarded (hcp, sid, signedAttributes, additionalSignedAttributes) and EST's auth object are now documented; they worked before and were absent from the reference.
|
|
35
|
+
- The guards match no patterns. A guard runs on the most hostile input the toolkit sees, and a pattern engine's cost on a rejecting string is a property of the pattern rather than of the length -- the one thing a caller's size cap cannot bound. Nine patterns across five guards are now explicit character walks, each one pass. Three carried a second defect settled by the rewrite: the whitespace fold above, a JSON number grammar written twice (once as the scan and once as a pattern re-matching what the scan had just read), and an RFC 4514 escape that ran a pattern replace and then a second loop over the same attacker-supplied value.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- A claimed-parsed structure must carry every field the matching pki.schema parser produces. The rule already existed at one door -- pki.path.build refused a partial claimed-parsed certificate -- while pki.path.validate, which build hands its result to, tested only for a truthy tbsBytes and passed the object into the RFC 5280 sec. 6.1 walk. Eleven doors now share it: pki.path.validate and build, pki.path.crlChecker, pki.crl.verify / isRevoked and the issuer of pki.crl.sign, the issuer of pki.x509.sign, pki.attrcert.sign, pki.ocsp's certificate argument, pki.lint, and the caller root certificates pki.webauthn takes for attestation and for android-safetynet. Completeness is measured against the parser rather than against what any one verb reads, because a field absent from an object is not a field with a safe default: an extension entry with no critical property read as non-critical, so a certificate rejected for an unknown critical extension when passed as bytes validated when passed as an object; a missing serialNumber surfaced as an error from the ASN.1 layer; and a missing issuer.bytes produced an OCSP request whose issuerNameHash covered nothing. Passing bytes, PEM, or the parser's own unmodified output is unaffected.
|
|
40
|
+
- A certificate or CRL a verdict is taken from is re-derived from the bytes its parser read. pki.path.validate and build, pki.path.crlChecker, pki.crl.verify and pki.crl.isRevoked all reach a decision, and completeness alone cannot carry one: a certificate is one signature over one byte range, but a parsed certificate presents that range, the signature, and every field the range encodes as separate properties. Keep a real CA certificate's signed bytes and signature and replace only its subjectPublicKeyInfo, and every field is well-formed, the signature verifies over the original range, and the substituted key is then what verifies the next certificate in the chain -- a forged chain built out of a genuine certificate. Emptying extensions is the same move against basicConstraints, keyUsage, name constraints and the unknown-critical rule; emptying a CRL's revokedCertificates leaves a correctly signed CRL reporting a revoked certificate as good. pki.schema.x509.parse and pki.schema.crl.parse now record what they read, these verbs parse it again from that record, and a certificate or CRL a caller assembled rather than parsed is refused. Passing bytes, PEM, or the parser's own unmodified output is unaffected.
|
|
41
|
+
- pki.ocsp.verify, pki.path.verifyOcspResponse, pki.pkcs12.verifyMac and pki.pkcs12.open compute their verdict over the bytes the parser read. A signature check has three parts -- the signature, the algorithm that verifies it, and the byte range it covers -- and on a parsed response all three are separate properties: pair a real CA's signature over a certificate that CA issued with that certificate's own signed bytes and algorithm, relabel the three, and every part of the check passes for a response the responder never produced. A PKCS#12 store has the same shape with two parts, the range the MAC covers and the bags handed back as verified, so one object could say verify this and return that. The parsers now record what they parsed and these verbs re-derive from that record, so an object edited or rebuilt after parsing is not what the verdict describes. Passing the parser's own result still works and is unchanged.
|
|
42
|
+
- pki.attrcert.sign derives both halves of a Holder's identity from the signed bytes. Issuer and serial together ARE the identity being bound; the issuer was decoded from tbsBytes while the serial was read off the object, so a parsed certificate with one field replaced produced a Holder naming a real issuer with a serial nobody issued.
|
|
43
|
+
- pki.trust.anchor answers from what the store read. A root program's metadata -- these purposes, until this date -- is a statement about a KEY, so an entry rebuilt with a substituted publicKey carried the program's word onto a key it never saw; the (name, key) pair is now re-derived from the certificate the store parsed, and an entry carrying store metadata without that provenance is refused. The purposes and distrust dates come from the same place and are copied on the way out, so neither editing a store entry nor writing through a returned anchor changes what that anchor authorizes -- pki.trust.anchor(entry).purposes.serverAuth = true no longer opens a gate the store never opened, and an anchor reports the store's bits and dates however the caller has since handled the entry. A caller asserting their own bare (name, key) anchor carries no metadata and is unaffected.
|
|
44
|
+
- A private key decoded for the crypto engine is wiped once the engine has imported it. A signer or recipient key may be given as a Buffer, a Uint8Array, or a PEM string; the first is the caller's own memory and is used in place, while the other two are decoded into a new buffer inside the toolkit -- a second copy of a private key, which until now stayed readable in the heap until the garbage collector happened to reuse the page. It is cleared on the failure path too, so a malformed key or a tampered message is not a way to leave one behind. A Buffer you supply is never written to: it is yours, you still hold it, and clearing it would destroy the key rather than protect it.
|
|
45
|
+
- pki.webauthn.verifyAssertion holds both accepted forms of a stored credential key to the same rules. The COSE bytes went through the curve and length rules, the 2048-bit RSA modulus floor and the exponent checks; the object form went through none, so one key was refused in one form and imported for signature verification in the other. Which form a relying party stores is a question about what their datastore round-trips, not about how carefully their credential is checked.
|
|
46
|
+
- A certificate's keyUsage is read the same way at every boundary that asks what the certificate may do. keyUsage is a NamedBitList, so DER drops its trailing zero bits (X.690 sec. 11.2.2) and RFC 5280 sec. 4.2.1.3 requires at least one bit set. Four boundaries read the bits themselves and applied neither rule, so one certificate could be authorized in one place and called malformed everywhere else: pki.crl.verify accepting a CRL signer, pki.tsp.verify accepting a timestamp authority, pki.cms.encrypt accepting a recipient, and the FIDO metadata reader accepting the leaf that signs a catalogue.
|
|
47
|
+
- The distinguished-name comparison that decides name chaining, revocation-issuer matching and name constraints folds the four ASCII whitespace characters X.520's caseIgnoreMatch names, and no others. It had been collapsing whitespace with a pattern, which also folds vertical tab, form feed, no-break space and every Unicode space separator -- equating names X.520 keeps distinct.
|
|
48
|
+
|
|
7
49
|
## v0.5.4 — 2026-08-15
|
|
8
50
|
|
|
9
51
|
A path verdict says whether revocation was ever established, a trust anchor's own distrust metadata can no longer sit inert, and a CRL is asked what only a certificate can answer.
|
package/MIGRATING.md
CHANGED
|
@@ -7,3 +7,75 @@ Some breaking changes cannot warn at runtime: an on-disk format break or a wire-
|
|
|
7
7
|
## No active deprecations
|
|
8
8
|
|
|
9
9
|
The toolkit has no `deprecate()`-marked surface awaiting removal.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Out-of-band breaking changes
|
|
14
|
+
|
|
15
|
+
Listed newest-first.
|
|
16
|
+
|
|
17
|
+
### v0.5.6 — `try { pki.<verb>(...) } catch`
|
|
18
|
+
|
|
19
|
+
A verb documented `-> Promise` rejects on a bad input instead of throwing before the promise exists.
|
|
20
|
+
|
|
21
|
+
If you awaited the call, or attached `.catch`, nothing changes and there is nothing to do.
|
|
22
|
+
|
|
23
|
+
What changes is the undocumented shape: a synchronous `try`/`catch` that never consumed the
|
|
24
|
+
returned promise.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
try {
|
|
28
|
+
pki.cms.verify(bytes); // no await, no .catch
|
|
29
|
+
} catch (e) { /* used to fire on a malformed input */ }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That `catch` no longer runs, and the rejection surfaces as an unhandled one. It worked by
|
|
33
|
+
accident on exactly the verbs where a check happened to run before the promise existed --
|
|
34
|
+
`pki.cms.verify`, `pki.cms.sign`, `pki.cms.countersign`, `pki.ocsp.sign`, `pki.tsp.sign`, six
|
|
35
|
+
`pki.acme` verbs, and nine verbs on the client `pki.acme.client(...)` returns. Which verbs
|
|
36
|
+
those were was not visible from the call, which is why they are now uniform.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
await pki.cms.verify(bytes); // or pki.cms.verify(bytes).catch(handleIt)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### v0.5.6 — `pki.pkcs12.build(spec)`
|
|
43
|
+
|
|
44
|
+
An omitted password is refused rather than encoded as the empty one.
|
|
45
|
+
|
|
46
|
+
A store whose password option was missing or misspelled no longer builds silently under `""`.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
await pki.pkcs12.build(spec); // now pkcs12/bad-input
|
|
50
|
+
await pki.pkcs12.build(spec, { password: "" }); // the empty password, asked for
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you were relying on the default, the second form restores the previous output byte for
|
|
54
|
+
byte. `opts.integrity.mode` is validated the same way: a spelling other than `"public-key"`
|
|
55
|
+
is now `pkcs12/bad-integrity-mode` instead of silently selecting password integrity and
|
|
56
|
+
dropping the signer.
|
|
57
|
+
|
|
58
|
+
### v0.5.5 — `require("@blamejs/pki/lib/...")`
|
|
59
|
+
|
|
60
|
+
The package resolves one entry point; a path into the package no longer resolves.
|
|
61
|
+
|
|
62
|
+
`require("@blamejs/pki")` and `import ... from "@blamejs/pki"` are unchanged. What no
|
|
63
|
+
longer resolves is a path INTO the package:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
require("@blamejs/pki/lib/schema-x509") // ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Every module under `lib/` carries `@internal` in its own header and none has ever appeared
|
|
70
|
+
in the API snapshot that freezes the public surface. They were reachable because the package
|
|
71
|
+
declared no `exports` map, not because they were offered -- and one of them mints the
|
|
72
|
+
provenance record the OCSP and PKCS#12 integrity verbs rely on, which reachable from outside
|
|
73
|
+
could be minted for any object.
|
|
74
|
+
|
|
75
|
+
Everything the internals do is on `pki.*`: the decoders are `pki.schema.<format>.parse`, the
|
|
76
|
+
codec is `pki.asn1`, the OID registry is `pki.oid`, the error classes are `pki.errors`. If you
|
|
77
|
+
are reaching for something with no `pki.*` route, that is a gap worth reporting rather than a
|
|
78
|
+
module worth importing -- the internals change shape between patch releases and carry no
|
|
79
|
+
compatibility promise.
|
|
80
|
+
|
|
81
|
+
`require("@blamejs/pki/package.json")` still resolves, for tooling that reads the version.
|
package/lib/acme.js
CHANGED
|
@@ -684,6 +684,12 @@ function _payloadBuf(obj) {
|
|
|
684
684
|
if (obj === undefined) return Buffer.alloc(0); // POST-as-GET
|
|
685
685
|
return Buffer.from(JSON.stringify(obj), "utf8");
|
|
686
686
|
}
|
|
687
|
+
// A verb documented `-> Promise` refuses by REJECTING, never by throwing (guard-async).
|
|
688
|
+
//
|
|
689
|
+
// The validation stays SYNCHRONOUS: these verbs read a caller's options object -- the key, the
|
|
690
|
+
// nonce, the identifiers -- and resolving those before any turn passes is what stops a value being
|
|
691
|
+
// swapped between the check and the use. Only the exit changes.
|
|
692
|
+
var _promised = guard.async.deferred;
|
|
687
693
|
function _signOuter(o, payloadObj) {
|
|
688
694
|
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
689
695
|
if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
|
|
@@ -707,10 +713,12 @@ function _signOuter(o, payloadObj) {
|
|
|
707
713
|
* await pki.acme.postAsGet({ key, alg: "ES256", nonce, url: orderUrl, kid });
|
|
708
714
|
*/
|
|
709
715
|
function postAsGet(o) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
716
|
+
return _promised(function () {
|
|
717
|
+
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
718
|
+
// An authenticated read is ALWAYS kid-signed; copy only the kid-mode fields so a
|
|
719
|
+
// leftover jwk (e.g. reused from a newAccount options object) cannot embed a key.
|
|
720
|
+
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, undefined);
|
|
721
|
+
});
|
|
714
722
|
}
|
|
715
723
|
|
|
716
724
|
// A contact URL (RFC 8555 sec. 7.3): a `mailto:` addr-spec carries no header
|
|
@@ -750,6 +758,7 @@ function _assertContacts(contacts) {
|
|
|
750
758
|
* await pki.acme.newAccount({ key, alg: "ES256", nonce, url, jwk, termsOfServiceAgreed: true });
|
|
751
759
|
*/
|
|
752
760
|
function newAccount(o) {
|
|
761
|
+
return _promised(function () {
|
|
753
762
|
if (!_isObject(o) || !_isObject(o.jwk)) throw E("acme/bad-input", "newAccount must embed the account public jwk (RFC 8555 sec. 7.3)");
|
|
754
763
|
var payload = {};
|
|
755
764
|
if (o.contact !== undefined) { _assertContacts(o.contact); payload.contact = o.contact; }
|
|
@@ -769,6 +778,7 @@ function newAccount(o) {
|
|
|
769
778
|
payload.externalAccountBinding = o.externalAccountBinding;
|
|
770
779
|
}
|
|
771
780
|
return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, jwk: o.jwk }, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
|
|
781
|
+
});
|
|
772
782
|
}
|
|
773
783
|
|
|
774
784
|
var _HMAC_HASH = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
|
|
@@ -855,6 +865,7 @@ function _validateOrderIdentifier(id) {
|
|
|
855
865
|
* await pki.acme.newOrder({ key, alg: "ES256", nonce, url, kid, identifiers: [{ type: "dns", value: "example.org" }] });
|
|
856
866
|
*/
|
|
857
867
|
function newOrder(o) {
|
|
868
|
+
return _promised(function () {
|
|
858
869
|
if (!_isObject(o) || !Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-order", "newOrder requires a non-empty identifiers array (RFC 8555 sec. 7.4)");
|
|
859
870
|
// Serialize the CANONICAL { type, value } each validator returns, never the caller's objects (which may carry
|
|
860
871
|
// getter-backed / inherited fields JSON.stringify would drop, or extra enumerable fields it would send).
|
|
@@ -863,6 +874,7 @@ function newOrder(o) {
|
|
|
863
874
|
if (o.notAfter !== undefined) { if (!_isRfc3339(o.notAfter)) throw E("acme/bad-order", "notAfter must be an RFC 3339 date-time"); payload.notAfter = o.notAfter; }
|
|
864
875
|
if (o.replaces !== undefined) { if (!_isString(o.replaces)) throw E("acme/bad-order", "replaces must be an ARI certID string (RFC 9773 sec. 5)"); payload.replaces = o.replaces; }
|
|
865
876
|
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload);
|
|
877
|
+
});
|
|
866
878
|
}
|
|
867
879
|
|
|
868
880
|
/**
|
|
@@ -1035,10 +1047,12 @@ async function finalize(o) {
|
|
|
1035
1047
|
* await pki.acme.challengeResponse({ key, alg: "ES256", nonce, url: challUrl, kid });
|
|
1036
1048
|
*/
|
|
1037
1049
|
function challengeResponse(o) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1050
|
+
return _promised(function () {
|
|
1051
|
+
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
1052
|
+
var payload = o.payload !== undefined ? o.payload : {};
|
|
1053
|
+
if (!_isObject(payload)) throw E("acme/bad-input", "a challenge response payload must be a JSON object (RFC 8555 sec. 7.5.1)");
|
|
1054
|
+
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload);
|
|
1055
|
+
});
|
|
1042
1056
|
}
|
|
1043
1057
|
|
|
1044
1058
|
/**
|
|
@@ -1061,8 +1075,10 @@ function challengeResponse(o) {
|
|
|
1061
1075
|
* await pki.acme.deactivate({ key, alg: "ES256", nonce, url: authzUrl, kid });
|
|
1062
1076
|
*/
|
|
1063
1077
|
function deactivate(o) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1078
|
+
return _promised(function () {
|
|
1079
|
+
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
1080
|
+
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { status: "deactivated" });
|
|
1081
|
+
});
|
|
1066
1082
|
}
|
|
1067
1083
|
|
|
1068
1084
|
/**
|
|
@@ -1091,6 +1107,7 @@ function deactivate(o) {
|
|
|
1091
1107
|
* await pki.acme.revokeCert({ key, alg: "ES256", nonce, url, kid, certificate: certDer, reason: 1 });
|
|
1092
1108
|
*/
|
|
1093
1109
|
function revokeCert(o) {
|
|
1110
|
+
return _promised(function () {
|
|
1094
1111
|
if (!_isObject(o) || !Buffer.isBuffer(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate Buffer (opts.certificate)");
|
|
1095
1112
|
x509.parse(o.certificate); // structural validation of the target
|
|
1096
1113
|
var hasKid = Object.prototype.hasOwnProperty.call(o, "kid");
|
|
@@ -1106,6 +1123,7 @@ function revokeCert(o) {
|
|
|
1106
1123
|
var header = { alg: o.alg, nonce: o.nonce, url: o.url };
|
|
1107
1124
|
if (hasKid) header.kid = o.kid; else header.jwk = o.jwk;
|
|
1108
1125
|
return jose.sign({ protected: header, payload: _payloadBuf(payload), key: o.key, jwk: o.jwk, profile: "acme-outer" });
|
|
1126
|
+
});
|
|
1109
1127
|
}
|
|
1110
1128
|
|
|
1111
1129
|
/**
|
|
@@ -2015,12 +2033,18 @@ function client(directoryUrl, opts) {
|
|
|
2015
2033
|
});
|
|
2016
2034
|
});
|
|
2017
2035
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
function
|
|
2036
|
+
// A bad URL leaves as a REJECTION, like every other verb on this client. It is not only a caller
|
|
2037
|
+
// typo that reaches here: the message layer deliberately accepts http as well as https, so an
|
|
2038
|
+
// order whose `authorizations` array carries an http URL validates, and the very next step --
|
|
2039
|
+
// getAuthorization(order.authorizations[0]) -- threw out of the middle of an async ACME loop
|
|
2040
|
+
// rather than rejecting into the handler already wrapped around it.
|
|
2041
|
+
function _getOrder(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("order", _json(res)); }); }); }
|
|
2042
|
+
function _getAuthorization(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("authorization", _json(res)); }); }); }
|
|
2043
|
+
function _getChallenge(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("challenge", _json(res)); }); }); }
|
|
2044
|
+
function _respondToChallenge(url) { return _promised(function () { return _post(_clientUrl(url), challengeResponse, null, "kid").then(function (res) { return validate("challenge", _json(res)); }); }); }
|
|
2045
|
+
|
|
2046
|
+
function _finalize(order, o) { return _promised(function () { return _finalizeBody(order, o); }); }
|
|
2047
|
+
function _finalizeBody(order, o) {
|
|
2024
2048
|
o = o || {};
|
|
2025
2049
|
if (!_isObject(order) || !_isString(order.finalize)) throw E("acme/bad-input", "finalize requires the order object with its finalize URL");
|
|
2026
2050
|
if (!Array.isArray(order.identifiers)) throw E("acme/bad-input", "finalize requires the order's identifiers to enforce the RFC 8555 sec. 7.4 CSR-set match");
|
|
@@ -2155,10 +2179,11 @@ function client(directoryUrl, opts) {
|
|
|
2155
2179
|
});
|
|
2156
2180
|
}
|
|
2157
2181
|
|
|
2158
|
-
function _deactivateAccount() { return _post(_requireKid(), deactivate, null, "kid").then(function (res) { return validate("account", _json(res)); }); }
|
|
2159
|
-
function _deactivateAuthorization(url) { return _post(_clientUrl(url), deactivate, null, "kid").then(function (res) { return validate("authorization", _json(res)); }); }
|
|
2182
|
+
function _deactivateAccount() { return _promised(function () { return _post(_requireKid(), deactivate, null, "kid").then(function (res) { return validate("account", _json(res)); }); }); }
|
|
2183
|
+
function _deactivateAuthorization(url) { return _promised(function () { return _post(_clientUrl(url), deactivate, null, "kid").then(function (res) { return validate("authorization", _json(res)); }); }); }
|
|
2160
2184
|
|
|
2161
|
-
function _keyChange(o) {
|
|
2185
|
+
function _keyChange(o) { return _promised(function () { return _keyChangeBody(o); }); }
|
|
2186
|
+
function _keyChangeBody(o) {
|
|
2162
2187
|
o = o || {};
|
|
2163
2188
|
if (!o.newKey || !_isObject(o.newJwk) || !_isString(o.newAlg)) throw E("acme/bad-input", "keyChange requires newKey, newJwk, and newAlg");
|
|
2164
2189
|
var account = _requireKid();
|
|
@@ -2181,6 +2206,9 @@ function client(directoryUrl, opts) {
|
|
|
2181
2206
|
|
|
2182
2207
|
// ARI RenewalInfo (RFC 9773 sec. 4.1) -- the SOLE UNAUTHENTICATED GET (no JWS, no nonce).
|
|
2183
2208
|
function _renewalInfo(certDer, clockFn, retryAfterCapSeconds) {
|
|
2209
|
+
return _promised(function () { return _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds); });
|
|
2210
|
+
}
|
|
2211
|
+
function _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds) {
|
|
2184
2212
|
if (!Buffer.isBuffer(certDer)) throw E("acme/bad-input", "renewalInfo requires a DER certificate Buffer");
|
|
2185
2213
|
var _rawClk = typeof clockFn === "function" ? clockFn : clock; // renewalWindow may pass a per-call clock
|
|
2186
2214
|
// The clock MUST return finite epoch ms: a NaN / Infinity would make the expiry comparison below silently
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -83,12 +83,13 @@ var _tbsNameBytes = pkiBuild.tbsNameField; // the AA issuerName / holder baseC
|
|
|
83
83
|
// Parse a certificate DER/PEM (or accept a parsed certificate), re-typing a raw x509/* parse fault to the
|
|
84
84
|
// attrcert domain so a malformed AA cert / holder cert surfaces attrcert/*, not a foreign CertificateError.
|
|
85
85
|
function _parseCert(cert, what) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
// Re-derived from the bytes its parser read. Issuer and serial together ARE the Holder's identity,
|
|
87
|
+
// so a certificate assembled from parts could bind an attribute certificate to a holder that no
|
|
88
|
+
// issuer ever named.
|
|
89
|
+
return guard.parsed.acceptDerived(cert, "certificate", function (bytes) {
|
|
90
|
+
try { return x509.parse(bytes); }
|
|
91
|
+
catch (e) { if (e instanceof AttrCertError) throw e; throw _err("attrcert/bad-input", what + " is not a well-formed certificate", e); }
|
|
92
|
+
}, _err, "attrcert/bad-input", what);
|
|
92
93
|
}
|
|
93
94
|
// The raw content octets of an OBJECT IDENTIFIER (past its own tag+len) -- the body of a [0] IMPLICIT OID.
|
|
94
95
|
function _oidContent(name) {
|
|
@@ -149,8 +150,11 @@ function _encodeHolder(holder) {
|
|
|
149
150
|
if (holder.fromCertificate != null) {
|
|
150
151
|
// Bind to a public-key certificate's identity: baseCertificateID = { issuer = the PKC's issuer DN as a
|
|
151
152
|
// directoryName, serial = the PKC serialNumber } (RFC 5755 sec. 4.1 / 7.3).
|
|
153
|
+
// BOTH halves from the signed bytes. issuer and serial together ARE the identity, so deriving
|
|
154
|
+
// the issuer from tbsBytes while reading the serial off the object let the two name different
|
|
155
|
+
// certificates: a Holder with a genuine issuer DN and whatever serial the caller wrote.
|
|
152
156
|
var pkc = _parseCert(holder.fromCertificate, "holder.fromCertificate");
|
|
153
|
-
var content = _issuerSerialContent({ issuer: [{ directoryName: _tbsNameBytes(pkc, "issuer") }], serial: pkc
|
|
157
|
+
var content = _issuerSerialContent({ issuer: [{ directoryName: _tbsNameBytes(pkc, "issuer") }], serial: pkiBuild.tbsSerialNumber(pkc) });
|
|
154
158
|
return b.sequence([b.contextConstructed(0, content)]);
|
|
155
159
|
}
|
|
156
160
|
// objectDigestInfo [2] IMPLICIT ObjectDigestInfo.
|
package/lib/cmp-session.js
CHANGED
|
@@ -129,12 +129,24 @@ function _boundedPool(base, added) {
|
|
|
129
129
|
// the unsigned extraCerts ordering, so a corrupted-signature copy sharing a valid issuer's TBS must NOT collapse
|
|
130
130
|
// onto it and evict the valid one. Returns null when the identity cannot be derived; a NON-deduped entry is safe
|
|
131
131
|
// (a redundant slot), a wrong merge (dropping a distinct or the only valid certificate) is not.
|
|
132
|
+
// The identity comes from the SAME derivation every other certificate door uses: the bytes the
|
|
133
|
+
// parser recorded, never the fields of the object handed in. This is a dedupe rather than a verdict,
|
|
134
|
+
// and no collision attack on it is apparent -- carrying another certificate's exact tbsBytes AND
|
|
135
|
+
// signature means being that certificate. But "no attack is apparent" is the reasoning that put a
|
|
136
|
+
// completeness-only door on nine deciding boundaries, so it is not the reasoning this uses: one
|
|
137
|
+
// derivation for certificates, everywhere, and the exceptions have to argue for themselves.
|
|
138
|
+
//
|
|
139
|
+
// Failure returns null rather than throwing, which is this function's own contract and is why the
|
|
140
|
+
// door is wrapped: an underivable identity is a redundant pool slot, while a wrong merge (dropping
|
|
141
|
+
// a distinct or the only valid certificate) is not. So a rebuilt entry simply does not dedupe.
|
|
132
142
|
function _certIdentity(cert) {
|
|
133
143
|
try {
|
|
134
|
-
var p =
|
|
135
|
-
if (!
|
|
136
|
-
return p.tbsBytes.toString("base64") + "|" +
|
|
137
|
-
} catch (_e) {
|
|
144
|
+
var p = guard.parsed.acceptDerived(cert, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
|
|
145
|
+
if (!guard.parsed.isCert(p)) return null;
|
|
146
|
+
return p.tbsBytes.toString("base64") + "|" + p.signatureValue.bytes.toString("base64");
|
|
147
|
+
} catch (_e) {
|
|
148
|
+
return null; // underivable: kept as its own slot, never merged onto another certificate's
|
|
149
|
+
}
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
// A canonical identity for a SubjectPublicKeyInfo: the algorithm OID + the AlgorithmIdentifier parameters +
|
|
@@ -383,12 +395,13 @@ function session(opts) {
|
|
|
383
395
|
var _es = opts.expectedSender;
|
|
384
396
|
try {
|
|
385
397
|
if (_es && Buffer.isBuffer(_es.tbsBytes)) { // the documented already-parsed form (pki.schema.x509.parse output), detected like _certIdentity
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
398
|
+
// The door's RETURN is what gets pinned, not the object handed in. coerceCert re-derives the
|
|
399
|
+
// certificate from the bytes its parser read, so calling it only as a check and then storing
|
|
400
|
+
// the caller's object keeps every edit the re-derivation exists to discard: this pin is
|
|
401
|
+
// compared against each response signer's subject and SAN, so an edited one would accept a
|
|
402
|
+
// different CMP signer than the caller meant to pin. A validator that normalizes has its
|
|
403
|
+
// return value as its contract -- using it as a predicate throws that contract away.
|
|
404
|
+
_expectedSenderCert = (_engine && _engine.coerceCert) ? _engine.coerceCert(_es) : _es;
|
|
392
405
|
}
|
|
393
406
|
else if (Buffer.isBuffer(_es) || _es instanceof Uint8Array) { _expectedSenderDer = Buffer.from(_es); _expectedSenderCert = x509.parse(_expectedSenderDer); }
|
|
394
407
|
else if (typeof _es === "string") { _expectedSenderDer = x509.pemDecode(_es); _expectedSenderCert = x509.parse(_expectedSenderDer); }
|
package/lib/cmp-verify.js
CHANGED
|
@@ -506,11 +506,19 @@ async function _verifySignature(m, protectedPart, protectionAlg, protection, opt
|
|
|
506
506
|
// A canonical certificate identity (tbs + signature) for the extraCerts pool dedup: it keys a Buffer /
|
|
507
507
|
// Uint8Array (parse) and an already-parsed candidate object identically (path.build accepts both forms), so
|
|
508
508
|
// an extraCert duplicating a caller intermediate is dropped regardless of which representation the caller used.
|
|
509
|
+
// The key is derived from the bytes the parser recorded, through the same door every other
|
|
510
|
+
// certificate boundary uses -- never from the fields of the object handed in. Deriving it from a
|
|
511
|
+
// caller-shaped object would let two different certificates collapse onto one key, which is the one
|
|
512
|
+
// outcome this must not have. Failure returns null (the caller's contract here): an underivable
|
|
513
|
+
// identity leaves a redundant pool slot, which is safe, while a wrong merge is not.
|
|
509
514
|
function _certKey(c) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
515
|
+
try {
|
|
516
|
+
var p = guard.parsed.acceptDerived(c, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
|
|
517
|
+
if (!guard.parsed.isCert(p)) return null;
|
|
518
|
+
return p.tbsBytes.toString("base64") + "|" + p.signatureValue.bytes.toString("base64");
|
|
519
|
+
} catch (_e) {
|
|
520
|
+
return null; // underivable: kept as its own slot, never merged onto another certificate's
|
|
521
|
+
}
|
|
514
522
|
}
|
|
515
523
|
|
|
516
524
|
async function _chainSigner(signer, m, opts, extra) {
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -211,41 +211,55 @@ async function _acquireCek(ri, km, opts) {
|
|
|
211
211
|
// ktri: OAEP or PKCS#1 v1.5 (v1.5 = decrypt-only + RFC 3218 implicit rejection).
|
|
212
212
|
async function _ktriCek(ri, km) {
|
|
213
213
|
var kea = ri.keyEncryptionAlgorithm;
|
|
214
|
-
var
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
var k = _normKeyDer(km.key);
|
|
215
|
+
// The wipe is in a `finally` so it happens on the reject path too: a message crafted to fail the
|
|
216
|
+
// unwrap must not be a way to leave the key copy in memory.
|
|
217
|
+
try {
|
|
218
|
+
if (kea.oid === O("rsaesOaep")) {
|
|
219
|
+
var hash = _oaepHashFromParams(kea.parameters);
|
|
220
|
+
var pub = await subtle.importKey("pkcs8", k.der, { name: "RSA-OAEP", hash: hash }, false, ["decrypt"]);
|
|
221
|
+
return Buffer.from(await subtle.decrypt({ name: "RSA-OAEP" }, pub, ri.encryptedKey));
|
|
222
|
+
}
|
|
223
|
+
if (kea.oid === O("rsaEncryption")) {
|
|
224
|
+
// RFC 3218 sec. 2.3.2 implicit rejection: NEVER surface a v1.5 failure here. Any decode fault
|
|
225
|
+
// yields a fresh random CEK of the content-alg length; the mismatch emerges at stage 3.
|
|
226
|
+
var keyObj = nodeCrypto.createPrivateKey({ key: k.der, format: "der", type: "pkcs8" });
|
|
227
|
+
try { return nodeCrypto.privateDecrypt({ key: keyObj, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }, ri.encryptedKey); }
|
|
228
|
+
catch (_e) {
|
|
229
|
+
return null; // signal: use a random CEK (length decided at open time)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
|
|
233
|
+
// emits OAEP or rsaEncryption) -- a fail-closed reject the fuzz harness exercises.
|
|
234
|
+
throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
|
|
235
|
+
} finally {
|
|
236
|
+
_releaseKeyDer(k);
|
|
226
237
|
}
|
|
227
|
-
// Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
|
|
228
|
-
// emits OAEP or rsaEncryption) -- a fail-closed reject the fuzz harness exercises.
|
|
229
|
-
throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
// kari: reconstruct Z from the originatorKey + recipient private key, KDF -> KEK, AES-KW unwrap.
|
|
233
241
|
async function _kariCek(ri, km) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// several recipients under one ephemeral key.
|
|
241
|
-
var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
|
|
242
|
-
var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
|
|
243
|
-
if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
|
|
244
|
-
var ukm = ri.ukm || null;
|
|
242
|
+
// The copy is taken and the protected region opens IMMEDIATELY. Everything between them would
|
|
243
|
+
// otherwise be an unprotected window, and it is not a narrow one: reading the wrap algorithm, the
|
|
244
|
+
// originator's key, the recipient's certificate and the matching RecipientEncryptedKey all parse
|
|
245
|
+
// attacker-supplied structure and all throw on malformed input. A message crafted to fail any one
|
|
246
|
+
// of them would be a way to leave the key copy in the heap -- the exact outcome the wipe is for.
|
|
247
|
+
var k = _normKeyDer(km.key);
|
|
245
248
|
// The agreement secret and the KEK derived from it are both allocated here; one `finally` clears
|
|
246
249
|
// whichever branch produced them, including when the unwrap below throws on a tampered key.
|
|
247
250
|
var kek, z, mz;
|
|
248
251
|
try {
|
|
252
|
+
var keyDer = k.der;
|
|
253
|
+
var kea = ri.keyEncryptionAlgorithm;
|
|
254
|
+
var wrapAlg = _kariWrap(kea);
|
|
255
|
+
var scheme = kea.oid;
|
|
256
|
+
var origSpki = _originatorSpki(ri.originator);
|
|
257
|
+
// Unwrap THIS recipient's RecipientEncryptedKey (matched by rid), not element 0 -- a kari may list
|
|
258
|
+
// several recipients under one ephemeral key.
|
|
259
|
+
var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
|
|
260
|
+
var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
|
|
261
|
+
if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
|
|
262
|
+
var ukm = ri.ukm || null;
|
|
249
263
|
if (_isMont(origSpki)) {
|
|
250
264
|
var mont = _montName(origSpki);
|
|
251
265
|
var recipPriv = await subtle.importKey("pkcs8", keyDer, { name: mont.name }, false, ["deriveBits"]);
|
|
@@ -275,6 +289,7 @@ async function _kariCek(ri, km) {
|
|
|
275
289
|
return await _aesKwUnwrap(kek, rek.encryptedKey);
|
|
276
290
|
} finally {
|
|
277
291
|
guard.secret.zeroizeAll([z, mz, kek], CmsError, "cms/bad-input", "the key-agreement shared secret");
|
|
292
|
+
_releaseKeyDer(k);
|
|
278
293
|
}
|
|
279
294
|
}
|
|
280
295
|
|
|
@@ -333,7 +348,12 @@ async function _kemriCek(ri, km) {
|
|
|
333
348
|
var kekBytes = Number(k.kekLength);
|
|
334
349
|
var wrapAlg = k.wrap;
|
|
335
350
|
if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail(); // M29 re-check on the consumer path
|
|
336
|
-
|
|
351
|
+
// The decapsulation key copy is released as soon as the engine has imported it -- the import is
|
|
352
|
+
// the only thing that reads it, so its lifetime does not need to span the decapsulation below.
|
|
353
|
+
var keyCopy = _normKeyDer(km.key);
|
|
354
|
+
var priv;
|
|
355
|
+
try { priv = await subtle.importKey("pkcs8", keyCopy.der, { name: wcName }, false, ["decapsulateBits"]); }
|
|
356
|
+
finally { _releaseKeyDer(keyCopy); }
|
|
337
357
|
var ss = null, kek = null, ssAb = null, kekAb = null;
|
|
338
358
|
try {
|
|
339
359
|
// The engine hands back an ArrayBuffer it allocated, and the Buffer below is a copy of it. Both
|
|
@@ -706,12 +726,33 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
706
726
|
// * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
|
|
707
727
|
// ktri (this producer emits OAEP); its behaviour is identical to the tested below-floor path (a
|
|
708
728
|
// random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
|
|
729
|
+
// _normKeyDer(key) -> { der, owned } -- the recipient private key as PKCS#8 DER, and whether the
|
|
730
|
+
// buffer is one THIS module made.
|
|
731
|
+
//
|
|
732
|
+
// The distinction decides who may wipe it. A caller handing in their own Buffer keeps a live
|
|
733
|
+
// reference and will use it again; wiping that would destroy the key out from under them. The other
|
|
734
|
+
// two forms produce a NEW buffer here -- a Uint8Array is copied, a PEM string is decoded -- and that
|
|
735
|
+
// buffer is a second copy of a private key which nothing else can reach, so it lives until the
|
|
736
|
+
// garbage collector happens to reuse the page unless this module clears it.
|
|
737
|
+
//
|
|
738
|
+
// Returning the flag rather than always copying keeps the caller's buffer un-duplicated: making our
|
|
739
|
+
// own copy of every key so we could uniformly wipe it would ADD a copy of the secret to solve the
|
|
740
|
+
// problem of having one.
|
|
709
741
|
function _normKeyDer(key) {
|
|
710
|
-
if (Buffer.isBuffer(key)) return key;
|
|
711
|
-
if (key instanceof Uint8Array) return Buffer.from(key);
|
|
712
|
-
if (typeof key === "string") {
|
|
742
|
+
if (Buffer.isBuffer(key)) return { der: key, owned: false };
|
|
743
|
+
if (key instanceof Uint8Array) return { der: Buffer.from(key), owned: true };
|
|
744
|
+
if (typeof key === "string") {
|
|
745
|
+
var der;
|
|
746
|
+
try { der = pkcs8.pemDecode(key); }
|
|
747
|
+
catch (e) { throw _err("cms/bad-input", "the recipient private-key PEM could not be decoded", e); }
|
|
748
|
+
return { der: der, owned: true };
|
|
749
|
+
}
|
|
713
750
|
throw _err("cms/bad-input", "the recipient private key must be a PKCS#8 DER Buffer or PEM string");
|
|
714
751
|
}
|
|
752
|
+
// Wipe a key buffer this module owns. A no-op for the caller's own Buffer, which is theirs.
|
|
753
|
+
function _releaseKeyDer(k) {
|
|
754
|
+
if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
|
|
755
|
+
}
|
|
715
756
|
function _normCertDer(cert) {
|
|
716
757
|
if (Buffer.isBuffer(cert)) return cert;
|
|
717
758
|
if (cert instanceof Uint8Array) return Buffer.from(cert);
|
package/lib/cms-sign.js
CHANGED
|
@@ -337,7 +337,13 @@ function _pemToDer(text) {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
// pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
340
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
|
|
341
|
+
// synchronous because they read the caller's mutable content and signer list.
|
|
340
342
|
function sign(content, signers, opts) {
|
|
343
|
+
return guard.async.deferred(function () { return _sign(content, signers, opts); });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _sign(content, signers, opts) {
|
|
341
347
|
opts = opts || {};
|
|
342
348
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.sign options must be an object");
|
|
343
349
|
var contentBuf = _toBuf(content, "content");
|
|
@@ -533,7 +539,12 @@ function _targetPreimage(siNode, opts) {
|
|
|
533
539
|
}
|
|
534
540
|
|
|
535
541
|
// pki.cms.countersign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
542
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async).
|
|
536
543
|
function countersign(cmsInput, signers, opts) {
|
|
544
|
+
return guard.async.deferred(function () { return _countersign(cmsInput, signers, opts); });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function _countersign(cmsInput, signers, opts) {
|
|
537
548
|
opts = opts || {};
|
|
538
549
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.countersign options must be an object");
|
|
539
550
|
var list = Array.isArray(signers) ? signers : [signers];
|