@blamejs/pki 0.1.31 → 0.2.0
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 +33 -0
- package/README.md +2 -1
- package/index.js +7 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +29 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +11 -0
- package/lib/guard-all.js +15 -0
- package/lib/guard-crypto.js +31 -1
- package/lib/guard-encoding.js +90 -0
- package/lib/guard-identifier.js +56 -0
- package/lib/guard-json.js +149 -0
- package/lib/guard-limits.js +58 -8
- package/lib/guard-name.js +61 -3
- package/lib/guard-range.js +40 -7
- package/lib/guard-text.js +7 -0
- package/lib/jose.js +17 -128
- package/lib/merkle.js +7 -16
- package/lib/oid.js +22 -9
- package/lib/path-validate.js +461 -56
- package/lib/schema-attrcert.js +5 -0
- package/lib/schema-cms.js +22 -1
- package/lib/schema-crmf.js +3 -5
- package/lib/schema-ocsp.js +5 -1
- package/lib/schema-pkix.js +92 -12
- package/lib/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-attrcert.js
CHANGED
|
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
|
|
|
37
37
|
var schema = require("./schema-engine");
|
|
38
38
|
var pkix = require("./schema-pkix");
|
|
39
39
|
var oid = require("./oid");
|
|
40
|
+
var guard = require("./guard-all");
|
|
40
41
|
var frameworkError = require("./framework-error");
|
|
41
42
|
|
|
42
43
|
var AttrCertError = frameworkError.AttrCertError;
|
|
@@ -119,6 +120,10 @@ var OBJECT_DIGEST_INFO = schema.seq([
|
|
|
119
120
|
if (m.fields.otherObjectTypeID.present) {
|
|
120
121
|
throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 sec. 7.3 forbids");
|
|
121
122
|
}
|
|
123
|
+
// The objectDigest is a whole-octet digest over the identified object; a
|
|
124
|
+
// non-octet-aligned BIT STRING is malformed (RFC 5755 sec. 4.1) and has no
|
|
125
|
+
// in-tree verify layer to catch it later, so reject it at parse.
|
|
126
|
+
guard.crypto.assertOctetAligned(m.fields.objectDigest.value, ctx.E, "attrcert/bad-object-digest-info", "objectDigest");
|
|
122
127
|
return {
|
|
123
128
|
digestedObjectType: t,
|
|
124
129
|
otherObjectTypeID: null,
|
package/lib/schema-cms.js
CHANGED
|
@@ -214,7 +214,13 @@ function _assertContentTypeMatchesAttrs(attrs, eContentType) {
|
|
|
214
214
|
for (var i = 0; i < attrs.length; i++) {
|
|
215
215
|
if (attrs[i].type !== OID_CONTENT_TYPE) continue;
|
|
216
216
|
if (attrs[i].values.length !== 1) throw NS.E("cms/bad-content-type-attr", "the content-type attribute must be single-valued (RFC 5652 sec. 11.1)");
|
|
217
|
-
|
|
217
|
+
// ContentType ::= OBJECT IDENTIFIER -- validate the value's full syntax (tag AND
|
|
218
|
+
// minimal base-128 OID content) with the CMS typed verdict, so a malformed value
|
|
219
|
+
// on a path that does not run _checkContentBindingAttrs (AuthEnvelopedData,
|
|
220
|
+
// RFC 5083 sec. 2.1) surfaces cms/bad-content-type-attr, not the raw asn1/* error.
|
|
221
|
+
var ctv;
|
|
222
|
+
try { ctv = asn1.read.oid(asn1.decode(attrs[i].values[0])); }
|
|
223
|
+
catch (e) { throw NS.E("cms/bad-content-type-attr", "the content-type attribute value must be a valid OBJECT IDENTIFIER", e); }
|
|
218
224
|
if (ctv !== eContentType) throw NS.E("cms/content-type-mismatch", "the content-type attribute (" + ctv + ") must equal the eContentType (" + eContentType + ") (RFC 5652 sec. 5.3)");
|
|
219
225
|
}
|
|
220
226
|
}
|
|
@@ -1155,6 +1161,20 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
|
|
|
1155
1161
|
function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
|
|
1156
1162
|
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
1157
1163
|
|
|
1164
|
+
// assertAttachedCiphertext(eci, E, code, label): reject an EnvelopedData /
|
|
1165
|
+
// EncryptedData encryptedContentInfo whose encryptedContent (the payload itself)
|
|
1166
|
+
// is absent OR zero-length. RFC 5652 sec. 6.1 / RFC 5083 permit DETACHED content,
|
|
1167
|
+
// so this is NOT enforced in the base walker -- it is a per-PROFILE opt-in for a
|
|
1168
|
+
// consumer that requires the ciphertext to be present (an EST server-generated
|
|
1169
|
+
// key, a PKCS#12 shrouded bag): a valid envelope that delivers NOTHING is
|
|
1170
|
+
// verification-of-nothing (CWE-20). E is the caller's (code, message) factory.
|
|
1171
|
+
function assertAttachedCiphertext(eci, E, code, label) {
|
|
1172
|
+
if (!eci || eci.encryptedContent === null || eci.encryptedContent.length === 0) {
|
|
1173
|
+
throw E(code, (label || "encrypted content") + " must carry a non-empty attached ciphertext (RFC 5652 sec. 6.1), not be detached or empty");
|
|
1174
|
+
}
|
|
1175
|
+
return eci;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1158
1178
|
module.exports = {
|
|
1159
1179
|
parse: parse,
|
|
1160
1180
|
pemDecode: pemDecode,
|
|
@@ -1163,4 +1183,5 @@ module.exports = {
|
|
|
1163
1183
|
walkEnvelopedData: walkEnvelopedData,
|
|
1164
1184
|
walkSignedData: walkSignedData,
|
|
1165
1185
|
walkEncryptedData: walkEncryptedData,
|
|
1186
|
+
assertAttachedCiphertext: assertAttachedCiphertext,
|
|
1166
1187
|
};
|
package/lib/schema-crmf.js
CHANGED
|
@@ -255,11 +255,9 @@ function popoPrivKey(type) {
|
|
|
255
255
|
}
|
|
256
256
|
// The encrypted key material MUST be present -- CMS allows a detached
|
|
257
257
|
// EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
|
|
258
|
-
// or archive is meaningless.
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry a non-empty encrypted key in encryptedContent (RFC 4211 sec. 4.2)");
|
|
262
|
-
}
|
|
258
|
+
// or archive is meaningless. The shared CMS assert rejects a null OR
|
|
259
|
+
// zero-length attached ciphertext (the rule the EST / PKCS#12 siblings hold).
|
|
260
|
+
cms.assertAttachedCiphertext(env.encryptedContentInfo, ctx.E, "crmf/bad-popo", "encryptedKey [4] EnvelopedData");
|
|
263
261
|
}
|
|
264
262
|
return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
|
|
265
263
|
});
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
|
|
|
37
37
|
var schema = require("./schema-engine");
|
|
38
38
|
var pkix = require("./schema-pkix");
|
|
39
39
|
var oid = require("./oid");
|
|
40
|
+
var constants = require("./constants");
|
|
40
41
|
var frameworkError = require("./framework-error");
|
|
41
42
|
|
|
42
43
|
var OcspError = frameworkError.OcspError;
|
|
@@ -119,7 +120,10 @@ function _rawSignature(field) {
|
|
|
119
120
|
|
|
120
121
|
// certs [0] EXPLICIT SEQUENCE OF Certificate -- each element raw. Shared by the
|
|
121
122
|
// request Signature and the BasicOCSPResponse.
|
|
122
|
-
|
|
123
|
+
// The certs list is capped: the delegated-responder authorization loop verifies
|
|
124
|
+
// each candidate BEFORE the response signature is checked, so an unbounded list in
|
|
125
|
+
// a relayed / stapled response would drive unbounded pre-auth signature verifies.
|
|
126
|
+
var CERTS = schema.seqOf(certificateBytes(), { assert: "sequence", code: "ocsp/bad-certs", what: "certs", max: constants.LIMITS.OCSP_MAX_CERTS, maxCode: "ocsp/too-many-certs" });
|
|
123
127
|
|
|
124
128
|
// Validate the OCSP protocol extension values in a decoded extension list --
|
|
125
129
|
// applied to every extension surface (request / single-request / response /
|
package/lib/schema-pkix.js
CHANGED
|
@@ -47,11 +47,8 @@ function pemDecode(text, label, PemError) {
|
|
|
47
47
|
if (!m) throw new PemError("pem/no-block", "no PEM block found");
|
|
48
48
|
if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
|
|
49
49
|
var b64 = m[2].replace(/[\r\n\t ]+/g, "");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
var der = Buffer.from(b64, "base64");
|
|
53
|
-
if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
|
|
54
|
-
return der;
|
|
50
|
+
// Strict canonical base64 (RFC 4648 sec. 3.5) via the shared encoding guard.
|
|
51
|
+
return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
// pemDecodeAll(text, label, PemError): decode EVERY PEM block under the STRICT
|
|
@@ -72,10 +69,7 @@ function pemDecodeAll(text, label, PemError) {
|
|
|
72
69
|
if (/\S/.test(text.slice(lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
|
|
73
70
|
if (m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
|
|
74
71
|
var b64 = m[2].replace(/[\r\n\t ]+/g, "");
|
|
75
|
-
|
|
76
|
-
var der = Buffer.from(b64, "base64");
|
|
77
|
-
if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
|
|
78
|
-
blocks.push(der);
|
|
72
|
+
blocks.push(guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
|
|
79
73
|
lastEnd = re.lastIndex;
|
|
80
74
|
}
|
|
81
75
|
if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
|
|
@@ -92,7 +86,10 @@ function pemEncode(der, label, PemError) {
|
|
|
92
86
|
if (typeof label !== "string" || !/^[A-Z0-9]+( [A-Z0-9]+)*$/.test(label)) {
|
|
93
87
|
throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
|
|
94
88
|
}
|
|
95
|
-
|
|
89
|
+
// Re-view through the byte guard: a string input would silently utf8-armor into
|
|
90
|
+
// a bogus PEM, and a detached-backed Buffer would armor an empty body -- both
|
|
91
|
+
// fail closed (a Uint8Array / Buffer of real DER re-views cleanly).
|
|
92
|
+
var buf = guard.bytes.view(der, PemError, "pem/bad-input", "pemEncode DER input");
|
|
96
93
|
var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
|
|
97
94
|
return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
|
|
98
95
|
}
|
|
@@ -270,10 +267,12 @@ function attrValueToString(ns) {
|
|
|
270
267
|
if (typeof value === "string" && value.charAt(0) === "\\") return asn1.build.utf8(value.slice(1));
|
|
271
268
|
if (typeof value === "string" && value.charAt(0) === "#") {
|
|
272
269
|
var hex = value.slice(1);
|
|
273
|
-
if (
|
|
270
|
+
if (hex.length === 0) {
|
|
274
271
|
throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
|
|
275
272
|
}
|
|
276
|
-
|
|
273
|
+
// Strict canonical hex via the shared encoding guard (even-length, canonical,
|
|
274
|
+
// no silent truncation at the first non-hex character).
|
|
275
|
+
var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
|
|
277
276
|
try { asn1.decode(raw); }
|
|
278
277
|
catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
|
|
279
278
|
return raw;
|
|
@@ -550,6 +549,36 @@ function generalNames(ns, opts) {
|
|
|
550
549
|
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
551
550
|
}
|
|
552
551
|
|
|
552
|
+
// DistributionPointName ::= CHOICE { fullName [0] IMPLICIT GeneralNames,
|
|
553
|
+
// nameRelativeToCRLIssuer [1] IMPLICIT RelativeDistinguishedName } (RFC 5280
|
|
554
|
+
// sec. 4.2.1.13). The caller hands the CHOICE node itself -- the alternative
|
|
555
|
+
// INSIDE the [0]-tagged distributionPoint field wrapper (a context tag on a
|
|
556
|
+
// CHOICE-typed field is always EXPLICIT, so the wire nests [0]{ [0]|[1] ... }).
|
|
557
|
+
// fullName surfaces each element's RAW GeneralName DER as { kind: "fullName",
|
|
558
|
+
// names: [Buffer, ...] } -- the byte-exact comparison key the sec. 5.2.5
|
|
559
|
+
// "identical encoding MUST be used" correspondence rule requires;
|
|
560
|
+
// nameRelativeToCRLIssuer surfaces its full [1]-tagged TLV as { kind: "rdn",
|
|
561
|
+
// bytes: Buffer } (byte-identical [1] TLVs are byte-identical RDN fragments).
|
|
562
|
+
// An empty fullName (GeneralNames is SIZE 1..MAX), a malformed element, or any
|
|
563
|
+
// other alternative throws the caller's `code`. Shared by the
|
|
564
|
+
// cRLDistributionPoints / freshestCRL extension decoders and the CRL checker's
|
|
565
|
+
// IssuingDistributionPoint decode, so the two sides of the sec. 6.3.3(b)(2)(i)
|
|
566
|
+
// correspondence comparison cannot drift.
|
|
567
|
+
function distributionPointName(ns, node, code) {
|
|
568
|
+
if (node && node.tagClass === "context" && node.tagNumber === 0) {
|
|
569
|
+
var gns = schema.walk(generalNames(ns, { implicitTag: 0, code: code, what: "DistributionPointName fullName" }), node, ns).result;
|
|
570
|
+
return { kind: "fullName", names: gns.names.map(function (n) { return n.bytes; }) };
|
|
571
|
+
}
|
|
572
|
+
if (node && node.tagClass === "context" && node.tagNumber === 1) {
|
|
573
|
+
// [1] IMPLICIT RelativeDistinguishedName -- the context tag replaces the
|
|
574
|
+
// SET tag, so the node's direct children are the AttributeTypeAndValues.
|
|
575
|
+
schema.walk(schema.implicitSetOf(1, attributeTypeAndValue(ns), {
|
|
576
|
+
min: 1, code: code, what: "DistributionPointName nameRelativeToCRLIssuer" }), node, ns);
|
|
577
|
+
return { kind: "rdn", bytes: node.bytes };
|
|
578
|
+
}
|
|
579
|
+
throw ns.E(code, "DistributionPointName must be fullName [0] or nameRelativeToCRLIssuer [1] (RFC 5280 sec. 4.2.1.13)");
|
|
580
|
+
}
|
|
581
|
+
|
|
553
582
|
// certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
|
|
554
583
|
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
555
584
|
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
@@ -837,6 +866,54 @@ function certExtensionDecoders(ns) {
|
|
|
837
866
|
return { poison: true };
|
|
838
867
|
}
|
|
839
868
|
|
|
869
|
+
// cRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
|
870
|
+
// (RFC 5280 sec. 4.2.1.13); DistributionPoint ::= SEQUENCE {
|
|
871
|
+
// distributionPoint [0] DistributionPointName OPTIONAL, reasons [1]
|
|
872
|
+
// ReasonFlags OPTIONAL, cRLIssuer [2] GeneralNames OPTIONAL }. "a
|
|
873
|
+
// DistributionPoint MUST NOT consist of only the reasons field; either
|
|
874
|
+
// distributionPoint or cRLIssuer MUST be present" -- a reasons-only (or
|
|
875
|
+
// empty) DistributionPoint is malformed. The DistributionPointName is
|
|
876
|
+
// surfaced through the shared helper (raw name encodings -- the sec. 5.2.5
|
|
877
|
+
// correspondence key); `reasons` stays a raw BIT STRING ({ unusedBits,
|
|
878
|
+
// bytes }); cRLIssuer is a validated GeneralNames with decoded values.
|
|
879
|
+
// FreshestCRL ::= CRLDistributionPoints (sec. 4.2.1.15), so the SAME decoder
|
|
880
|
+
// serves both OIDs -- one codec, both registry rows.
|
|
881
|
+
function crlDistributionPoints(buf) {
|
|
882
|
+
var C = ns.prefix + "/bad-crl-distribution-points";
|
|
883
|
+
var kids = seqChildren(buf, C, "CRLDistributionPoints");
|
|
884
|
+
if (kids.length < 1) throw ns.E(C, "CRLDistributionPoints must contain at least one DistributionPoint (RFC 5280 sec. 4.2.1.13)");
|
|
885
|
+
return kids.map(function (dp) {
|
|
886
|
+
if (dp.tagClass !== "universal" || dp.tagNumber !== _T.SEQUENCE || !dp.children) {
|
|
887
|
+
throw ns.E(C, "DistributionPoint must be a SEQUENCE { distributionPoint?, reasons?, cRLIssuer? }");
|
|
888
|
+
}
|
|
889
|
+
var out = { distributionPoint: null, reasons: null, cRLIssuer: null };
|
|
890
|
+
var lastTag = -1;
|
|
891
|
+
dp.children.forEach(function (f) {
|
|
892
|
+
if (f.tagClass !== "context") throw ns.E(C, "DistributionPoint fields are context-tagged [0]/[1]/[2]");
|
|
893
|
+
if (f.tagNumber <= lastTag) throw ns.E(C, "DistributionPoint fields must be unique and in ascending order (DER)");
|
|
894
|
+
lastTag = f.tagNumber;
|
|
895
|
+
if (f.tagNumber === 0) {
|
|
896
|
+
// distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE.
|
|
897
|
+
if (!f.children || f.children.length !== 1) throw ns.E(C, "DistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
|
|
898
|
+
out.distributionPoint = distributionPointName(ns, f.children[0], C);
|
|
899
|
+
} else if (f.tagNumber === 1) {
|
|
900
|
+
var bs;
|
|
901
|
+
try { bs = asn1.read.bitStringImplicit(f, 1); }
|
|
902
|
+
catch (e) { throw ns.E(C, "DistributionPoint reasons [1] must be an IMPLICIT ReasonFlags BIT STRING", e); }
|
|
903
|
+
out.reasons = { unusedBits: bs.unusedBits, bytes: bs.bytes };
|
|
904
|
+
} else if (f.tagNumber === 2) {
|
|
905
|
+
out.cRLIssuer = schema.walk(generalNames(ns, { implicitTag: 2, decodeValue: true, code: C, what: "DistributionPoint cRLIssuer" }), f, ns).result;
|
|
906
|
+
} else {
|
|
907
|
+
throw ns.E(C, "DistributionPoint has an unexpected field [" + f.tagNumber + "]");
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
if (out.distributionPoint === null && out.cRLIssuer === null) {
|
|
911
|
+
throw ns.E(C, "a DistributionPoint must include distributionPoint or cRLIssuer -- it MUST NOT consist of only the reasons field (RFC 5280 sec. 4.2.1.13)");
|
|
912
|
+
}
|
|
913
|
+
return out;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
840
917
|
// byName returns undefined for an unregistered name; keying a decoder row
|
|
841
918
|
// under "undefined" would silently drop that extension from dispatch (the
|
|
842
919
|
// consumer would treat the extension as structurally unvalidated / absent),
|
|
@@ -861,6 +938,8 @@ function certExtensionDecoders(ns) {
|
|
|
861
938
|
byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
|
|
862
939
|
byOid[O("signedCertificateTimestampList")] = sctList;
|
|
863
940
|
byOid[O("precertificatePoison")] = precertPoison;
|
|
941
|
+
byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
|
|
942
|
+
byOid[O("freshestCRL")] = crlDistributionPoints;
|
|
864
943
|
return { byOid: byOid };
|
|
865
944
|
}
|
|
866
945
|
|
|
@@ -1069,6 +1148,7 @@ module.exports = {
|
|
|
1069
1148
|
name: name,
|
|
1070
1149
|
generalName: generalName,
|
|
1071
1150
|
generalNames: generalNames,
|
|
1151
|
+
distributionPointName: distributionPointName,
|
|
1072
1152
|
generalizedTime: generalizedTime,
|
|
1073
1153
|
utf8Text: utf8Text,
|
|
1074
1154
|
rawNonEmptySequence: rawNonEmptySequence,
|