@blamejs/pki 0.5.6 → 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 CHANGED
@@ -4,7 +4,29 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## v0.5.6 — 2026-08-16
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
8
30
 
9
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.
10
32
 
package/MIGRATING.md CHANGED
@@ -14,6 +14,28 @@ 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
+
17
39
  ### v0.5.6 — `try { pki.<verb>(...) } catch`
18
40
 
19
41
  A verb documented `-> Promise` rejects on a bad input instead of throwing before the promise exists.
@@ -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
- return Promise.resolve().then(function () { return _sign(spec, issuer, opts); });
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
- // Assembled SYNCHRONOUSLY. _build reads the spec, every request buffer and the
485
- // signer as it goes, so deferring that work would read them a turn after the
486
- // call -- and a caller reusing a pooled CSR buffer, or reaching back into the
487
- // spec on the next line, would have the message signed over something other
488
- // than what was handed in. Only the signing itself is async, and by then the
489
- // bytes are fixed.
490
- try {
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 Promise.resolve().then(function () { return _verify(frozenResponse, frozenSent); });
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 Buffer.from(v);
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
- return Promise.resolve().then(function () { return _build(message, opts); });
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 Promise.resolve().then(function () { return _transfer(url, message, opts); });
741
+ return guard.async.deferred(function () { return _transfer(url, message, opts); });
737
742
  }
738
743
 
739
744
  function _transfer(url, message, opts) {
@@ -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
- if (Buffer.isBuffer(pk)) return pk;
896
- if (pk instanceof Uint8Array) return Buffer.from(pk);
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 Promise.resolve().then(function () { return _verify(message, opts); });
621
+ return guard.async.deferred(function () { return _verify(message, opts); });
622
622
  }
623
623
 
624
624
  async function _verify(message, opts) {
@@ -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
  }
@@ -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
  }
@@ -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 && !Buffer.isBuffer(c)) c = Buffer.from(c); // a Uint8Array -> Buffer (below)
329
- if (Buffer.isBuffer(c)) return c[0] === 0x30 ? c : _pemToDer(c.toString("latin1")); // DER as-is, else PEM
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
  }
