@blamejs/pki 0.5.5 → 0.5.7
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 +63 -0
- package/lib/acme.js +47 -19
- package/lib/attrcert-sign.js +5 -1
- package/lib/cmc-build.js +8 -13
- package/lib/cmc-verify.js +2 -2
- package/lib/cmp-build.js +7 -2
- package/lib/cmp-session.js +4 -2
- package/lib/cmp-verify.js +1 -1
- package/lib/cms-compress.js +1 -2
- package/lib/cms-decrypt.js +2 -4
- package/lib/cms-encrypt.js +1 -2
- package/lib/cms-sign.js +42 -4
- package/lib/cms-verify.js +68 -17
- package/lib/crl-sign.js +9 -3
- package/lib/crmf-sign.js +5 -1
- package/lib/csr-sign.js +5 -1
- package/lib/est.js +5 -4
- package/lib/guard-all.js +2 -0
- package/lib/guard-async.js +37 -0
- package/lib/guard-bytes.js +368 -5
- package/lib/guard-parsed.js +117 -9
- package/lib/ocsp.js +24 -6
- package/lib/pbes2.js +11 -1
- package/lib/pkcs12-build.js +90 -27
- package/lib/pki-build.js +2 -3
- package/lib/schema-cms.js +150 -2
- package/lib/sign-scheme.js +7 -4
- package/lib/sigstore.js +10 -0
- package/lib/tsp-sign.js +116 -19
- package/lib/validator-tpm.js +1 -1
- package/lib/webauthn-mds.js +1 -1
- package/lib/webauthn.js +1 -1
- package/lib/x509-sign.js +11 -2
- package/package.json +1 -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.7 — 2026-08-16
|
|
8
|
+
|
|
9
|
+
A CMS signature made over signed attributes can no longer be re-presented as one made over content.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The pki.cms.verify verdict carries eContentType, and each signers[i] carries signedAttributesPresent. Signing with attributes and signing the content directly are different claims -- attributes bind a content type and a signing time alongside the digest, content-only binds nothing but the bytes -- and one message may carry a signer of each. A caller whose profile is stricter than RFC 5652's, such as RFC 8551 S/MIME which requires signed attributes, can now enforce that from the verdict instead of parsing the message a second time. A check that needs a second parse is a check most callers will not write.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Because the producing verbs now copy their arguments at entry, each property of a spec or options object -- own or inherited -- is read exactly once, when the verb is called. A field defined as a getter is therefore evaluated at that point even if the verb has no use for it, and a getter that throws surfaces as that module's bad-input fault before its own validation of any other field. Reading each property once is the point rather than a side effect: a getter consulted twice can answer differently the second time, which is the same problem the copy exists to remove. Plain data specs are unaffected.
|
|
18
|
+
- One argument shape is refused rather than copied: an object whose state this toolkit cannot read -- a WeakMap or WeakSet, a promise, a CryptoKey -- carrying its own named fields alongside. There is no safe handling for it, because it cannot be copied and passing it through would leave those fields changeable after the checks had read them, so it fails with the module's bad-input code and says to pass the fields as a plain object. The same objects are accepted as before when they carry only what their kind defines, which is what a real key, a real promise and a real WeakMap do.
|
|
19
|
+
- SECURITY.md previously said an attacker could "neither swap the content out from under a set of signed attributes, nor strip the attributes and present a signature made over them as one made over the content". The first half was true; the second was not, and had not been since the claim was written. The entry now describes what is actually defended and how, and names the case it costs: content which genuinely is an encoded SignedAttributes block must be signed WITH signed attributes. The v0.5.6 notes described the parsed-object re-derivation as closing this forgery; it closed the half reachable through a caller-assembled object, and this release closes the half reachable from bytes.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- pki.cms.verify refuses a SignerInfo with no signed attributes whose content is itself an encoded SignedAttributes block, as cms/ambiguous-content. This is Attack Type 1 of draft-vangeest-lamps-cms-euf-cma-signeddata: take a message signed with attributes present, drop the signedAttrs field, set the encapsulated content to the DER of those attributes, keep the signature. The signature genuinely verifies over those bytes -- the refusal is the shape, not a failed signature check, which is why it has its own code rather than reading as cms/bad-signature. The condition is necessary to the attack rather than a guess at anything SET OF shaped: RFC 5652 section 5.3 requires signed attributes to carry both a content-type and a message-digest attribute, so every message the attack produces has content carrying both, and content that is a set of attributes missing either one is not refused. Ordinary content -- a certificate, a JSON payload, arbitrary bytes -- does not have the shape at all. Verified against the shipped verb before and after, and the standards fixes for this are protocol changes (signing under a context string that names the mode) which no verifier can apply on its own.
|
|
24
|
+
- pki.cms.sign refuses to sign content that is itself an encoded SignedAttributes block when signedAttributes is false. That is the other direction of the same problem (Attack Type 2): such a signature can afterwards be promoted into an attributes-present message, because the signature does not commit to which mode was used -- the attacker attaches the signed bytes AS the SignedAttributes and swaps in whatever content their message-digest attribute names. Refusing to mint the ambiguous signature is the only point at which that direction can be stopped. Sign the same content WITH signed attributes and it is unambiguous again.
|
|
25
|
+
- A byte argument whose backing store has been transferred away is refused instead of read as empty. Transferring an ArrayBuffer -- a structuredClone with transfer, a worker hand-off, a stream that adopts the buffer -- leaves every view of it reading zero-length rather than throwing, so a boundary that passed the caller's object straight on operated on nothing and succeeded: pki.cms.sign produced a sound, verifiable signature covering no content at all, pki.cms.compress the same, and pki.pkcs12.build derived its MAC and encryption keys from the empty password. Every boundary that takes caller bytes now re-views the input first and refuses a detached one with that module's own bad-input code. Where the empty read already failed further down -- an empty certificate does not parse, an empty private key does not import -- the refusal now carries the calling module's code and names the argument, rather than surfacing whatever the later failure raised.
|
|
26
|
+
- A producing verb reads its arguments once, at entry. Every one of them does work across more than one promise turn, so a caller still holding a spec, an options object or a signer could change a field after the call returned and have a later turn read the new value -- the checks ran against one input and the artifact was built from another. Every argument of pki.cms.sign, pki.cms.countersign, pki.x509.sign, pki.csr.sign, pki.crl.sign, pki.attrcert.sign, pki.crmf.build, pki.cmc.build, pki.cmp.build, pki.ocsp.buildRequest, pki.ocsp.sign, pki.tsp.sign and pki.pkcs12.build is now copied whole at entry, at every depth, and each copy is cleared when the call settles. Reachable cases included flipping signedAttributes from true to false to skip the content check the entry above describes, rewriting a certificate's key identifier or a CRL's authority key identifier between the check and the encoding, changing the encoding pki.x509.sign returns after the signature came back, rewriting the PKCS#12 password partway through so the file's MAC and its bag encryption were keyed to two different values, and rewriting the nested pki.cmp.build MAC secret so the message went out authenticated under a value the caller never supplied. Copying at one level does not cover the last of those and copying without clearing duplicates the secret, so both halves are the rule. A parsed structure passed inside a spec keeps its identity rather than being copied, so it still satisfies the verbs that require parser provenance, and a CryptoKey is used rather than cloned.
|
|
27
|
+
- The verbs documented as returning a Promise now run their body at the call, not a turn later. Ten of them deferred everything -- including reading the caller's arguments -- until after the call had already returned, which left the window above open even for a verb that copies its input on the first line. They still report a fault by rejecting rather than throwing; only the timing of the work changed.
|
|
28
|
+
|
|
29
|
+
## v0.5.6 — 2026-08-15
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- 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.
|
|
36
|
+
- 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.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- 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.
|
|
41
|
+
- 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 "".
|
|
42
|
+
- 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.
|
|
43
|
+
- 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.
|
|
44
|
+
- 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.
|
|
45
|
+
- 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.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
7
49
|
## v0.5.5 — 2026-08-15
|
|
8
50
|
|
|
9
51
|
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.
|
package/MIGRATING.md
CHANGED
|
@@ -14,6 +14,69 @@ The toolkit has no `deprecate()`-marked surface awaiting removal.
|
|
|
14
14
|
|
|
15
15
|
Listed newest-first.
|
|
16
16
|
|
|
17
|
+
### v0.5.7 — `content that is an encoded SignedAttributes block`
|
|
18
|
+
|
|
19
|
+
Signing or verifying such content WITHOUT signed attributes is refused as cms/ambiguous-content.
|
|
20
|
+
|
|
21
|
+
A CMS signature does not commit to whether signed attributes were present, so a signature made
|
|
22
|
+
over a SignedAttributes block can be re-presented as one made over content. The shape is now
|
|
23
|
+
refused at both ends.
|
|
24
|
+
|
|
25
|
+
This only affects you if your CMS content genuinely IS a DER SET OF Attribute carrying both a
|
|
26
|
+
content-type and a message-digest attribute -- the shape RFC 5652 sec. 5.3 gives a
|
|
27
|
+
SignedAttributes -- AND you sign it with `signedAttributes: false`. Ordinary content is
|
|
28
|
+
unaffected, and so is a set of attributes missing either of those two.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
await pki.cms.sign(attrShapedContent, signer, { signedAttributes: false }); // cms/ambiguous-content
|
|
32
|
+
await pki.cms.sign(attrShapedContent, signer); // signed attributes: fine
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Signing it WITH signed attributes makes the message unambiguous and it verifies normally.
|
|
36
|
+
Existing messages of this shape already in your archive will not verify; re-sign them with
|
|
37
|
+
signed attributes.
|
|
38
|
+
|
|
39
|
+
### v0.5.6 — `try { pki.<verb>(...) } catch`
|
|
40
|
+
|
|
41
|
+
A verb documented `-> Promise` rejects on a bad input instead of throwing before the promise exists.
|
|
42
|
+
|
|
43
|
+
If you awaited the call, or attached `.catch`, nothing changes and there is nothing to do.
|
|
44
|
+
|
|
45
|
+
What changes is the undocumented shape: a synchronous `try`/`catch` that never consumed the
|
|
46
|
+
returned promise.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
try {
|
|
50
|
+
pki.cms.verify(bytes); // no await, no .catch
|
|
51
|
+
} catch (e) { /* used to fire on a malformed input */ }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That `catch` no longer runs, and the rejection surfaces as an unhandled one. It worked by
|
|
55
|
+
accident on exactly the verbs where a check happened to run before the promise existed --
|
|
56
|
+
`pki.cms.verify`, `pki.cms.sign`, `pki.cms.countersign`, `pki.ocsp.sign`, `pki.tsp.sign`, six
|
|
57
|
+
`pki.acme` verbs, and nine verbs on the client `pki.acme.client(...)` returns. Which verbs
|
|
58
|
+
those were was not visible from the call, which is why they are now uniform.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
await pki.cms.verify(bytes); // or pki.cms.verify(bytes).catch(handleIt)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### v0.5.6 — `pki.pkcs12.build(spec)`
|
|
65
|
+
|
|
66
|
+
An omitted password is refused rather than encoded as the empty one.
|
|
67
|
+
|
|
68
|
+
A store whose password option was missing or misspelled no longer builds silently under `""`.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
await pki.pkcs12.build(spec); // now pkcs12/bad-input
|
|
72
|
+
await pki.pkcs12.build(spec, { password: "" }); // the empty password, asked for
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If you were relying on the default, the second form restores the previous output byte for
|
|
76
|
+
byte. `opts.integrity.mode` is validated the same way: a spelling other than `"public-key"`
|
|
77
|
+
is now `pkcs12/bad-integrity-mode` instead of silently selecting password integrity and
|
|
78
|
+
dropping the signer.
|
|
79
|
+
|
|
17
80
|
### v0.5.5 — `require("@blamejs/pki/lib/...")`
|
|
18
81
|
|
|
19
82
|
The package resolves one entry point; a path into the package no longer resolves.
|
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
|
@@ -456,7 +456,11 @@ function _buildExtensions(extSpec, aaSpki) {
|
|
|
456
456
|
* pki.schema.attrcert.parse(ac).attributes[0].type; // the role attribute OID
|
|
457
457
|
*/
|
|
458
458
|
function sign(spec, issuer, opts) {
|
|
459
|
-
|
|
459
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
460
|
+
// on the same call in x509-sign.
|
|
461
|
+
return guard.bytes.fixedCall(AttrCertError, "attrcert/bad-input", [
|
|
462
|
+
[spec, "the attribute-certificate spec"], [issuer, "the issuer"], [opts, "pki.attrcert.sign options"],
|
|
463
|
+
], _sign);
|
|
460
464
|
}
|
|
461
465
|
|
|
462
466
|
function _sign(spec, issuer, opts) {
|
package/lib/cmc-build.js
CHANGED
|
@@ -365,8 +365,7 @@ function _assertNoDuplicateBinding(callerControls, spec) {
|
|
|
365
365
|
function _asBigInt(v, what) { return guard.range.authoredInteger(v, E, "cmc/bad-input", what); }
|
|
366
366
|
|
|
367
367
|
function _der(v, what) {
|
|
368
|
-
if (Buffer.isBuffer(v)) return v;
|
|
369
|
-
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
368
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmcError, "cmc/bad-input", what);
|
|
370
369
|
throw E("cmc/bad-input", what + " must be DER bytes");
|
|
371
370
|
}
|
|
372
371
|
function _oidOf(v) {
|
|
@@ -481,17 +480,13 @@ function popLinkWitnessV2(secret, R) {
|
|
|
481
480
|
// The operator-facing @primitive block for this function lives beside its
|
|
482
481
|
// re-export in cmc-verify.js, the pki.cmc @module home.
|
|
483
482
|
function build(spec, signer, opts) {
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return _build(spec, signer, opts);
|
|
492
|
-
} catch (e) {
|
|
493
|
-
return Promise.reject(e); // the surface stays promise-rejecting, never throwing
|
|
494
|
-
}
|
|
483
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
484
|
+
// on the same call in x509-sign. It matters most here: the Identity Proof and POP Link witnesses
|
|
485
|
+
// are computed over the bytes this builder is about to emit, so a spec that changed in between
|
|
486
|
+
// would witness a different request than the one that goes out.
|
|
487
|
+
return guard.bytes.fixedCall(CmcError, "cmc/bad-input", [
|
|
488
|
+
[spec, "the CMC request spec"], [signer, "the signer"], [opts, "pki.cmc.build options"],
|
|
489
|
+
], _build);
|
|
495
490
|
}
|
|
496
491
|
|
|
497
492
|
function _build(spec, signer, opts) {
|
package/lib/cmc-verify.js
CHANGED
|
@@ -318,7 +318,7 @@ function verify(response, sent) {
|
|
|
318
318
|
} catch (e) {
|
|
319
319
|
return Promise.reject(e); // the surface stays promise-rejecting, never throwing
|
|
320
320
|
}
|
|
321
|
-
return
|
|
321
|
+
return guard.async.deferred(function () { return _verify(frozenResponse, frozenSent); });
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function _assertOpts(sent) {
|
|
@@ -354,7 +354,7 @@ function _snapshotIfBytes(input) {
|
|
|
354
354
|
// the very gap this closes -- reopening the window for exactly the inputs that came in by the
|
|
355
355
|
// wider one. A DataView is copied over its OWN window, not the whole buffer it happens to sit in.
|
|
356
356
|
function _copyAnyBytes(v) {
|
|
357
|
-
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return
|
|
357
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmcError, "cmc/bad-input", "a byte field of the request");
|
|
358
358
|
if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
|
|
359
359
|
if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
|
|
360
360
|
return v;
|
package/lib/cmp-build.js
CHANGED
|
@@ -550,7 +550,12 @@ function _resolveProtection(opts) {
|
|
|
550
550
|
// ---- orchestrator ----
|
|
551
551
|
|
|
552
552
|
function build(message, opts) {
|
|
553
|
-
|
|
553
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
554
|
+
// on the same call in x509-sign. `opts.mac.secret` is why the copy has to be deep: it sits a
|
|
555
|
+
// level below the options object and is read by the PBMAC1 derivation after the first turn.
|
|
556
|
+
return guard.bytes.fixedCall(CmpError, "cmp/bad-input", [
|
|
557
|
+
[message, "the PKIMessage spec"], [opts, "pki.cmp.build options"],
|
|
558
|
+
], _build);
|
|
554
559
|
}
|
|
555
560
|
|
|
556
561
|
function _build(message, opts) {
|
|
@@ -733,7 +738,7 @@ function _classifyCmpResponse(status, headers, body, tls) {
|
|
|
733
738
|
}
|
|
734
739
|
|
|
735
740
|
function transfer(url, message, opts) {
|
|
736
|
-
return
|
|
741
|
+
return guard.async.deferred(function () { return _transfer(url, message, opts); });
|
|
737
742
|
}
|
|
738
743
|
|
|
739
744
|
function _transfer(url, message, opts) {
|
package/lib/cmp-session.js
CHANGED
|
@@ -892,8 +892,10 @@ function session(opts) {
|
|
|
892
892
|
catch (_e) { /* allow:swallow-unverified an unparseable p10cr CSR fails closed at the cmp.build boundary; the key-match is simply not applied to a request that never sends */ return null; }
|
|
893
893
|
}
|
|
894
894
|
var pk = armSpec && armSpec.certTemplate ? armSpec.certTemplate.publicKey : null;
|
|
895
|
-
|
|
896
|
-
|
|
895
|
+
// SNAPSHOT: this is held in session state across the transport round trip and then compared
|
|
896
|
+
// against the issued certificate's key, so an alias would let the caller rewrite the key the
|
|
897
|
+
// response is checked against after the request went out.
|
|
898
|
+
if (Buffer.isBuffer(pk) || pk instanceof Uint8Array) return guard.bytes.snapshot(pk, CmpError, "cmp/bad-input", "the certTemplate publicKey");
|
|
897
899
|
return null;
|
|
898
900
|
}
|
|
899
901
|
|
package/lib/cmp-verify.js
CHANGED
|
@@ -618,7 +618,7 @@ function _nonEmptySecret(s) {
|
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
function verify(message, opts) {
|
|
621
|
-
return
|
|
621
|
+
return guard.async.deferred(function () { return _verify(message, opts); });
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
async function _verify(message, opts) {
|
package/lib/cms-compress.js
CHANGED
|
@@ -86,8 +86,7 @@ async function decompress(input, opts) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function _toDer(input) {
|
|
89
|
-
if (Buffer.isBuffer(input)) return input;
|
|
90
|
-
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
89
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) return guard.bytes.snapshot(input, CmsError, "cms/bad-input", "input");
|
|
91
90
|
if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
|
|
92
91
|
throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
|
|
93
92
|
}
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -106,8 +106,7 @@ function _parse(input) {
|
|
|
106
106
|
return schemaCms.parse(_toDer(input));
|
|
107
107
|
}
|
|
108
108
|
function _toDer(input) {
|
|
109
|
-
if (Buffer.isBuffer(input)) return input;
|
|
110
|
-
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
109
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) return guard.bytes.snapshot(input, CmsError, "cms/bad-input", "input");
|
|
111
110
|
if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
|
|
112
111
|
throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
|
|
113
112
|
}
|
|
@@ -754,8 +753,7 @@ function _releaseKeyDer(k) {
|
|
|
754
753
|
if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
|
|
755
754
|
}
|
|
756
755
|
function _normCertDer(cert) {
|
|
757
|
-
if (Buffer.isBuffer(cert)) return cert;
|
|
758
|
-
if (cert instanceof Uint8Array) return Buffer.from(cert);
|
|
756
|
+
if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.snapshot(cert, CmsError, "cms/bad-input", "the recipient certificate");
|
|
759
757
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", "the recipient certificate PEM could not be decoded", e); } }
|
|
760
758
|
throw _err("cms/bad-input", "the recipient certificate must be a DER Buffer or PEM string");
|
|
761
759
|
}
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -40,8 +40,7 @@ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(nam
|
|
|
40
40
|
// A certificate descriptor -> raw DER (the recipient cert is parsed for dispatch + rid; the caller
|
|
41
41
|
// supplies bytes, not a re-encoded parse).
|
|
42
42
|
function _normCertDer(cert, what) {
|
|
43
|
-
if (Buffer.isBuffer(cert)) return cert;
|
|
44
|
-
if (cert instanceof Uint8Array) return Buffer.from(cert);
|
|
43
|
+
if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.snapshot(cert, CmsError, "cms/bad-input", what || "a certificate");
|
|
45
44
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
|
|
46
45
|
throw _err("cms/bad-input", (what || "a certificate") + " must be a DER Buffer, Uint8Array, or PEM string");
|
|
47
46
|
}
|
package/lib/cms-sign.js
CHANGED
|
@@ -325,8 +325,10 @@ function _keyOnlyKeyId(so) {
|
|
|
325
325
|
|
|
326
326
|
function _normCertDer(c) {
|
|
327
327
|
if (c == null) throw _err("cms/bad-input", "each signer requires a certificate (cert)");
|
|
328
|
-
if (c instanceof Uint8Array
|
|
329
|
-
|
|
328
|
+
if (c instanceof Uint8Array || Buffer.isBuffer(c)) {
|
|
329
|
+
c = guard.bytes.snapshot(c, CmsError, "cms/bad-input", "a signer certificate");
|
|
330
|
+
return c[0] === 0x30 ? c : _pemToDer(c.toString("latin1")); // DER as-is, else PEM
|
|
331
|
+
}
|
|
330
332
|
if (typeof c === "string") return _pemToDer(c);
|
|
331
333
|
throw _err("cms/bad-input", "a signer certificate must be a DER Buffer or a PEM string");
|
|
332
334
|
}
|
|
@@ -337,9 +339,24 @@ function _pemToDer(text) {
|
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
// pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
342
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
|
|
343
|
+
// synchronous because they read the caller's mutable content and signer list.
|
|
340
344
|
function sign(content, signers, opts) {
|
|
345
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
346
|
+
// on the same call in x509-sign. Here it is what makes the attribute-shaped-content refusal below
|
|
347
|
+
// hold: the value that decides it and the value that gets signed are now the same read.
|
|
348
|
+
return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
|
|
349
|
+
[content, "content"], [signers, "the signer list"], [opts, "pki.cms.sign options"],
|
|
350
|
+
], _sign);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function _sign(content, signers, opts) {
|
|
341
354
|
opts = opts || {};
|
|
342
355
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.sign options must be an object");
|
|
356
|
+
// The arguments were copied at entry (see `sign` above), which is what makes the refusal below
|
|
357
|
+
// hold: flipping signedAttributes from true to false after the call returns would otherwise skip
|
|
358
|
+
// the attribute-shaped-content check while the signer signs that content directly -- the very
|
|
359
|
+
// signature the stripping attack needs.
|
|
343
360
|
var contentBuf = _toBuf(content, "content");
|
|
344
361
|
var list = Array.isArray(signers) ? signers : [signers];
|
|
345
362
|
if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
|
|
@@ -363,6 +380,18 @@ function sign(content, signers, opts) {
|
|
|
363
380
|
if (opts.signedAttributes === false && eContentType !== OID_DATA) {
|
|
364
381
|
throw _err("cms/bad-input", "signed attributes are required when eContentType is not id-data (RFC 5652 sec. 5.3)");
|
|
365
382
|
}
|
|
383
|
+
// The signer's half of the signed-attribute stripping problem
|
|
384
|
+
// (draft-vangeest-lamps-cms-euf-cma-signeddata, Attack Type 2). Signing attribute-shaped content
|
|
385
|
+
// WITHOUT attributes produces a signature that can afterwards be promoted into an
|
|
386
|
+
// attributes-present message, because the signature does not commit to which mode was used: the
|
|
387
|
+
// attacker attaches the signed bytes AS the SignedAttributes and swaps in whatever content their
|
|
388
|
+
// message-digest attribute names. Refusing to mint the ambiguous signature is the only point at
|
|
389
|
+
// which this direction can be stopped -- by the time it is a message, the damage is done.
|
|
390
|
+
if (opts.signedAttributes === false && cms.looksLikeSignedAttributes(contentBuf)) {
|
|
391
|
+
throw _err("cms/ambiguous-content", "this content is itself an encoded SignedAttributes block, so signing " +
|
|
392
|
+
"it WITHOUT signed attributes would produce a signature that could be re-presented as one over " +
|
|
393
|
+
"attributes (RFC 5652 sec. 5.4); sign it with signed attributes instead");
|
|
394
|
+
}
|
|
366
395
|
// A supplied signing-time MUST be a valid Date (or false to omit the attribute) -- never a
|
|
367
396
|
// silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
|
|
368
397
|
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
@@ -405,8 +434,7 @@ function _dedupe(ders) {
|
|
|
405
434
|
}
|
|
406
435
|
|
|
407
436
|
function _toBuf(v, what) {
|
|
408
|
-
if (Buffer.isBuffer(v)) return v;
|
|
409
|
-
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
437
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmsError, "cms/bad-input", what);
|
|
410
438
|
throw _err("cms/bad-input", what + " must be a Buffer");
|
|
411
439
|
}
|
|
412
440
|
|
|
@@ -533,7 +561,17 @@ function _targetPreimage(siNode, opts) {
|
|
|
533
561
|
}
|
|
534
562
|
|
|
535
563
|
// pki.cms.countersign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
564
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async).
|
|
536
565
|
function countersign(cmsInput, signers, opts) {
|
|
566
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
567
|
+
// on the same call in x509-sign. `signerIndex` and `countersignatureOf` SELECT which signature is
|
|
568
|
+
// countersigned, so a late read could attach the countersignature to a different one.
|
|
569
|
+
return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
|
|
570
|
+
[cmsInput, "the CMS message"], [signers, "the signer list"], [opts, "pki.cms.countersign options"],
|
|
571
|
+
], _countersign);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function _countersign(cmsInput, signers, opts) {
|
|
537
575
|
opts = opts || {};
|
|
538
576
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.countersign options must be an object");
|
|
539
577
|
var list = Array.isArray(signers) ? signers : [signers];
|