@blamejs/pki 0.8.52 → 0.8.54
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 +37 -0
- package/README.md +2 -2
- package/lib/asn1-der.js +112 -4
- package/lib/cmc-build.js +3 -3
- package/lib/cmp-session.js +2 -2
- package/lib/cmp-verify.js +2 -2
- package/lib/cms-decrypt.js +40 -3
- package/lib/cms-encrypt.js +64 -2
- package/lib/cms-sign.js +67 -2
- package/lib/cms-verify.js +148 -24
- package/lib/crmf-sign.js +1 -1
- package/lib/ct.js +3 -2
- package/lib/guard-der.js +118 -29
- package/lib/guard-encoding.js +2 -1
- package/lib/guard-identifier.js +29 -1
- package/lib/guard-name.js +6 -3
- package/lib/guard-text.js +6 -2
- package/lib/lint.js +69 -13
- package/lib/oid.js +11 -1
- package/lib/path-validate.js +82 -12
- package/lib/revocation-fetch.js +1 -3
- package/lib/scep.js +8 -3
- package/lib/schema-all.js +2 -1
- package/lib/schema-cms.js +377 -6
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-sign.js
CHANGED
|
@@ -40,13 +40,14 @@ function O(name) { return oid.byName(name); }
|
|
|
40
40
|
|
|
41
41
|
var KNOWN_SIGN_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
42
42
|
signedAttributes: 1, signingTime: 1, additionalSignedAttributes: 1, unsignedAttributes: 1,
|
|
43
|
+
algorithmProtection: 1,
|
|
43
44
|
sid: 1, eContentType: 1, detached: 1, certificates: 1, pem: 1,
|
|
44
45
|
});
|
|
45
46
|
var KNOWN_SIGNER_CERT_KEYS = intrinsic.assign(intrinsic.create(null), { cert: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 });
|
|
46
47
|
var KNOWN_SIGNER_KEY_ONLY_KEYS = intrinsic.assign(intrinsic.create(null), { spki: 1, keyIdentifier: 1, key: 1, pss: 1, digestAlgorithm: 1, combinedRsaSig: 1 });
|
|
47
48
|
var KNOWN_COUNTERSIGN_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
48
49
|
signerIndex: 1, countersignatureOf: 1, signingTime: 1, certificates: 1, pem: 1,
|
|
49
|
-
signedAttributes: 1, additionalSignedAttributes: 1, sid: 1,
|
|
50
|
+
signedAttributes: 1, additionalSignedAttributes: 1, sid: 1, algorithmProtection: 1,
|
|
50
51
|
});
|
|
51
52
|
var NS = pkix.makeNS("cms", CmsError, oid);
|
|
52
53
|
var _b = pkiBuild.makeBuilder({
|
|
@@ -118,6 +119,12 @@ var UNSIGNED_FORBIDDEN = intrinsic.create(null);
|
|
|
118
119
|
UNSIGNED_FORBIDDEN[O("contentType")] = "content-type";
|
|
119
120
|
UNSIGNED_FORBIDDEN[O("messageDigest")] = "message-digest";
|
|
120
121
|
UNSIGNED_FORBIDDEN[O("signingTime")] = "signing-time";
|
|
122
|
+
/** @internal RFC 6211 sec. 2: the algorithm protection attribute "MUST NOT be an unsigned
|
|
123
|
+
* attribute", and sec. 5 adds that one placed there "does not provide any additional security".
|
|
124
|
+
* Both identifiers, since the two spell one attribute. The parser refuses the placement, so without
|
|
125
|
+
* the door here this verb would emit a message its own verifier rejects. */
|
|
126
|
+
UNSIGNED_FORBIDDEN[O("cmsAlgorithmProtection")] = "algorithm protection";
|
|
127
|
+
UNSIGNED_FORBIDDEN[O("cmsAlgorithmProtect")] = "algorithm protection";
|
|
121
128
|
|
|
122
129
|
function _buildUnsignedAttrs(list) {
|
|
123
130
|
if (list == null) return null;
|
|
@@ -141,6 +148,49 @@ function _buildUnsignedAttrs(list) {
|
|
|
141
148
|
return wire;
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
/** @internal The RFC 6211 attribute built from the very TLVs the structure's own fields are built
|
|
152
|
+
* from. Sec. 2 says the attribute "contains a copy of the SignerInfo.digestAlgorithm field ...
|
|
153
|
+
* including any parameters associated with it", so the copy is made from the value in hand rather
|
|
154
|
+
* than re-derived from a registry row: a re-derivation can differ from the field it claims to copy,
|
|
155
|
+
* and the parameters are the substitution target the attribute exists to pin.
|
|
156
|
+
*
|
|
157
|
+
* `armTag` is 1 for a `SignerInfo` signature algorithm and 2 for an `AuthenticatedData` MAC
|
|
158
|
+
* algorithm, which sec. 2 makes exclusive to their own placements. `attrOid` is the identifier the
|
|
159
|
+
* caller's option resolved to, taken at the door so the pair carries the one that was asked for. */
|
|
160
|
+
function _algorithmProtectionPair(attrOid, digestAlgId, armAlgId, armTag) {
|
|
161
|
+
return { type: attrOid, values: [b.sequence([digestAlgId, b.implicit(armTag, armAlgId)])] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @internal An asked-for protection that cannot be placed is a refusal rather than a message that
|
|
165
|
+
* silently lacks it. The attribute rides in the signed attributes, so with none there is nowhere to
|
|
166
|
+
* put it, which is why RFC 8933 sec. 4.1 conditions its SHOULD on their presence. A caller who
|
|
167
|
+
* names both options is asking for two things that cannot both hold.
|
|
168
|
+
*
|
|
169
|
+
* The option is resolved FIRST, on every route, and the placement question asked second. Resolving
|
|
170
|
+
* it inside the placement test would only read it on the route that cannot place it: a streamed
|
|
171
|
+
* content is consumed and hashed before the attributes are built, so a misspelled value would be
|
|
172
|
+
* refused after the stream had been read, which for a stream that cannot be replayed is a refusal
|
|
173
|
+
* the caller cannot act on, and for one that never ends is no refusal at all.
|
|
174
|
+
*
|
|
175
|
+
* The emitter below reads the same option again, which is sound because the options door refuses
|
|
176
|
+
* one supplied through an accessor, own or inherited, before any value is read: two reads of a
|
|
177
|
+
* plain field are one answer. */
|
|
178
|
+
function _assertProtectionPlaceable(opts) {
|
|
179
|
+
var wanted = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
|
|
180
|
+
if (opts.signedAttributes === false && wanted) {
|
|
181
|
+
throw _err("cms/bad-input", "algorithmProtection needs signed attributes to ride in, and " +
|
|
182
|
+
"signedAttributes: false signs the content directly (RFC 6211 sec. 2 places the attribute in " +
|
|
183
|
+
"SignerInfo.signedAttrs); drop one of the two");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @internal The producer door, applied to this verb's attribute records. The policy itself lives in
|
|
188
|
+
* `schema-cms`, so the signing verbs and the authenticate verb enforce one rule rather than three
|
|
189
|
+
* copies of it. */
|
|
190
|
+
function _assertAlgorithmProtectionPairs(pairs, digestAlgId, armAlgId, armTag) {
|
|
191
|
+
cms.assertAlgorithmProtectionEmission(pairs, b.sequence([digestAlgId, b.implicit(armTag, armAlgId)]), _err);
|
|
192
|
+
}
|
|
193
|
+
|
|
144
194
|
function _resolveSignerContext(signer, opts) {
|
|
145
195
|
var so = signer || {};
|
|
146
196
|
var keyOnly = so.cert == null && so.spki != null;
|
|
@@ -166,7 +216,13 @@ async function _finishSignerInfo(rc, md, content, eContentType, opts) {
|
|
|
166
216
|
{ type: O("messageDigest"), values: [b.octetString(md)] },
|
|
167
217
|
];
|
|
168
218
|
if (opts.signingTime !== false) intrinsic.push(pairs, { type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
219
|
+
var apOid = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
|
|
220
|
+
if (apOid) {
|
|
221
|
+
intrinsic.push(pairs, _algorithmProtectionPair(apOid,
|
|
222
|
+
rc.scheme.digestAlgId, rc.scheme.sigAlgId, 1));
|
|
223
|
+
}
|
|
169
224
|
pairs = intrinsic.concat(pairs, _resolveAttrPairs(opts.additionalSignedAttributes, "a signed attribute value"));
|
|
225
|
+
_assertAlgorithmProtectionPairs(pairs, rc.scheme.digestAlgId, rc.scheme.sigAlgId, 1);
|
|
170
226
|
toSign = _buildSignedAttrs(pairs);
|
|
171
227
|
}
|
|
172
228
|
var signedBytes = toSign.setOf ? toSign.setOf : toSign;
|
|
@@ -290,6 +346,7 @@ function _signDispatch(content, signers, opts) {
|
|
|
290
346
|
async function _signStream(stream, signers, opts) {
|
|
291
347
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
292
348
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
349
|
+
_assertProtectionPlaceable(opts);
|
|
293
350
|
if (opts.detached !== true) {
|
|
294
351
|
throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
|
|
295
352
|
"SignedData embeds the content as a definite-length OCTET STRING, which cannot be produced without buffering it");
|
|
@@ -329,6 +386,7 @@ async function _signStream(stream, signers, opts) {
|
|
|
329
386
|
function _sign(content, signers, opts) {
|
|
330
387
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
331
388
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
389
|
+
_assertProtectionPlaceable(opts);
|
|
332
390
|
var contentBuf = _toBuf(content, "content");
|
|
333
391
|
var list = Array.isArray(signers) ? _b.reqDenseArray(signers, "the signer list") : [signers];
|
|
334
392
|
if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
|
|
@@ -465,9 +523,15 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
|
|
|
465
523
|
return _digest(scheme.digest, targetSigOctets).then(function (md) {
|
|
466
524
|
var pairs = [{ type: O("messageDigest"), values: [b.octetString(md)] }];
|
|
467
525
|
if (opts.signingTime !== false) pairs.push({ type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
526
|
+
var csApOid = cms.algorithmProtectionOid(opts.algorithmProtection, _err);
|
|
527
|
+
if (csApOid) {
|
|
528
|
+
intrinsic.push(pairs, _algorithmProtectionPair(csApOid, scheme.digestAlgId, scheme.sigAlgId, 1));
|
|
529
|
+
}
|
|
468
530
|
var extra = _resolveAttrPairs(opts.additionalSignedAttributes, "a countersignature signed attribute value");
|
|
469
531
|
extra.forEach(function (p) { if (p.type === O("contentType")) throw _err("cms/bad-input", "a countersignature must not carry a content-type attribute (RFC 5652 sec. 11.4)"); });
|
|
470
|
-
|
|
532
|
+
var all = intrinsic.concat(pairs, extra);
|
|
533
|
+
_assertAlgorithmProtectionPairs(all, scheme.digestAlgId, scheme.sigAlgId, 1);
|
|
534
|
+
return _buildSignedAttrs(all);
|
|
471
535
|
});
|
|
472
536
|
}).then(function (attrs) {
|
|
473
537
|
var preimage = attrs ? attrs.setOf : targetSigOctets;
|
|
@@ -559,6 +623,7 @@ function countersign(cmsInput, signers, opts) {
|
|
|
559
623
|
function _countersign(cmsInput, signers, opts) {
|
|
560
624
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
|
|
561
625
|
guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
626
|
+
_assertProtectionPlaceable(opts);
|
|
562
627
|
var list = Array.isArray(signers) ? signers : [signers];
|
|
563
628
|
if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
|
|
564
629
|
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertEncodable(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
package/lib/cms-verify.js
CHANGED
|
@@ -232,23 +232,22 @@ function _hashAlgOid(seq) {
|
|
|
232
232
|
return o;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** @internal The parameter fields come from `cms.taggedParameterFields`, which holds the DER rule
|
|
236
|
+
* for a SEQUENCE of tagged OPTIONAL and DEFAULT fields: one element per tag, ascending, no repeat,
|
|
237
|
+
* exactly one value each. The comparison that reads these same fields for their defaults walks the
|
|
238
|
+
* same reader, so the verifier and the comparison cannot disagree about what a field is. */
|
|
235
239
|
function _resolvePss(paramsBytes) {
|
|
236
240
|
if (!paramsBytes) return null;
|
|
237
|
-
var
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
var hash = null, saltLen = null, mgfNode = null, trailer = 1n, last = -1;
|
|
243
|
-
for (var i = 0; i < n.children.length; i++) {
|
|
244
|
-
var f = n.children[i];
|
|
245
|
-
if (f.tagClass !== "context" || f.tagNumber > 3 || f.tagNumber <= last || !f.children || f.children.length !== 1) return null;
|
|
246
|
-
last = f.tagNumber;
|
|
241
|
+
var fields = cms.taggedParameterFields(paramsBytes, 3);
|
|
242
|
+
if (fields === null) return null;
|
|
243
|
+
var hash = null, saltLen = null, mgfNode = null, trailer = 1n;
|
|
244
|
+
for (var i = 0; i < fields.length; i++) {
|
|
245
|
+
var f = fields[i];
|
|
247
246
|
try {
|
|
248
|
-
if (f.
|
|
249
|
-
else if (f.
|
|
250
|
-
else if (f.
|
|
251
|
-
else { trailer = asn1.read.integer(f.
|
|
247
|
+
if (f.tag === 0) { hash = PSS_HASH[_hashAlgOid(f.node)]; if (!hash) return null; }
|
|
248
|
+
else if (f.tag === 1) { mgfNode = f.node; }
|
|
249
|
+
else if (f.tag === 2) { saltLen = asn1.read.integer(f.node); }
|
|
250
|
+
else { trailer = asn1.read.integer(f.node); }
|
|
252
251
|
} catch (_e3) { return null; }
|
|
253
252
|
}
|
|
254
253
|
if (hash === null || mgfNode === null) return null;
|
|
@@ -464,6 +463,54 @@ function _surfaceSignedAttrs(si) {
|
|
|
464
463
|
});
|
|
465
464
|
}
|
|
466
465
|
|
|
466
|
+
/** @internal What the RFC 6211 algorithm protection attribute says about the structure carrying it,
|
|
467
|
+
* read ONCE per signer and used for both the verdict and the comparison, so the row an operator
|
|
468
|
+
* reads and the decision the verdict rests on cannot disagree.
|
|
469
|
+
*
|
|
470
|
+
* The attribute is a copy of the algorithm identifiers the structure already names, placed where
|
|
471
|
+
* the signature covers them. Sec. 3.1 makes both comparisons mandatory for a validator that
|
|
472
|
+
* supports the attribute, and support is claimed the moment this runs: "If the fields are not the
|
|
473
|
+
* same (modulo encoding), then signature validation MUST fail."
|
|
474
|
+
*
|
|
475
|
+
* Both arms compare modulo encoding, in the two ways sec. 3 names. A field with a default compares
|
|
476
|
+
* the same written out as omitted, which the clause makes a MUST and says a binary compare cannot
|
|
477
|
+
* satisfy; RSASSA-PSS is where that arises, since RFC 4055 clause 3.1 defaults all four of its
|
|
478
|
+
* parameter fields. Absent and DER NULL parameters read as one algorithm on the digest arm, because
|
|
479
|
+
* RFC 3370 clause 2.1 and RFC 5754 clause 2 require both encodings to be ACCEPTED; sec. 3 leaves
|
|
480
|
+
* that case to the implementer. Anything else in a parameter field is read as written: that is
|
|
481
|
+
* where RSASSA-PSS puts its hash, mask generator, salt length and trailer field, which the
|
|
482
|
+
* signature itself does not cover, so collapsing two values there would admit the substitution the
|
|
483
|
+
* attribute exists to refuse.
|
|
484
|
+
*
|
|
485
|
+
* A `[2]` MAC arm inside `signedAttrs` is a mismatch rather than an absent signature arm: sec. 2
|
|
486
|
+
* populates that arm "only if the attribute is placed in an AuthenticatedData.authAttrs sequence",
|
|
487
|
+
* so the attribute is making a claim about a structure this is not. */
|
|
488
|
+
function _algorithmProtectionReport(si, armName, armIdentifier) {
|
|
489
|
+
return cms.algorithmProtectionReport(si.signedAttrs || si.authAttrs || [],
|
|
490
|
+
si.digestAlgorithm, armName, armIdentifier);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** @internal The report applied to a verdict. A mismatch fails the signer, and the option that
|
|
494
|
+
* requires the attribute fails an absent one; a verdict that already failed keeps its own code,
|
|
495
|
+
* since the signature's verdict is the stronger statement and masking it would tell an operator the
|
|
496
|
+
* wrong thing about why the message was refused. */
|
|
497
|
+
function _applyAlgorithmProtection(verdict, report, required) {
|
|
498
|
+
var fields = { algorithmProtection: guard.verdict.of(report) };
|
|
499
|
+
if (verdict.ok !== true) return guard.verdict.of(verdict, fields);
|
|
500
|
+
if (report.mismatch !== null) {
|
|
501
|
+
return guard.verdict.of(verdict, intrinsic.assign(fields,
|
|
502
|
+
{ ok: false, code: report.mismatch.code, message: report.mismatch.message }));
|
|
503
|
+
}
|
|
504
|
+
if (required && !report.present) {
|
|
505
|
+
return guard.verdict.of(verdict, intrinsic.assign(fields, { ok: false,
|
|
506
|
+
code: "cms/missing-algorithm-protection",
|
|
507
|
+
message: "this signature carries no RFC 6211 algorithm protection attribute, which " +
|
|
508
|
+
"opts.requireAlgorithmProtection asks for; RFC 8933 sec. 4.1 makes including it a SHOULD, " +
|
|
509
|
+
"so a message without one is not malformed" }));
|
|
510
|
+
}
|
|
511
|
+
return guard.verdict.of(verdict, fields);
|
|
512
|
+
}
|
|
513
|
+
|
|
467
514
|
/** @internal The CAdES baseline requirements this verb decides from one SignerInfo's own signed
|
|
468
515
|
* attributes, reported beside the signature verdict rather than folded into it: a signature can be
|
|
469
516
|
* cryptographically sound and still not be a CAdES signature, and the two answers are separate.
|
|
@@ -784,7 +831,7 @@ function _signerNotAfter(verdict) {
|
|
|
784
831
|
catch (_e) { return null; }
|
|
785
832
|
}
|
|
786
833
|
|
|
787
|
-
async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
|
|
834
|
+
async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests, requireProtection) {
|
|
788
835
|
var targetSig = _toBuf(si.signature, "the countersigned signature");
|
|
789
836
|
var values = [];
|
|
790
837
|
intrinsic.forEach(si.unsignedAttrs || [], function (a) {
|
|
@@ -792,17 +839,24 @@ async function _verifyCountersignatures(si, parsedCerts, allowWeakDigests) {
|
|
|
792
839
|
intrinsic.forEach(a.values, function (v) { guard.list.append(values, v && v.bytes ? v.bytes : v); });
|
|
793
840
|
});
|
|
794
841
|
var out = [];
|
|
795
|
-
for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests));
|
|
842
|
+
for (var _cp = 0; _cp < values.length; _cp++) guard.list.append(out, await _verifyOneCountersig(values[_cp], targetSig, parsedCerts, allowWeakDigests, requireProtection));
|
|
796
843
|
return out;
|
|
797
844
|
}
|
|
798
845
|
|
|
799
|
-
async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests) {
|
|
846
|
+
async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDigests, requireProtection) {
|
|
800
847
|
var csSi;
|
|
801
848
|
try { csSi = cms.walkCountersignature(asn1.decode(intrinsic.isBuffer(vDer) ? vDer : intrinsic.bufferFrom(vDer))); }
|
|
802
849
|
catch (e) { return _row({ ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }); }
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
850
|
+
/** @internal A countersignature is its own SignerInfo with its own algorithm identifiers, so the
|
|
851
|
+
* attribute inside it protects those and is compared against them rather than against the
|
|
852
|
+
* signature it countersigns. */
|
|
853
|
+
var apReport = _algorithmProtectionReport(csSi, "signatureAlgorithm", csSi.signatureAlgorithm);
|
|
854
|
+
var verdict = _applyAlgorithmProtection(
|
|
855
|
+
await _verifyOne(csSi, targetSig, null, parsedCerts, targetSig, undefined, allowWeakDigests),
|
|
856
|
+
apReport, requireProtection === true);
|
|
857
|
+
var nested = await _verifyCountersignatures(csSi, parsedCerts, allowWeakDigests, requireProtection);
|
|
858
|
+
var head = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name,
|
|
859
|
+
algorithmProtection: verdict.algorithmProtection };
|
|
806
860
|
return guard.verdict.of(
|
|
807
861
|
verdict.code ? guard.verdict.of(head, { code: verdict.code }) : head,
|
|
808
862
|
{ unsignedAttrs: _surfaceUnsignedAttrs(csSi), countersignatures: nested });
|
|
@@ -822,7 +876,7 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
822
876
|
* Verify a CMS SignedData signature (RFC 5652 sec. 5). `input` is a PEM string, a DER
|
|
823
877
|
* `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, trusted, eContentType, eContent, signers }`
|
|
824
878
|
* (`eContent` the encapsulated content when present, else null) where each `signers[i]` is
|
|
825
|
-
* `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures, signatureTimeStamps, cadesBaseline }`
|
|
879
|
+
* `{ ok, sid, cert, trusted, signedAttributesPresent, signedAttributes, unsignedAttrs, digestAlgorithm, countersignatures, signatureTimeStamps, cadesBaseline, algorithmProtection }`
|
|
826
880
|
* (`cert` the matched signer certificate DER) or carries a `code` on a structural failure; `valid` is true when
|
|
827
881
|
* there is at least one signer and every signer verified.
|
|
828
882
|
*
|
|
@@ -873,6 +927,15 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
873
927
|
* content, and a token over it answers for either. `pki.cms.attachTimestamp` adds such an attribute to an
|
|
874
928
|
* existing message and `pki.cms.timestampImprint` produces the imprint a TSA signs.
|
|
875
929
|
*
|
|
930
|
+
* `algorithmProtection` reports the RFC 6211 attribute, `{ present, oid, compared, mismatch }`. The
|
|
931
|
+
* attribute is a copy of the digest and signature algorithm identifiers this signer names, placed
|
|
932
|
+
* where the signature covers them, and `compared` lists the identifiers checked against it. A
|
|
933
|
+
* disagreement fails the signer with `cms/algorithm-protection-mismatch`, which sec. 3.1 requires of
|
|
934
|
+
* any validator that supports the attribute: the identifiers the attribute protects are otherwise
|
|
935
|
+
* covered by nothing, and RFC 8933 sec. 6 names RSASSA-PSS as the case where the signature itself
|
|
936
|
+
* pins no digest. An absent attribute is not a failure unless `requireAlgorithmProtection` asks for
|
|
937
|
+
* one, and `present: false` says so rather than looking like a comparison that passed.
|
|
938
|
+
*
|
|
876
939
|
* `cadesBaseline` is a third claim, `{ conformant, signingCertificateAttribute, findings,
|
|
877
940
|
* requirementsChecked }`: a signature can be sound and still not be a CAdES signature.
|
|
878
941
|
* `requirementsChecked` lists the EN 319 122-1 requirements decided here, each unmet one carrying a
|
|
@@ -908,6 +971,14 @@ async function _verifyOneCountersig(vDer, targetSig, parsedCerts, allowWeakDiges
|
|
|
908
971
|
* trust decision from the signer: an operator may anchor one and not the other.
|
|
909
972
|
* @opts timestampRevocationChecker A revocation checker for the TSA chain, as `pki.path.validate`
|
|
910
973
|
* takes one. Each row reports what it established in `revocationChecked`.
|
|
974
|
+
* @opts requireAlgorithmProtection Fail a signer that carries no RFC 6211
|
|
975
|
+
* `CMSAlgorithmProtection` attribute, with `cms/missing-algorithm-protection`. Off
|
|
976
|
+
* by default: RFC 6211 says nothing about absence and RFC 8933 sec. 4.1 makes
|
|
977
|
+
* including it a SHOULD, so a message without one is not malformed and most CMS in
|
|
978
|
+
* the field carries none. A signer that DOES carry one is always held to it, with no
|
|
979
|
+
* option, because sec. 3.1's comparisons are MUSTs once a validator supports the
|
|
980
|
+
* attribute. The option is the boolean it documents: `false`, `null` and an absent
|
|
981
|
+
* option are the off forms, and any other value is refused `cms/bad-input`.
|
|
911
982
|
* @example
|
|
912
983
|
* var pair = await pki.key.generate("Ed25519");
|
|
913
984
|
* var key = await pki.key.export(pair.privateKey);
|
|
@@ -950,7 +1021,8 @@ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, content:
|
|
|
950
1021
|
* authority is a different trust decision from the signer's: an operator may anchor the TSA while
|
|
951
1022
|
* leaving the signer unanchored, or the reverse, and one `trustAnchors` for both would make those
|
|
952
1023
|
* two answers one. */
|
|
953
|
-
timestampCerts: 1, timestampTrustAnchors: 1, timestampRevocationChecker: 1
|
|
1024
|
+
timestampCerts: 1, timestampTrustAnchors: 1, timestampRevocationChecker: 1,
|
|
1025
|
+
requireAlgorithmProtection: 1 });
|
|
954
1026
|
|
|
955
1027
|
function verify(input, opts) {
|
|
956
1028
|
return guard.async.deferred(function () { return _verify(input, opts); });
|
|
@@ -959,12 +1031,19 @@ function verify(input, opts) {
|
|
|
959
1031
|
async function _verify(input, opts) {
|
|
960
1032
|
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verify options");
|
|
961
1033
|
guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "cms/bad-input", "pki.cms.verify has an unknown option ");
|
|
1034
|
+
/** @internal The two policy switches are taken FIRST, ahead of everything below that can run the
|
|
1035
|
+
* caller's own code. Deriving the message runs accessors on a parsed input, copying the trust
|
|
1036
|
+
* material runs them on each anchor, and a streamed detached content runs the generator while
|
|
1037
|
+
* this verb is pending; any of those can write to an options object the caller still holds, so a
|
|
1038
|
+
* switch read after them is read from whatever they left behind rather than from the call. */
|
|
1039
|
+
var allowWeakDigests = opts.allowWeakDigests === true;
|
|
1040
|
+
var requireProtection = guard.identifier.booleanOption(opts.requireAlgorithmProtection, _err,
|
|
1041
|
+
"cms/bad-input", "pki.cms.verify opts.requireAlgorithmProtection");
|
|
962
1042
|
var parsed = guard.parsed.acceptDerived(input, "cms", function (bytes) {
|
|
963
1043
|
return cms.parse(_snapshotIfBytes(bytes, "pki.cms.verify"));
|
|
964
1044
|
}, _err, "cms/bad-input", "the SignedData");
|
|
965
1045
|
if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
|
|
966
1046
|
var trustCfg = _snapshotTrust(opts);
|
|
967
|
-
var allowWeakDigests = opts.allowWeakDigests === true;
|
|
968
1047
|
var tsTrustCfg = _snapshotTimestampTrust(opts, allowWeakDigests);
|
|
969
1048
|
var parsedCerts = [];
|
|
970
1049
|
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
@@ -991,8 +1070,13 @@ async function _verify(input, opts) {
|
|
|
991
1070
|
var signers = [];
|
|
992
1071
|
for (var _sp = 0; _sp < parsed.signerInfos.length; _sp++) {
|
|
993
1072
|
var siOne = parsed.signerInfos[_sp];
|
|
1073
|
+
/** @internal Read once, before the signature is checked, and used for both the verdict row and
|
|
1074
|
+
* the decision: the attribute is an independent requirement on the same signer, the way trust
|
|
1075
|
+
* is, so it composes onto the verdict rather than being folded into the signature check. */
|
|
1076
|
+
var apReport = _algorithmProtectionReport(siOne, "signatureAlgorithm", siOne.signatureAlgorithm);
|
|
994
1077
|
var verdict = await _verifyOne(siOne, content, eContentType, parsedCerts, null, digestByName, allowWeakDigests);
|
|
995
|
-
|
|
1078
|
+
verdict = _applyAlgorithmProtection(verdict, apReport, requireProtection);
|
|
1079
|
+
var counters = await _verifyCountersignatures(siOne, parsedCerts, allowWeakDigests, requireProtection);
|
|
996
1080
|
/** @internal The timestamp rows are built from the same `signature` octets the signature verdict
|
|
997
1081
|
* above was computed over, which is why they are here and not in a verb of their own. The
|
|
998
1082
|
* signer's own certificate supplies the expiry half of EN 319 122-1 clause 6.3 requirement m. */
|
|
@@ -1204,6 +1288,17 @@ function _addCert(out, der) {
|
|
|
1204
1288
|
* (`timeStampToken`); content-type / message-digest / signing-time are
|
|
1205
1289
|
* rejected here (RFC 5652 sec. 11). Use `pki.cms.countersign` for a
|
|
1206
1290
|
* countersignature specifically.
|
|
1291
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` signed attribute, a copy of
|
|
1292
|
+
* this signer's digest and signature algorithm identifiers placed where the
|
|
1293
|
+
* signature covers them. RFC 8933 sec. 4.1 makes including it a SHOULD, and
|
|
1294
|
+
* RFC 9882 sec. 4 and RFC 9814 sec. 5 repeat that for ML-DSA and SLH-DSA,
|
|
1295
|
+
* which carry no digest identifier inside the signature. Off by default,
|
|
1296
|
+
* because turning it on changes the bytes of every signature. `true` emits
|
|
1297
|
+
* the identifier RFC 6211 sec. 2 states, which OpenSSL and Bouncy Castle
|
|
1298
|
+
* use; `"registry"` emits the S/MIME arc value the IANA registry and
|
|
1299
|
+
* draft-ietf-lamps-rfc6211-update give it. A verifier here reads both.
|
|
1300
|
+
* `false`, `null` and an absent option emit none, and any other value is
|
|
1301
|
+
* refused `cms/bad-input`.
|
|
1207
1302
|
* @example
|
|
1208
1303
|
* var pair = await pki.key.generate("Ed25519");
|
|
1209
1304
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -1246,6 +1341,10 @@ var sign = cmsSign.sign;
|
|
|
1246
1341
|
* is rejected, RFC 5652 sec. 11.4).
|
|
1247
1342
|
* @opts sid `"issuerAndSerial"` (default) or `"ski"`.
|
|
1248
1343
|
* @opts certificates Embed the countersigner certificate(s) in the SignedData. Default true.
|
|
1344
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` attribute on the
|
|
1345
|
+
* countersignature, copying ITS own digest and signature algorithm
|
|
1346
|
+
* identifiers rather than the countersigned signature's. Off by
|
|
1347
|
+
* default; `"registry"` selects the IANA registry identifier.
|
|
1249
1348
|
* @opts pem Return a PEM string instead of a DER Buffer.
|
|
1250
1349
|
* @example
|
|
1251
1350
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -1341,6 +1440,18 @@ var encrypt = cmsEncrypt.encrypt;
|
|
|
1341
1440
|
* @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
1342
1441
|
* @opts oaepHash The RSAES-OAEP hash for ktri recipients wrapping the MAC key: `"sha256"` (default) / `"sha384"` / `"sha512"`.
|
|
1343
1442
|
* @opts ukm User keying material (a Buffer) for kari / kemri recipients.
|
|
1443
|
+
* @opts algorithmProtection Emit the RFC 6211 `CMSAlgorithmProtection` authenticated attribute, a
|
|
1444
|
+
* copy of the digest and MAC algorithm identifiers this structure names. It matters more here
|
|
1445
|
+
* than on a signature: RFC 5652 sec. 9.2 makes the MAC input the DER encoding of `authAttrs`
|
|
1446
|
+
* alone, so `digestAlgorithm` and `macAlgorithm` are covered by nothing, and rewriting the
|
|
1447
|
+
* digest algorithm leaves the MAC verifying while the recipient recomputes the content digest
|
|
1448
|
+
* under the attacker's choice, no key required. `true` emits the identifier RFC 6211 sec. 2
|
|
1449
|
+
* states, `"registry"` the IANA registry value. Off by default, because it changes the bytes
|
|
1450
|
+
* of every message. `false`, `null` and an absent option emit none, and any other value is
|
|
1451
|
+
* refused `cms/bad-input`. With `authenticatedAttributes: false` the MAC covers the content
|
|
1452
|
+
* octets directly and no attribute set is built, so asking for the attribute there is refused
|
|
1453
|
+
* while the off forms are still accepted. `pki.cms.decrypt` reads both identifiers and
|
|
1454
|
+
* compares the copies it finds.
|
|
1344
1455
|
* @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
|
|
1345
1456
|
* @example
|
|
1346
1457
|
* // authenticate a message to an RSA recipient, then verify the MAC via decrypt
|
|
@@ -1403,6 +1514,19 @@ var authenticate = cmsEncrypt.authenticate;
|
|
|
1403
1514
|
* @opts maxIterations Lower the PBKDF2 iteration cap (a DoS bound; downward only).
|
|
1404
1515
|
* @opts stream Return `content` as an async iterable of plaintext `Buffer` chunks instead of a single `Buffer`.
|
|
1405
1516
|
* @opts allowUnauthenticatedRsa15 Read a message that uses RSAES-PKCS1-v1_5 key transport over a content with no integrity tag, such as AES-CBC. Refused by default with `cms/unauthenticated-rsa-v15`: the content padding is that combination's only integrity check, so decrypting it either returns the plaintext of a substituted key or tells an attacker submitting chosen ciphertexts that theirs decoded (RFC 8017 sec. 7.2). `openssl cms -encrypt` emits exactly this combination when no algorithm is named, so reading one of its messages needs this option. An AEAD content or an RSAES-OAEP recipient needs none.
|
|
1517
|
+
* @opts requireAlgorithmProtection For an AuthenticatedData, refuse one that carries no RFC 6211
|
|
1518
|
+
* `CMSAlgorithmProtection` authenticated attribute, with `cms/missing-algorithm-protection`.
|
|
1519
|
+
* Off by default, since RFC 8933 sec. 4.1 makes including it a SHOULD. A message that DOES
|
|
1520
|
+
* carry one is always held to it, with no option: its `digestAlgorithm` and `macAlgorithm`
|
|
1521
|
+
* copies are compared against the fields of the `AuthenticatedData` itself, and a disagreement
|
|
1522
|
+
* is `cms/algorithm-protection-mismatch`. That comparison runs before the MAC, because a
|
|
1523
|
+
* mismatch is a statement about the message rather than about the key. The result's
|
|
1524
|
+
* `algorithmProtection` field reports what was compared. The option is the boolean it
|
|
1525
|
+
* documents: `false`, `null` and an absent option are the off forms, and any other value is
|
|
1526
|
+
* refused `cms/bad-input`. Asking for it while decrypting any other content type is refused
|
|
1527
|
+
* the same way: RFC 8933 sec. 6 gives the attribute a place in an `AuthenticatedData` alone,
|
|
1528
|
+
* so an `EnvelopedData`, `AuthEnvelopedData` or `EncryptedData` cannot satisfy the
|
|
1529
|
+
* requirement and returning its plaintext would answer neither question.
|
|
1406
1530
|
* @example
|
|
1407
1531
|
* var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
|
|
1408
1532
|
* var pair = await pki.key.generate(rsa);
|
package/lib/crmf-sign.js
CHANGED
|
@@ -943,7 +943,7 @@ async function _assertEnclosedKeyCorresponds(privateKeyDer, templateSpki) {
|
|
|
943
943
|
* failing, not the input. */
|
|
944
944
|
if (e && e.isPkiError === true && e.code === "key/bad-input") {
|
|
945
945
|
throw _err("crmf/bad-input", "pop.privateKey could not be read as a private key to check against " +
|
|
946
|
-
"certTemplate.publicKey: " + (e
|
|
946
|
+
"certTemplate.publicKey: " + guard.text.describeThrown(e), e);
|
|
947
947
|
}
|
|
948
948
|
throw _err("crmf/bad-popo", "the private key enclosed in the encryptedKey proof of possession could not be " +
|
|
949
949
|
"exercised with certTemplate.publicKey, so it cannot be shown to be that key's private half (RFC 4211 sec. 4.2)", e);
|
package/lib/ct.js
CHANGED
|
@@ -2177,7 +2177,7 @@ function _b64Array(obj, name, code, label, exactLen, maxCount) {
|
|
|
2177
2177
|
* @opts headers OPTIONAL extra request headers (the request-framing headers are stripped; the verb owns the GET method).
|
|
2178
2178
|
* @opts timeout OPTIONAL ms budget, default 30s (cap-validated).
|
|
2179
2179
|
* @opts maxResponseBytes OPTIONAL per-GET size cap, default 4 MiB, tightenable DOWNWARD only.
|
|
2180
|
-
* @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET.
|
|
2180
|
+
* @opts requireJsonContentType OPTIONAL boolean (default false) -- opt in to a strict `ct/bad-content-type` gate on the JSON GET. A value other than `true`, `false`, `null` or an absent option is refused `ct/bad-input` before the request goes out.
|
|
2181
2181
|
* @example
|
|
2182
2182
|
* // a live distributor uses the default pki.transport.https; here an injected transport returns the pair
|
|
2183
2183
|
* var r = await pki.ct.fetchLogList({ url: "https://ct.example/log_list.json", signerKey: googleSignerSpki,
|
|
@@ -2241,7 +2241,8 @@ async function fetchLogList(opts) {
|
|
|
2241
2241
|
var req = { headers: headers, tls: fetchTls, timeout: timeout, maxResponseBytes: maxResponseBytes };
|
|
2242
2242
|
/** @internal Read before the request goes out, so the option that decides the check is the one the call
|
|
2243
2243
|
* was made with rather than whatever it says once the response is back. */
|
|
2244
|
-
var requireJsonContentType = opts.requireJsonContentType
|
|
2244
|
+
var requireJsonContentType = guard.identifier.booleanOption(opts.requireJsonContentType, _ctErr,
|
|
2245
|
+
"ct/bad-input", "fetchLogList opts.requireJsonContentType");
|
|
2245
2246
|
var jsonRes = await _fetchBody(transport, jsonUrl, req, "CT log-list JSON");
|
|
2246
2247
|
if (requireJsonContentType && jsonRes.contentType !== "application/json") throw _ctErr("ct/bad-content-type", "the CT log-list JSON GET returned content-type " + JSON.stringify(jsonRes.contentType || null) + " (opts.requireJsonContentType is set)");
|
|
2247
2248
|
var sigRes = await _fetchBody(transport, sigUrl, req, "CT log-list signature");
|