@@ -340,12 +342,21 @@ function _pemToDer(text) {
340
342
  // Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
341
343
  // synchronous because they read the caller's mutable content and signer list.
342
344
  function sign(content, signers, opts) {
343
- return guard.async.deferred(function () { return _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);
344
351
  }
345
352
 
346
353
  function _sign(content, signers, opts) {
347
354
  opts = opts || {};
348
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.
349
360
  var contentBuf = _toBuf(content, "content");
350
361
  var list = Array.isArray(signers) ? signers : [signers];
351
362
  if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
@@ -369,6 +380,18 @@ function _sign(content, signers, opts) {
369
380
  if (opts.signedAttributes === false && eContentType !== OID_DATA) {
370
381
  throw _err("cms/bad-input", "signed attributes are required when eContentType is not id-data (RFC 5652 sec. 5.3)");
371
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
+ }
372
395
  // A supplied signing-time MUST be a valid Date (or false to omit the attribute) -- never a
373
396
  // silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
374
397
  if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
@@ -411,8 +434,7 @@ function _dedupe(ders) {
411
434
  }
412
435
 
413
436
  function _toBuf(v, what) {
414
- if (Buffer.isBuffer(v)) return v;
415
- 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);
416
438
  throw _err("cms/bad-input", what + " must be a Buffer");
417
439
  }
418
440
 
@@ -541,7 +563,12 @@ function _targetPreimage(siNode, opts) {
541
563
  // pki.cms.countersign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
542
564
  // Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async).
543
565
  function countersign(cmsInput, signers, opts) {
544
- return guard.async.deferred(function () { return _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);
545
572
  }
546
573
 
547
574
  function _countersign(cmsInput, signers, opts) {
package/lib/cms-verify.js CHANGED
@@ -119,8 +119,7 @@ EC_CURVE[oid.byName("secp384r1")] = { curve: "P-384", coordLen: 48 };
119
119
  EC_CURVE[oid.byName("secp521r1")] = { curve: "P-521", coordLen: 66 };
120
120
 
121
121
  function _toBuf(v, what) {
122
- if (Buffer.isBuffer(v)) return v;
123
- if (v instanceof Uint8Array) return Buffer.from(v);
122
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmsError, "cms/bad-input", what);
124
123
  throw _err("cms/bad-input", what + " must be a Buffer");
125
124
  }
126
125
 
@@ -420,7 +419,24 @@ function _computeCountersigBytes(si, csTarget) {
420
419
  // content-type / message-digest attribute disagrees.
421
420
  function _computeSignedBytes(si, content, eContentType) {
422
421
  return Promise.resolve().then(function () {
423
- if (!si.signedAttrsBytes) return content; // no signed attributes: sign over the content directly
422
+ if (!si.signedAttrsBytes) {
423
+ // No signed attributes: the signature is over the content itself (RFC 5652 sec. 5.4). That is
424
+ // also exactly what a stripped-attributes forgery looks like, because a CMS signature does not
425
+ // commit to whether attributes were present -- so a signature made over a SignedAttributes
426
+ // block re-presented as one made over content verifies, and with no attributes there is no
427
+ // message-digest or content-type attribute left to disagree. Refuse the shape: content that
428
+ // parses as a SignedAttributes block cannot be told apart from that forgery, and it is not
429
+ // the verifier's place to guess which one it is holding.
430
+ if (cms.looksLikeSignedAttributes(content)) {
431
+ return { mismatch: {
432
+ code: "cms/ambiguous-content",
433
+ message: "the content of a SignerInfo with no signed attributes is itself an encoded " +
434
+ "SignedAttributes block, which is indistinguishable from a signature over attributes " +
435
+ "re-presented as one over content (RFC 5652 sec. 5.4); sign such content WITH signed attributes",
436
+ } };
437
+ }
438
+ return content;
439
+ }
424
440
  // With signed attributes: decode them from the EXACT bytes the signature covers -- the
425
441
  // SignedAttributes SET OF, the on-wire [0] IMPLICIT tag replaced by a universal SET OF
426
442
  // (RFC 5652 sec. 5.4) -- so the content-type / message-digest checks bind the same bytes
@@ -574,7 +590,7 @@ function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
574
590
 
575
591
  /**
576
592
  * @primitive pki.cms.verify
577
- * @signature pki.cms.verify(input, opts?) -> Promise<{ valid, trusted, signers }>
593
+ * @signature pki.cms.verify(input, opts?) -> Promise<{ valid, trusted, eContentType, signers }>
578
594
  * @since 0.2.14
579
595
  * @status stable
580
596
  * @spec RFC 5652
@@ -584,10 +600,17 @@ function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
584
600
  * @related pki.schema.cms.parse, pki.path.validate
585
601
  *
586
602
  * Verify a CMS SignedData signature (RFC 5652 sec. 5). `input` is a PEM string, a DER
587
- * `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, trusted, signers }` where each
588
- * `signers[i]` is `{ ok, sid, cert, trusted }` (`cert` the matched signer certificate DER) or carries
589
- * a `code` on a structural failure; `valid` is true when there is at least one signer and
590
- * every signer verified.
603
+ * `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, trusted, eContentType, signers }`
604
+ * where each `signers[i]` is `{ ok, sid, cert, trusted, signedAttributesPresent }` (`cert` the
605
+ * matched signer certificate DER) or carries a `code` on a structural failure; `valid` is true when
606
+ * there is at least one signer and every signer verified.
607
+ *
608
+ * `eContentType` and `signedAttributesPresent` are there for a caller whose profile is stricter
609
+ * than RFC 5652's. Signing WITH attributes and signing the content directly are different claims --
610
+ * attributes bind a content type and a signing time alongside the digest, content-only binds
611
+ * nothing but the bytes -- and one message may carry a signer of each. A profile that requires
612
+ * attributes (RFC 8551 S/MIME does) or a particular content type can enforce it from the verdict
613
+ * rather than parsing the message a second time.
591
614
  *
592
615
  * `valid` and `trusted` are DIFFERENT claims and neither implies the other. A SignedData carries
593
616
  * its own certificates, so `valid` establishes that the message is internally consistent -- the
@@ -721,11 +744,25 @@ function _verify(input, opts) {
721
744
  return _verifyCountersignatures(si, parsedCerts).then(function (countersignatures) {
722
745
  verdict.countersignatures = countersignatures;
723
746
  verdict.unsignedAttrs = _surfaceUnsignedAttrs(si);
747
+ // Whether THIS signer signed attributes or signed the content directly. The two are
748
+ // different claims -- attributes bind a content type and a signing time alongside the
749
+ // digest, and content-only binds nothing but the bytes -- and RFC 5652 lets a message carry
750
+ // one signer of each. A caller whose profile requires attributes (RFC 8551 S/MIME does) can
751
+ // only enforce it if the verdict says which they got.
752
+ verdict.signedAttributesPresent = !!si.signedAttrsBytes;
724
753
  return verdict;
725
754
  });
726
755
  });
727
756
  })).then(function (signers) {
728
- var res = { valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }), signers: signers };
757
+ // The content type travels with the verdict. An operator applying a policy of their own -- "I
758
+ // only accept id-data", or a profile that names its own type -- otherwise had to parse the
759
+ // message a second time to learn it, and a check that needs a second parse is a check most
760
+ // callers will not write.
761
+ var res = {
762
+ valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }),
763
+ eContentType: parsed.encapContentInfo.eContentType,
764
+ signers: signers,
765
+ };
729
766
  return _applyTrust(res, parsedCerts, trustCfg).then(function () { return res; });
730
767
  });
731
768
  }
package/lib/crl-sign.js CHANGED
@@ -144,7 +144,7 @@ function _resolveReason(reason, isDelta) {
144
144
  // The AKI keyIdentifier from the issuer: an explicit Buffer, or true -> the issuer cert's subjectKeyIdentifier,
145
145
  // else the SHA-1 of the issuer SPKI (RFC 5280 sec. 5.2.1 key-identifier method).
146
146
  function _akiKeyId(val, ctx) {
147
- if (Buffer.isBuffer(val)) return val;
147
+ if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, CrlError, "crl/bad-input", "the authorityKeyIdentifier keyIdentifier");
148
148
  if (val === true) {
149
149
  if (ctx.issuerCert) {
150
150
  var ski = (ctx.issuerCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
@@ -507,7 +507,13 @@ function _sign(spec, issuer, opts) {
507
507
  * }, { cert: signerCertDer, key: signerKeyPkcs8 });
508
508
  * pki.schema.crl.parse(der).revokedCertificates[0].serialNumberHex; // "1234"
509
509
  */
510
- function sign(spec, issuer, opts) { return Promise.resolve().then(function () { return _sign(spec, issuer, opts); }); }
510
+ function sign(spec, issuer, opts) {
511
+ // Every caller-owned argument copied at entry and released when the call settles -- see the note
512
+ // on the same call in x509-sign.
513
+ return guard.bytes.fixedCall(CrlError, "crl/bad-input", [
514
+ [spec, "the CRL spec"], [issuer, "the issuer"], [opts, "pki.crl.sign options"],
515
+ ], _sign);
516
+ }
511
517
 
512
518
  // A CRL these verbs answer from is re-derived from the bytes its parser read. Completeness -- every
513
519
  // field present with the right type -- is not enough for a verdict: the signature covers a byte
@@ -598,7 +604,7 @@ function _issuerMaySign(parsed, cert) {
598
604
  * { cert: signerCertDer, key: signerKeyPkcs8 });
599
605
  * var ok = await pki.crl.verify(crlDer, { publicKey: signerSpki }); // true / false
600
606
  */
601
- function verify(crl, issuer) { return Promise.resolve().then(function () { return _verify(crl, issuer); }); }
607
+ function verify(crl, issuer) { return guard.async.deferred(function () { return _verify(crl, issuer); }); }
602
608
  function _verify(crl, issuer) {
603
609
  var parsed = _coerceCrl(crl);
604
610
  var resolved = _resolveIssuer(issuer);
package/lib/crmf-sign.js CHANGED
@@ -238,7 +238,11 @@ function _buildProofOfPossession(pop, certReqDer, template, signingKey, opts) {
238
238
  * pki.schema.crmf.parse(msg).messages[0].certReq.certTemplate.subject.dn; // "CN=device-42"
239
239
  */
240
240
  function build(spec, key, opts) {
241
- return Promise.resolve().then(function () { return _build(spec, key, opts); });
241
+ // Every caller-owned argument copied at entry and released when the call settles -- see the note
242
+ // on the same call in x509-sign.
243
+ return guard.bytes.fixedCall(CrmfError, "crmf/bad-input", [
244
+ [spec, "the certificate-request-message spec"], [key, "the signing key"], [opts, "pki.crmf.build options"],
245
+ ], _build);
242
246
  }
243
247
 
244
248
  function _buildCertReqMsg(spec, key, opts) {
package/lib/csr-sign.js CHANGED
@@ -101,7 +101,11 @@ function _challengePassword(pw) {
101
101
  * pki.schema.csr.parse(req).subject.dn; // "CN=req.example.com"
102
102
  */
103
103
  function sign(spec, key, opts) {
104
- return Promise.resolve().then(function () { return _sign(spec, key, opts); });
104
+ // Every caller-owned argument copied at entry and released when the call settles -- see the note
105
+ // on the same call in x509-sign.
106
+ return guard.bytes.fixedCall(CsrError, "csr/bad-input", [
107
+ [spec, "the certification-request spec"], [key, "the signing key"], [opts, "pki.csr.sign options"],
108
+ ], _sign);
105
109
  }
106
110
 
107
111
  function _sign(spec, key, opts) {
package/lib/est.js CHANGED
@@ -826,7 +826,7 @@ var MAX_TIMEOUT = constants.TIME.seconds(600);
826
826
  // The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
827
827
  // decoded. Any other input is a config-time est/bad-input.
828
828
  function _csrDer(input) {
829
- if (Buffer.isBuffer(input)) return input;
829
+ if (Buffer.isBuffer(input)) return guard.bytes.snapshot(input, EstError, "est/bad-input", "a CSR");
830
830
  if (typeof input === "string") return csr.pemDecode(input);
831
831
  throw E("est/bad-input", "a CSR must be a DER Buffer or a PEM CERTIFICATE REQUEST string");
832
832
  }
@@ -1428,7 +1428,7 @@ function _cmcSent(opts, der) {
1428
1428
  // window this exists to close, open for exactly the inputs that came in by the wider door.
1429
1429
  // A DataView is copied over its OWN window, not the whole backing buffer it happens to sit in.
1430
1430
  function _copyBytes(v) {
1431
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v);
1431
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, EstError, "est/bad-input", "a byte field of the request");
1432
1432
  if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
1433
1433
  if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
1434
1434
  return v;
@@ -1591,8 +1591,9 @@ function _shallowCopy(o) {
1591
1591
  function _cmcRequestDer(request) {
1592
1592
  // COPIED, not aliased: these bytes are parsed now (for the requested keys) and
1593
1593
  // transmitted later, so sharing the caller's buffer would let the two disagree.
1594
- if (Buffer.isBuffer(request)) return Buffer.from(request);
1595
- if (request instanceof Uint8Array) return Buffer.from(request);
1594
+ if (Buffer.isBuffer(request) || request instanceof Uint8Array) {
1595
+ return guard.bytes.snapshot(request, EstError, "est/bad-input", "the Full PKI Request");
1596
+ }
1596
1597
  if (typeof request === "string") return cms.pemDecode(request);
1597
1598
  throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
1598
1599
  }