@blamejs/pki 0.8.50 → 0.8.52
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 +4 -4
- package/index.js +2 -1
- package/lib/acme.js +33 -13
- package/lib/cmp-build.js +9 -3
- package/lib/cms-sign.js +254 -0
- package/lib/cms-verify.js +456 -8
- package/lib/constants.js +18 -2
- package/lib/est.js +31 -14
- package/lib/guard-intrinsic.js +4 -0
- package/lib/guard-time.js +98 -0
- package/lib/http-digest.js +7 -2
- package/lib/inspect.js +3 -2
- package/lib/lint.js +105 -2
- package/lib/path-validate.js +89 -8
- package/lib/pki-build.js +40 -0
- package/lib/revocation-fetch.js +11 -5
- package/lib/scep.js +26 -12
- package/lib/schema-all.js +1 -1
- package/lib/schema-smime.js +215 -5
- package/lib/tsp-sign.js +58 -95
- package/lib/x509-sign.js +22 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/pki-build.js
CHANGED
|
@@ -851,6 +851,39 @@ function makeBuilder(ctx) {
|
|
|
851
851
|
}
|
|
852
852
|
/** @internal InhibitAnyPolicy (RFC 5280 sec. 4.2.1.14): a bare SkipCerts INTEGER. */
|
|
853
853
|
function extInhibitAnyPolicy(v) { return b.integer(skipCount(v, "inhibitAnyPolicy")); }
|
|
854
|
+
/** @internal Features (RFC 7633 sec. 4): `SEQUENCE OF INTEGER`, each a TLS ExtensionType value.
|
|
855
|
+
* sec. 4.1 binds those values to the IANA TLS Extensions registry, so the registry names this
|
|
856
|
+
* build knows are accepted beside the numbers, resolved through the one table `C.NAMES.TLS_FEATURE`
|
|
857
|
+
* that `pki.inspect` prints them from rather than a second copy of it. The type states no range and
|
|
858
|
+
* no ordering, so a value outside the registry's 16-bit width and a repeat both encode, and the
|
|
859
|
+
* linter is what grades them: an encoder that dropped or sorted them would hide what it emitted
|
|
860
|
+
* from the rule that reads it. An EMPTY list is refused here although the decoder accepts one on
|
|
861
|
+
* the wire, because an empty Features advertises nothing and makes the extension inert, which is a
|
|
862
|
+
* caller's typo rather than a document to read. */
|
|
863
|
+
function extTlsFeature(list) {
|
|
864
|
+
if (!_isArray(list) || !list.length) {
|
|
865
|
+
throw E("bad-input", "tlsFeature must name at least one TLS ExtensionType value (RFC 7633 sec. 4.1); " +
|
|
866
|
+
"an empty Features advertises nothing, so the extension it would emit is inert");
|
|
867
|
+
}
|
|
868
|
+
var dense = reqDenseArray(list, "tlsFeature");
|
|
869
|
+
return b.sequence(_mapIntrinsic(dense, function (f, i) {
|
|
870
|
+
return b.integer(tlsFeatureValue(f, "tlsFeature entry " + i));
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
/** @internal A registry name resolved off the printing table, so the two directions cannot drift. */
|
|
874
|
+
function tlsFeatureValue(f, who) {
|
|
875
|
+
if (typeof f !== "string") {
|
|
876
|
+
return guard.range.authoredInteger(f, rawErr, ctx.prefix + "/bad-input", who);
|
|
877
|
+
}
|
|
878
|
+
var table = C.NAMES.TLS_FEATURE;
|
|
879
|
+
var keys = intrinsic.keys(table);
|
|
880
|
+
for (var i = 0; i < keys.length; i++) {
|
|
881
|
+
if (table[keys[i]] === f) return intrinsic.BigInt(keys[i]);
|
|
882
|
+
}
|
|
883
|
+
throw E("bad-input", who + " names no TLS ExtensionType this build knows: " + _stringify(f) +
|
|
884
|
+
" (accepted names: " + intrinsic.join(_mapIntrinsic(keys, function (k) { return table[k]; }), ", ") +
|
|
885
|
+
"; any other value is given as its number)");
|
|
886
|
+
}
|
|
854
887
|
/** @internal PolicyMappings (RFC 5280 sec. 4.2.1.5): SEQUENCE SIZE (1..MAX) OF
|
|
855
888
|
* { issuerDomainPolicy, subjectDomainPolicy }. The clause forbids mapping either to or from
|
|
856
889
|
* anyPolicy, so neither side may name it. */
|
|
@@ -1282,6 +1315,7 @@ function makeBuilder(ctx) {
|
|
|
1282
1315
|
nameConstraints: 1, authorityInfoAccess: 1, cRLDistributionPoints: 1, freshestCRL: 1,
|
|
1283
1316
|
policyConstraints: 1, inhibitAnyPolicy: 1, policyMappings: 1, policyMappingsCritical: 1,
|
|
1284
1317
|
issuerAltName: 1, qcStatements: 1, qcStatementsCritical: 1,
|
|
1318
|
+
tlsFeature: 1, tlsFeatureCritical: 1,
|
|
1285
1319
|
msCertificateTemplate: 1, msEnrollCertType: 1, msApplicationPolicies: 1,
|
|
1286
1320
|
subjectInfoAccess: 1, subjectDirectoryAttributes: 1,
|
|
1287
1321
|
};
|
|
@@ -1336,6 +1370,11 @@ function makeBuilder(ctx) {
|
|
|
1336
1370
|
if (extSpec.policyMappings != null) guard.list.append(out, ext(O("policyMappings"), !!extSpec.policyMappingsCritical, extPolicyMappings(extSpec.policyMappings)));
|
|
1337
1371
|
if (extSpec.issuerAltName != null) guard.list.append(out, ext(O("issuerAltName"), false, encodeGeneralNames(reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
|
|
1338
1372
|
if (extSpec.qcStatements != null) guard.list.append(out, ext(O("qcStatements"), !!extSpec.qcStatementsCritical, extQcStatements(extSpec.qcStatements)));
|
|
1373
|
+
/** @internal RFC 7633 sec. 4.2.1 lets a request state the features the server supports, and
|
|
1374
|
+
* sec. 4.3.2 asks a server to generate it, so a feature declaration is the requester's wish and
|
|
1375
|
+
* belongs in this table. The criticality knob is here too, because a request carries the
|
|
1376
|
+
* Extension shape whole and the CA decides what it issues. */
|
|
1377
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, ext(O("tlsFeature"), !!extSpec.tlsFeatureCritical, extTlsFeature(extSpec.tlsFeature)));
|
|
1339
1378
|
if (extSpec.msCertificateTemplate != null) guard.list.append(out, ext(O("msCertificateTemplate"), false, extMsCertificateTemplate(extSpec.msCertificateTemplate)));
|
|
1340
1379
|
if (extSpec.msEnrollCertType != null) guard.list.append(out, ext(O("msEnrollCertType"), false, extMsEnrollCertType(extSpec.msEnrollCertType)));
|
|
1341
1380
|
if (extSpec.msApplicationPolicies != null) guard.list.append(out, ext(O("msApplicationPolicies"), false, extCertPolicies(extSpec.msApplicationPolicies)));
|
|
@@ -1395,6 +1434,7 @@ function makeBuilder(ctx) {
|
|
|
1395
1434
|
extNameConstraints: extNameConstraints, extAuthorityInfoAccess: extAuthorityInfoAccess,
|
|
1396
1435
|
extCrlDistributionPoints: extCrlDistributionPoints, extPolicyConstraints: extPolicyConstraints,
|
|
1397
1436
|
extInhibitAnyPolicy: extInhibitAnyPolicy, extPolicyMappings: extPolicyMappings,
|
|
1437
|
+
extTlsFeature: extTlsFeature,
|
|
1398
1438
|
extQcStatements: extQcStatements,
|
|
1399
1439
|
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
|
|
1400
1440
|
requestedExtensions: requestedExtensions,
|
package/lib/revocation-fetch.js
CHANGED
|
@@ -32,8 +32,13 @@ var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
|
32
32
|
|
|
33
33
|
var _append = guard.list.append;
|
|
34
34
|
/** @internal The URL parser, taken at load: the host it reports is the distribution point a fetch is
|
|
35
|
-
* directed to.
|
|
35
|
+
* directed to. Its accessors are taken with it, since the constructor's capture does not cover the
|
|
36
|
+
* prototype the value reads its parts off and the host read here is the one the filter applies to. */
|
|
36
37
|
var _URL = intrinsic.URL;
|
|
38
|
+
var _urlHref = intrinsic.urlHref;
|
|
39
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
40
|
+
var _urlHostname = intrinsic.urlHostname;
|
|
41
|
+
var _urlSetHash = intrinsic.urlSetHash;
|
|
37
42
|
var LIMITS = constants.LIMITS;
|
|
38
43
|
|
|
39
44
|
var CHECKER_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
@@ -167,16 +172,17 @@ function _acceptableUrl(value, net_) {
|
|
|
167
172
|
var u;
|
|
168
173
|
try { u = new _URL(value); }
|
|
169
174
|
catch (_e) { return _no("is not one this client fetches"); } // allow:swallow-unverified a value that is not a URL names no location
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
var scheme = _urlProtocol(u);
|
|
176
|
+
if (scheme !== "https:" && !(net_.allowHttp && scheme === "http:")) return _no("is not one this client fetches");
|
|
177
|
+
var host = _urlHostname(u);
|
|
172
178
|
if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
|
|
173
179
|
if (net.isIP(host) !== 0) {
|
|
174
180
|
if (httpTransport.isBlockedIp(host)) return _no("is not one this client fetches");
|
|
175
181
|
} else if (!net_.guardsAddresses) {
|
|
176
182
|
return _no("names a host whose resolved address opts.transport does not declare it filters");
|
|
177
183
|
}
|
|
178
|
-
u
|
|
179
|
-
return { url: u
|
|
184
|
+
_urlSetHash(u, "");
|
|
185
|
+
return { url: _urlHref(u), why: null };
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
function _no(why) { return { url: null, why: why }; }
|
package/lib/scep.js
CHANGED
|
@@ -43,6 +43,15 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
43
43
|
* handed is taken with it, since a replacement answers with a host of its own. */
|
|
44
44
|
var _URL = intrinsic.URL;
|
|
45
45
|
var _stringOf = intrinsic.String;
|
|
46
|
+
/** @internal And the accessors its result is read through: capturing the constructor does not capture
|
|
47
|
+
* the prototype the value reads its parts off, and the scheme here decides whether a redirect may
|
|
48
|
+
* be followed while the origin decides whether it is a cross-origin one. */
|
|
49
|
+
var _urlHref = intrinsic.urlHref;
|
|
50
|
+
var _urlOrigin = intrinsic.urlOrigin;
|
|
51
|
+
var _urlProtocol = intrinsic.urlProtocol;
|
|
52
|
+
var _urlSearchParams = intrinsic.urlSearchParams;
|
|
53
|
+
var _urlHash = intrinsic.urlHash;
|
|
54
|
+
var _searchSet = intrinsic.uncurry(URLSearchParams.prototype.set);
|
|
46
55
|
var oid = require("./oid");
|
|
47
56
|
var asn1 = require("./asn1-der");
|
|
48
57
|
var b = asn1.build;
|
|
@@ -675,21 +684,26 @@ function _tlsForRequest(opts) {
|
|
|
675
684
|
function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
676
685
|
if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
|
|
677
686
|
var resolved;
|
|
678
|
-
try { resolved = new _URL(_stringOf(location), current
|
|
687
|
+
try { resolved = new _URL(_stringOf(location), _urlHref(current)); }
|
|
679
688
|
catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
680
|
-
|
|
681
|
-
|
|
689
|
+
/** @internal Each scheme read ONCE: the first test decides whether the target is HTTP at all and
|
|
690
|
+
* the second whether it is a downgrade, and those two have to be answered about one value. */
|
|
691
|
+
var toScheme = _urlProtocol(resolved);
|
|
692
|
+
var fromScheme = _urlProtocol(current);
|
|
693
|
+
if (toScheme !== "http:" && toScheme !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + toScheme);
|
|
694
|
+
if (fromScheme === "https:" && toScheme !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + toScheme + " is refused");
|
|
682
695
|
var safe = method === "GET" || method === "HEAD";
|
|
683
|
-
if (resolved
|
|
696
|
+
if (_urlOrigin(resolved) !== _urlOrigin(current) && !safe && !allowCrossOrigin) throw _err("scep/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect");
|
|
684
697
|
return resolved;
|
|
685
698
|
}
|
|
686
699
|
function _scepUrl(baseUrl, operation) {
|
|
687
700
|
var url;
|
|
688
701
|
try { url = new _URL(_stringOf(baseUrl)); }
|
|
689
702
|
catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + _showValue(baseUrl), e); }
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
url
|
|
703
|
+
var baseScheme = _urlProtocol(url);
|
|
704
|
+
if (baseScheme !== "http:" && baseScheme !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + baseScheme);
|
|
705
|
+
if (_urlHash(url)) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
|
|
706
|
+
_searchSet(_urlSearchParams(url), "operation", operation);
|
|
693
707
|
return url;
|
|
694
708
|
}
|
|
695
709
|
function _pkiMethod(opts) {
|
|
@@ -700,27 +714,27 @@ function _pkiMethod(opts) {
|
|
|
700
714
|
}
|
|
701
715
|
function _pkiRequest(method, opUrl, reqMsg, clientOpts) {
|
|
702
716
|
if (method === "GET") {
|
|
703
|
-
var getUrl = new _URL(opUrl
|
|
704
|
-
getUrl
|
|
717
|
+
var getUrl = new _URL(_urlHref(opUrl));
|
|
718
|
+
_searchSet(_urlSearchParams(getUrl), "message", Buffer.from(reqMsg).toString("base64"));
|
|
705
719
|
return _client("GET", getUrl, null, {}, clientOpts);
|
|
706
720
|
}
|
|
707
721
|
return _client("POST", opUrl, reqMsg, { "content-type": CT_PKI_MESSAGE }, clientOpts);
|
|
708
722
|
}
|
|
709
723
|
function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
710
|
-
var redirects = 0, curMethod = method, curBody = body, initialOrigin = url
|
|
724
|
+
var redirects = 0, curMethod = method, curBody = body, initialOrigin = _urlOrigin(url);
|
|
711
725
|
var proxySnap = _snapshotProxy(opts.proxy);
|
|
712
726
|
var tlsBase = _tlsForRequest(opts);
|
|
713
727
|
var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
|
|
714
728
|
function _hopTls(current) {
|
|
715
729
|
var t = tlsBase;
|
|
716
|
-
if (current
|
|
730
|
+
if (_urlOrigin(current) !== initialOrigin) {
|
|
717
731
|
t = Object.assign({}, t);
|
|
718
732
|
t.cert = null; t.key = null; t.servername = null;
|
|
719
733
|
}
|
|
720
734
|
return t;
|
|
721
735
|
}
|
|
722
736
|
function step(current) {
|
|
723
|
-
var req = { method: curMethod, url: current
|
|
737
|
+
var req = { method: curMethod, url: _urlHref(current), headers: headers, tls: _hopTls(current), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes };
|
|
724
738
|
if (proxySnap != null) req.proxy = proxySnap;
|
|
725
739
|
if (curBody != null) req.body = curBody;
|
|
726
740
|
return guard.async.invoked(transport, [req], _err, "scep/bad-input", "opts.transport").then(function (res) {
|
package/lib/schema-all.js
CHANGED
|
@@ -379,7 +379,7 @@ module.exports = {
|
|
|
379
379
|
pemDecode: trustanchor.pemDecode, pemEncode: trustanchor.pemEncode },
|
|
380
380
|
attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode,
|
|
381
381
|
decodeExtensions: attrcert.decodeExtensions, decodeExtension: attrcert.decodeExtension },
|
|
382
|
-
smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
|
|
382
|
+
smime: { buildSigningCertificateV2: smime.buildSigningCertificateV2, parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
|
|
383
383
|
cmc: { parse: cmc.parse, parsePkiData: cmc.parsePkiData, parsePkiResponse: cmc.parsePkiResponse },
|
|
384
384
|
pem: {
|
|
385
385
|
decodeBundle: function (text, opts) { return pkix.pemDecodeBundle(text, opts, PemError, _detectOrNull); },
|
package/lib/schema-smime.js
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* (raw), the hash algorithm (decoded for v2, or the implied SHA-1 for v1), and
|
|
23
23
|
* the optional `issuerSerial` (issuer `GeneralNames` validated + surfaced raw,
|
|
24
24
|
* serial as a BigInt + hex)) plus the optional certificate policies.
|
|
25
|
+
* `buildSigningCertificateV2` encodes the v2 attribute value a signer attaches:
|
|
26
|
+
* one `ESSCertIDv2` per certificate, the SHA-256 form with the DEFAULT algorithm
|
|
27
|
+
* omitted and SHA-384 / SHA-512 with an absent parameters field.
|
|
25
28
|
* `parseSmimeCapabilities` decodes the ordered `SMIMECapabilities` list (each a
|
|
26
29
|
* capability OID + raw parameters). `decodeAttribute` takes a CMS-shaped
|
|
27
30
|
* `{ type, values }` attribute, enforces the single-`AttributeValue` rule
|
|
@@ -46,9 +49,14 @@ var schema = require("./schema-engine");
|
|
|
46
49
|
var pkix = require("./schema-pkix");
|
|
47
50
|
var oid = require("./oid");
|
|
48
51
|
var frameworkError = require("./framework-error");
|
|
52
|
+
var guard = require("./guard-all");
|
|
53
|
+
var intrinsic = require("./guard-intrinsic");
|
|
54
|
+
var constants = require("./constants");
|
|
55
|
+
var x509 = require("./schema-x509");
|
|
49
56
|
|
|
50
57
|
var SmimeError = frameworkError.SmimeError;
|
|
51
58
|
var PemError = frameworkError.PemError;
|
|
59
|
+
var b = asn1.build;
|
|
52
60
|
|
|
53
61
|
var NS = pkix.makeNS("smime", SmimeError, oid);
|
|
54
62
|
var TAGS = asn1.TAGS;
|
|
@@ -61,6 +69,12 @@ var OID_SIGNING_CERTIFICATE = oid.byName("signingCertificate");
|
|
|
61
69
|
var OID_SIGNING_CERTIFICATE_V2 = oid.byName("signingCertificateV2");
|
|
62
70
|
var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
|
|
63
71
|
|
|
72
|
+
/** @internal The digests this module will BIND a certificate with. RFC 5035 sec. 4 leaves
|
|
73
|
+
* `hashAlgorithm` open, and the two readers that consume the field disagree on the set they admit,
|
|
74
|
+
* so the builder emits only what the strictest consumer accepts: SHA-1 is excluded because a
|
|
75
|
+
* binding is an identity claim, and an identifier a verifier refuses is a token nobody can use. */
|
|
76
|
+
var BIND_DIGESTS = intrinsic.assign(intrinsic.create(null), { sha256: 1, sha384: 1, sha512: 1 });
|
|
77
|
+
|
|
64
78
|
var GENERAL_NAME_DIRECTORY = 4;
|
|
65
79
|
var ISSUER_SERIAL = schema.seq([
|
|
66
80
|
schema.field("issuer", pkix.generalNames(NS, { code: "smime/bad-general-names" })),
|
|
@@ -71,7 +85,7 @@ var ISSUER_SERIAL = schema.seq([
|
|
|
71
85
|
return {
|
|
72
86
|
issuer: m.fields.issuer.value.result,
|
|
73
87
|
serialNumber: m.fields.serialNumber.value,
|
|
74
|
-
serialNumberHex: m.fields.serialNumber.node.content
|
|
88
|
+
serialNumberHex: intrinsic.bufToString(m.fields.serialNumber.node.content, "hex"),
|
|
75
89
|
};
|
|
76
90
|
},
|
|
77
91
|
});
|
|
@@ -105,7 +119,7 @@ var POLICY_INFORMATION = schema.seq([
|
|
|
105
119
|
});
|
|
106
120
|
var POLICIES = schema.seqOf(POLICY_INFORMATION, {
|
|
107
121
|
assert: "sequence", min: 0, code: "smime/bad-policies", what: "policies",
|
|
108
|
-
build: function (m) { return m.items
|
|
122
|
+
build: function (m) { return intrinsic.map(m.items, function (it) { return it.value.result; }); },
|
|
109
123
|
});
|
|
110
124
|
|
|
111
125
|
var ESS_CERT_ID = schema.seq([
|
|
@@ -155,7 +169,7 @@ function signingCertificateSchema(essCertId, code, what) {
|
|
|
155
169
|
], {
|
|
156
170
|
assert: "sequence", code: code, what: what,
|
|
157
171
|
build: function (m, ctx) {
|
|
158
|
-
var certs = m.fields.certs.value.items
|
|
172
|
+
var certs = intrinsic.map(m.fields.certs.value.items, function (it) { return it.value.result; });
|
|
159
173
|
assertSignerIssuerIsDirectoryName(certs, function (msg) { throw ctx.E("smime/bad-issuer-serial", msg); });
|
|
160
174
|
return {
|
|
161
175
|
certs: certs,
|
|
@@ -182,7 +196,7 @@ var SMIME_CAPABILITY = schema.seq([
|
|
|
182
196
|
});
|
|
183
197
|
var SMIME_CAPABILITIES = schema.seqOf(SMIME_CAPABILITY, {
|
|
184
198
|
assert: "sequence", min: 0, code: "smime/bad-capabilities", what: "SMIMECapabilities",
|
|
185
|
-
build: function (m) { return { capabilities: m.items
|
|
199
|
+
build: function (m) { return { capabilities: intrinsic.map(m.items, function (it) { return it.value.result; }) }; },
|
|
186
200
|
});
|
|
187
201
|
|
|
188
202
|
/**
|
|
@@ -266,6 +280,197 @@ var parseSigningCertificateV2 = pkix.makeParser({ pemLabel: null, PemError: PemE
|
|
|
266
280
|
*/
|
|
267
281
|
var parseSmimeCapabilities = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: SmimeError, prefix: "smime", what: "SMIMECapabilities", topSchema: SMIME_CAPABILITIES, ns: NS });
|
|
268
282
|
|
|
283
|
+
function _err(code, message) { return new SmimeError(code, message); }
|
|
284
|
+
|
|
285
|
+
/** @internal The digests a signing-certificate identifier may BIND a certificate with, on the
|
|
286
|
+
* reading side. It is the set the builder emits, and it is narrower than the set the ESS decoder
|
|
287
|
+
* will decode: an identifier is an identity claim, so admitting a digest nobody should bind with
|
|
288
|
+
* would let the claim be satisfied by a certificate other than the one that signed. SHA-1 is not
|
|
289
|
+
* here and is reported as weak by its own flag, because the v1 attribute has no algorithm field
|
|
290
|
+
* and is SHA-1 by definition (RFC 2634), so a reader must be able to tell "weak" from "unknown". */
|
|
291
|
+
var BIND_VERIFY_DIGESTS = intrinsic.assign(intrinsic.create(null), { sha256: 1, sha384: 1, sha512: 1 });
|
|
292
|
+
var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true, code: "smime/bad-general-names" });
|
|
293
|
+
|
|
294
|
+
/** @internal What ONE signing-certificate attribute claims about `certDer`, as facts rather than a
|
|
295
|
+
* verdict. Each claim is read and compared here so there is one implementation of "does this
|
|
296
|
+
* identifier name that certificate", and the two consumers apply their own policy to the result:
|
|
297
|
+
* `pki.tsp.verify` refuses a token, and `pki.cms.verify` reports a CAdES conformance finding.
|
|
298
|
+
*
|
|
299
|
+
* An ESSCertID states the signer in two independent ways, and both are compared: the hash under
|
|
300
|
+
* the identifier's own digest, and the issuer and serial where it carries them (RFC 5035 clause 5,
|
|
301
|
+
* RFC 2634 clause 5.4). A hash that agrees beside an issuer and serial naming another certificate
|
|
302
|
+
* is a contradiction, not a match. Identifiers after the first are not read: RFC 2634 clause 5.4
|
|
303
|
+
* makes the first the signing certificate and leaves the rest as the chain. */
|
|
304
|
+
function _bindingClaim(attr) {
|
|
305
|
+
var out = { form: attr.type === OID_SIGNING_CERTIFICATE_V2 ? "v2" : "v1", readable: false,
|
|
306
|
+
digestName: null, supported: false, weak: false, certHash: null, issuerSerial: null };
|
|
307
|
+
var decoded;
|
|
308
|
+
try { decoded = decodeAttribute(attr); }
|
|
309
|
+
catch (_e) { return out; }
|
|
310
|
+
var ess = decoded.certs && decoded.certs[0];
|
|
311
|
+
if (!ess) return out;
|
|
312
|
+
/** @internal The digest identifier is held to its own encoding before its NAME is read. A SHA-2
|
|
313
|
+
* identifier carries absent or NULL parameters and nothing else (RFC 5754 clause 2, "MUST accept
|
|
314
|
+
* SHA2 AlgorithmIdentifiers with absent parameters ... with NULL parameters"), and the same two
|
|
315
|
+
* forms are what RFC 3370 clause 2.1 states for SHA-1. The ANY parameters field is carried
|
|
316
|
+
* through the parse unvalidated, so an identifier naming a digest this build computes while
|
|
317
|
+
* carrying arbitrary bytes beside it would otherwise be hashed as a well-formed binding. */
|
|
318
|
+
if (!guard.der.paramsAbsentOrNull(ess.hashAlgorithm == null ? null : ess.hashAlgorithm.parameters)) return out;
|
|
319
|
+
out.readable = true;
|
|
320
|
+
out.digestName = ess.hashAlgorithm == null ? "sha1" : ess.hashAlgorithm.name;
|
|
321
|
+
out.weak = intrinsic.hasOwn(constants.NAMES.WEAK_DIGESTS, out.digestName);
|
|
322
|
+
out.supported = out.weak || intrinsic.hasOwn(BIND_VERIFY_DIGESTS, out.digestName);
|
|
323
|
+
out.certHash = ess.certHash;
|
|
324
|
+
out.issuerSerial = ess.issuerSerial || null;
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** @internal One claim compared against one certificate. The claim is read ONCE, by
|
|
329
|
+
* `certBindingClaims`, and this is what runs per certificate: a signer identifier can match many
|
|
330
|
+
* candidates, and re-reading the attribute for each would multiply the attribute's size by the
|
|
331
|
+
* candidate count before any signature is checked. Only the first identifier is ever compared, so
|
|
332
|
+
* only it is kept.
|
|
333
|
+
*
|
|
334
|
+
* @internal
|
|
335
|
+
*/
|
|
336
|
+
function certBindingAgainst(claim, certDer) {
|
|
337
|
+
var out = { form: claim.form, readable: claim.readable, digestName: claim.digestName,
|
|
338
|
+
supported: claim.supported, weak: claim.weak, matches: null, certUnreadable: false,
|
|
339
|
+
issuerSerialPresent: claim.issuerSerial !== null, issuerSerialMatches: null };
|
|
340
|
+
if (!claim.readable || !claim.supported) return out;
|
|
341
|
+
/** @internal Through the captured digest and the constant-time comparison: this IS the binding
|
|
342
|
+
* between a signature and the certificate its attribute names, so neither what the digest covers
|
|
343
|
+
* nor how the result is compared is the caller's to choose. */
|
|
344
|
+
out.matches = guard.crypto.constantTimeEqual(guard.crypto.digest(claim.digestName, certDer), claim.certHash);
|
|
345
|
+
if (claim.issuerSerial === null) return out;
|
|
346
|
+
/** @internal A certificate that does not parse is its own fact, kept apart from an attribute that
|
|
347
|
+
* does not decode: one says the identifier is malformed and the other says the thing it would be
|
|
348
|
+
* compared against is, and a consumer orders the two itself. Folding them into one flag moved a
|
|
349
|
+
* refusal from "this identifier names another certificate" to "this attribute is unreadable". */
|
|
350
|
+
var cert;
|
|
351
|
+
try { cert = x509.parse(certDer); }
|
|
352
|
+
catch (_e2) { out.certUnreadable = true; return out; }
|
|
353
|
+
if (claim.issuerSerial.serialNumber !== cert.serialNumber) { out.issuerSerialMatches = false; return out; }
|
|
354
|
+
var names = claim.issuerSerial.issuer.names || [];
|
|
355
|
+
var gn = null;
|
|
356
|
+
try {
|
|
357
|
+
if (names.length === 1) {
|
|
358
|
+
gn = schema.embeddedDer(GENERAL_NAME, names[0].bytes, NS,
|
|
359
|
+
{ code: "smime/bad-general-names", what: "GeneralName" });
|
|
360
|
+
}
|
|
361
|
+
} catch (_e3) { gn = null; }
|
|
362
|
+
out.issuerSerialMatches = !!gn && gn.tagNumber === GENERAL_NAME_DIRECTORY && !!gn.value &&
|
|
363
|
+
guard.name.dnEqual(gn.value.rdns, cert.issuer.rdns, _err, "smime/bad-issuer-serial",
|
|
364
|
+
"the ESSCertID issuer");
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** @internal Every signing-certificate attribute in `signedAttrs`, read against `certDer`.
|
|
369
|
+
* `chosen` is the v2 attribute where there is one, which supersedes v1 (RFC 5035); `superseded`
|
|
370
|
+
* holds the rest, which a consumer still holds to the same claim, because one structure making two
|
|
371
|
+
* incompatible statements about one fact is a contradiction rather than something to resolve in
|
|
372
|
+
* favor of whichever attribute a reader prefers. EVERY attribute is collected rather than the first
|
|
373
|
+
* of each type: `signedAttrs` is a DER SET OF, so which of two comes first is a function of their
|
|
374
|
+
* encodings and a hostile producer picks it.
|
|
375
|
+
*
|
|
376
|
+
* @internal
|
|
377
|
+
*/
|
|
378
|
+
function certBindingClaims(signedAttrs) {
|
|
379
|
+
var found = [];
|
|
380
|
+
intrinsic.forEach(signedAttrs || [], function (a) {
|
|
381
|
+
if (a && (a.type === OID_SIGNING_CERTIFICATE || a.type === OID_SIGNING_CERTIFICATE_V2)) {
|
|
382
|
+
guard.list.append(found, a);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (!found.length) return { present: false, chosen: null, superseded: [] };
|
|
386
|
+
var chosenAt = 0;
|
|
387
|
+
for (var i = 0; i < found.length; i++) {
|
|
388
|
+
if (found[i].type === OID_SIGNING_CERTIFICATE_V2) { chosenAt = i; break; }
|
|
389
|
+
}
|
|
390
|
+
var superseded = [];
|
|
391
|
+
for (var j = 0; j < found.length; j++) {
|
|
392
|
+
if (j !== chosenAt) guard.list.append(superseded, _bindingClaim(found[j]));
|
|
393
|
+
}
|
|
394
|
+
return { present: true, chosen: _bindingClaim(found[chosenAt]), superseded: superseded };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** @internal The claims read and compared in one call, for a consumer holding a single certificate.
|
|
398
|
+
* A consumer with several reads the claims once and calls `certBindingAgainst` per certificate.
|
|
399
|
+
*
|
|
400
|
+
* @internal
|
|
401
|
+
*/
|
|
402
|
+
function certBindingFacts(signedAttrs, certDer) {
|
|
403
|
+
var claims = certBindingClaims(signedAttrs);
|
|
404
|
+
if (!claims.present) return { present: false, chosen: null, superseded: [] };
|
|
405
|
+
return {
|
|
406
|
+
present: true,
|
|
407
|
+
chosen: certBindingAgainst(claims.chosen, certDer),
|
|
408
|
+
superseded: intrinsic.map(claims.superseded, function (c) { return certBindingAgainst(c, certDer); }),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
var _BUILD_V2_OPTS = intrinsic.assign(intrinsic.create(null), { hashAlgorithm: 1 });
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @primitive pki.schema.smime.buildSigningCertificateV2
|
|
416
|
+
* @signature pki.schema.smime.buildSigningCertificateV2(certs, opts?) -> Buffer
|
|
417
|
+
* @since 0.8.52
|
|
418
|
+
* @status stable
|
|
419
|
+
* @spec RFC 5035, RFC 5754
|
|
420
|
+
* @related pki.schema.smime.parseSigningCertificateV2, pki.cms.sign, pki.tsp.sign
|
|
421
|
+
*
|
|
422
|
+
* Encode an ESS `SigningCertificateV2` attribute value (RFC 5035 sec. 5.4.1), the signed attribute
|
|
423
|
+
* that binds a signature to the certificate that made it. `certs` is the signer's certificate DER,
|
|
424
|
+
* or a list of certificate DERs whose first entry is the signer's (RFC 5035 sec. 5.4); each entry
|
|
425
|
+
* becomes an `ESSCertIDv2` carrying that certificate's hash. The result is the single
|
|
426
|
+
* `AttributeValue` of a `signingCertificateV2` signed attribute, which is what CAdES requires a
|
|
427
|
+
* signature to carry (ETSI EN 319 122-1 clauses 5.2.2.2 and 5.2.2.3).
|
|
428
|
+
*
|
|
429
|
+
* `opts.hashAlgorithm` names the binding digest: `sha256` (the default), `sha384` or `sha512`. The
|
|
430
|
+
* SHA-256 form omits the `hashAlgorithm` field, which is the RFC 5035 sec. 4 DEFAULT and so must be
|
|
431
|
+
* absent (X.690 sec. 11.5); the other two emit an `AlgorithmIdentifier` with the parameters field
|
|
432
|
+
* absent, which RFC 5754 sec. 2 requires an implementation to generate. SHA-1 is refused
|
|
433
|
+
* `smime/unsupported-algorithm`: a binding is an identity claim, and `pki.tsp.verify` refuses a
|
|
434
|
+
* SHA-1 one as a weak binding.
|
|
435
|
+
*
|
|
436
|
+
* The OPTIONAL `issuerSerial` and `policies` fields are not emitted; CAdES has the issuer serial
|
|
437
|
+
* omitted (ETSI EN 319 122-1 clause 6.3 requirement g). A caller that needs either field builds the
|
|
438
|
+
* value with `pki.asn1.build`, and `parseSigningCertificateV2` reads both back.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
442
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
443
|
+
* var signerCertDer = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
444
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
|
|
445
|
+
* var av = pki.schema.smime.buildSigningCertificateV2(signerCertDer);
|
|
446
|
+
* var msg = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: key },
|
|
447
|
+
* { additionalSignedAttributes: [{ type: "signingCertificateV2", values: [av] }] });
|
|
448
|
+
* (await pki.cms.verify(msg, { certs: [signerCertDer] })).signers[0].cadesBaseline.conformant; // true
|
|
449
|
+
*/
|
|
450
|
+
function buildSigningCertificateV2(certs, opts) {
|
|
451
|
+
guard.identifier.assertKnownKeys(opts, _BUILD_V2_OPTS, _err, "smime/bad-input",
|
|
452
|
+
"pki.schema.smime.buildSigningCertificateV2 has an unknown option ");
|
|
453
|
+
var settings = guard.identifier.pickSettings(opts, ["hashAlgorithm"]);
|
|
454
|
+
var hashName = settings.hashAlgorithm === undefined || settings.hashAlgorithm === null ? "sha256" : settings.hashAlgorithm;
|
|
455
|
+
if (typeof hashName !== "string" || !BIND_DIGESTS[guard.text.keyOf(hashName)]) {
|
|
456
|
+
throw _err("smime/unsupported-algorithm", "the ESSCertIDv2 binding digest must be sha256, sha384 or sha512, got " +
|
|
457
|
+
guard.text.showValue(hashName));
|
|
458
|
+
}
|
|
459
|
+
var list = guard.list.snapshot(intrinsic.isArray(certs) ? certs : [certs]);
|
|
460
|
+
if (!list.length) {
|
|
461
|
+
throw _err("smime/bad-input", "a SigningCertificateV2 needs at least one certificate, the signer's first (RFC 5035 sec. 5.4)");
|
|
462
|
+
}
|
|
463
|
+
var ids = [];
|
|
464
|
+
for (var i = 0; i < list.length; i++) {
|
|
465
|
+
var der = guard.bytes.view(list[i], SmimeError, "smime/bad-input", "the certificate at certs[" + i + "]");
|
|
466
|
+
var certHash = b.octetString(guard.crypto.digest(hashName, der));
|
|
467
|
+
guard.list.append(ids, hashName === "sha256"
|
|
468
|
+
? b.sequence([certHash])
|
|
469
|
+
: b.sequence([b.sequence([b.oid(oid.byName(hashName))]), certHash]));
|
|
470
|
+
}
|
|
471
|
+
return b.sequence([b.sequence(ids)]);
|
|
472
|
+
}
|
|
473
|
+
|
|
269
474
|
/**
|
|
270
475
|
* @primitive pki.schema.smime.decodeAttribute
|
|
271
476
|
* @signature pki.schema.smime.decodeAttribute(attr) -> { kind, ... }
|
|
@@ -292,7 +497,7 @@ var parseSmimeCapabilities = pkix.makeParser({ pemLabel: null, PemError: PemErro
|
|
|
292
497
|
* got.kind; // "signingCertificateV2"
|
|
293
498
|
*/
|
|
294
499
|
function decodeAttribute(attr) {
|
|
295
|
-
if (!attr || typeof attr !== "object" || typeof attr.type !== "string" || !
|
|
500
|
+
if (!attr || typeof attr !== "object" || typeof attr.type !== "string" || !intrinsic.isArray(attr.values)) {
|
|
296
501
|
throw new SmimeError("smime/bad-input", "decodeAttribute expects a CMS attribute { type, values }");
|
|
297
502
|
}
|
|
298
503
|
if (attr.type === OID_SIGNING_CERTIFICATE || attr.type === OID_SIGNING_CERTIFICATE_V2 || attr.type === OID_SMIME_CAPABILITIES) {
|
|
@@ -313,6 +518,11 @@ function decodeAttribute(attr) {
|
|
|
313
518
|
}
|
|
314
519
|
|
|
315
520
|
module.exports = {
|
|
521
|
+
buildSigningCertificateV2: buildSigningCertificateV2,
|
|
522
|
+
certBindingFacts: certBindingFacts, // @internal
|
|
523
|
+
certBindingClaims: certBindingClaims, // @internal
|
|
524
|
+
certBindingAgainst: certBindingAgainst, // @internal
|
|
525
|
+
|
|
316
526
|
parseSigningCertificate: parseSigningCertificate,
|
|
317
527
|
parseSigningCertificateV2: parseSigningCertificateV2,
|
|
318
528
|
parseSmimeCapabilities: parseSmimeCapabilities,
|