@blamejs/pki 0.3.0 → 0.3.2
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 +21 -1
- package/README.md +2 -0
- package/index.js +11 -0
- package/lib/attrcert-sign.js +504 -0
- package/lib/cms-sign.js +2 -9
- package/lib/csr-sign.js +209 -0
- package/lib/pki-build.js +310 -0
- package/lib/schema-attrcert.js +17 -0
- package/lib/x509-sign.js +14 -284
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/csr-sign.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module pki.csr
|
|
7
|
+
* @nav Signing
|
|
8
|
+
* @title Certification requests
|
|
9
|
+
* @intro The PKCS#10 certification-request producing side. `pki.csr.sign` builds a
|
|
10
|
+
* `CertificationRequestInfo`, signs it with the SUBJECT's own private key (proof of possession -- a
|
|
11
|
+
* CSR has no issuer), and emits a `CertificationRequest` (RFC 2986) that `pki.schema.csr.parse`,
|
|
12
|
+
* OpenSSL, and a CA enrollment pipeline all accept. Requested v3 extensions ride in a PKCS#9
|
|
13
|
+
* `extensionRequest` attribute (RFC 2985) a CA copies into the issued certificate. Parsing lives at
|
|
14
|
+
* `pki.schema.csr.parse`.
|
|
15
|
+
* @spec RFC 2986
|
|
16
|
+
* @card Build and sign a PKCS#10 certification request (proof of possession by the subject key).
|
|
17
|
+
*/
|
|
18
|
+
//
|
|
19
|
+
// The signature algorithm resolves from the SUBJECT public key through the shared sign-scheme registry
|
|
20
|
+
// (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite), so there is no per-algorithm branch here. The
|
|
21
|
+
// distinguished-name / extension / SPKI encoders + the post-sign self-check are the shared lib/pki-build
|
|
22
|
+
// primitives (the same ones pki.x509.sign composes), bound to the csr namespace.
|
|
23
|
+
|
|
24
|
+
var asn1 = require("./asn1-der");
|
|
25
|
+
var oid = require("./oid");
|
|
26
|
+
var csr = require("./schema-csr");
|
|
27
|
+
var signScheme = require("./sign-scheme");
|
|
28
|
+
var pkix = require("./schema-pkix");
|
|
29
|
+
var pkiBuild = require("./pki-build");
|
|
30
|
+
var frameworkError = require("./framework-error");
|
|
31
|
+
|
|
32
|
+
var CsrError = frameworkError.CsrError;
|
|
33
|
+
var b = asn1.build;
|
|
34
|
+
function _err(code, message, cause) { return new CsrError(code, message, cause); }
|
|
35
|
+
function _signE(kind, message, cause) { return new CsrError("csr/" + kind, message, cause); }
|
|
36
|
+
function O(n) { return oid.byName(n); }
|
|
37
|
+
|
|
38
|
+
var NS = pkix.makeNS("csr", CsrError, oid);
|
|
39
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
40
|
+
var _b = pkiBuild.makeBuilder({
|
|
41
|
+
ErrorClass: CsrError, prefix: "csr", O: O, NS: NS,
|
|
42
|
+
NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// The requested-extension object keys (a CSR REQUESTS extensions; it does NOT enforce issuance policy,
|
|
46
|
+
// so there are no CA cross-field gates and no authorityKeyIdentifier auto-derivation).
|
|
47
|
+
var KNOWN_REQ_EXT_KEYS = {
|
|
48
|
+
subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
|
|
49
|
+
basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
|
|
50
|
+
};
|
|
51
|
+
// challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
|
|
52
|
+
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
53
|
+
|
|
54
|
+
// attributes ::= [0] IMPLICIT SET OF Attribute (RFC 2986). The IMPLICIT tag replaces the universal SET,
|
|
55
|
+
// so it is a context-constructed [0] whose DER-sorted members ARE the SET-OF; empty -> A0 00.
|
|
56
|
+
function _implicitSetOf0(members) {
|
|
57
|
+
return b.contextConstructed(0, Buffer.concat(members.slice().sort(Buffer.compare)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The requested v3 Extensions (a bare SEQUENCE OF Extension, RFC 2985 sec. 5.4.2) from the object form,
|
|
61
|
+
// or an array of pre-encoded Extension DER. Reuses the shared extension-value encoders; no CA gates.
|
|
62
|
+
function _buildRequestedExtensions(extSpec, subjectSpki) {
|
|
63
|
+
if (Array.isArray(extSpec)) {
|
|
64
|
+
if (!extSpec.length) throw _err("csr/bad-input", "extensionRequest must carry at least one extension");
|
|
65
|
+
var seen = {};
|
|
66
|
+
return b.sequence(extSpec.map(function (e, i) {
|
|
67
|
+
var der = _b.reqDer(e, "extension");
|
|
68
|
+
_b.assertValidExtension(der, i);
|
|
69
|
+
var n = asn1.decode(der);
|
|
70
|
+
var extnId = asn1.read.oid(n.children[0]);
|
|
71
|
+
if (seen[extnId]) throw _err("csr/bad-input", "duplicate extension " + extnId + " in extensionRequest (RFC 5280 sec. 4.2)");
|
|
72
|
+
seen[extnId] = true;
|
|
73
|
+
// Fully validate a RECOGNIZED extension via its real RFC 5280 sec. 4.2.1 value decoder (a malformed
|
|
74
|
+
// value fails closed with the decoder's typed code); an unrecognized extension stays opaque.
|
|
75
|
+
var dec = EXT_DECODERS[extnId];
|
|
76
|
+
if (dec) {
|
|
77
|
+
try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
|
|
78
|
+
catch (err) { if (err instanceof CsrError) throw err; throw _err("csr/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
|
|
79
|
+
}
|
|
80
|
+
return b.raw(der);
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
if (!extSpec || typeof extSpec !== "object") throw _err("csr/bad-input", "extensionRequest must be an object or an array of pre-encoded Extension DER");
|
|
84
|
+
Object.keys(extSpec).forEach(function (k) {
|
|
85
|
+
if (!KNOWN_REQ_EXT_KEYS[k]) throw _err("csr/bad-input", "unknown extensionRequest extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
|
|
86
|
+
});
|
|
87
|
+
var out = [];
|
|
88
|
+
if (extSpec.subjectKeyIdentifier != null) out.push(_b.ext(O("subjectKeyIdentifier"), false, _b.extSki(_b.skiKeyId(extSpec.subjectKeyIdentifier, subjectSpki))));
|
|
89
|
+
if (extSpec.keyUsage != null) out.push(_b.ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _b.extKeyUsage(extSpec.keyUsage)));
|
|
90
|
+
if (extSpec.extendedKeyUsage != null) out.push(_b.ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _b.extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
91
|
+
if (extSpec.basicConstraints != null) { _b.validateBcSpec(extSpec.basicConstraints); out.push(_b.ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, _b.extBasicConstraints(extSpec.basicConstraints))); }
|
|
92
|
+
if (extSpec.subjectAltName != null) out.push(_b.ext(O("subjectAltName"), false, _b.extSan(extSpec.subjectAltName)));
|
|
93
|
+
if (extSpec.certificatePolicies != null) out.push(_b.ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _b.extCertPolicies(extSpec.certificatePolicies)));
|
|
94
|
+
if (!out.length) throw _err("csr/bad-input", "extensionRequest must request at least one extension");
|
|
95
|
+
return b.sequence(out);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// challengePassword ::= DirectoryString bounded 1..255 (RFC 2985 sec. 5.4.1). PrintableString when the
|
|
99
|
+
// value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
|
|
100
|
+
function _challengePassword(pw) {
|
|
101
|
+
if (typeof pw !== "string" || pw.length < 1 || pw.length > 255) throw _err("csr/bad-input", "challengePassword must be a 1..255 character string (RFC 2985 sec. 5.4.1)");
|
|
102
|
+
return PRINTABLE_RE.test(pw) ? b.printable(pw) : b.utf8(pw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @primitive pki.csr.sign
|
|
107
|
+
* @signature pki.csr.sign(spec, key, opts?) -> Promise<Buffer|string>
|
|
108
|
+
* @since 0.3.1
|
|
109
|
+
* @status stable
|
|
110
|
+
* @spec RFC 2986, RFC 2985
|
|
111
|
+
* @defends forged-certification-request (CWE-347)
|
|
112
|
+
* @related pki.schema.csr.parse, pki.x509.sign
|
|
113
|
+
*
|
|
114
|
+
* Build, sign, and DER-encode a PKCS#10 certification request. `spec` describes the request -- `subject`
|
|
115
|
+
* (a common-name string, an array of RDNs, or raw Name DER; MAY be empty), `subjectPublicKey` (the SPKI
|
|
116
|
+
* DER of the key being certified), and optional `extensionRequest` (requested v3 extensions -- an object
|
|
117
|
+
* of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies /
|
|
118
|
+
* subjectKeyIdentifier, or an array of pre-encoded Extension DER) and `challengePassword`. `key` (or
|
|
119
|
+
* `{ key }`) is the SUBJECT's own PKCS#8 private key / WebCrypto CryptoKey -- the request is self-signed
|
|
120
|
+
* to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
|
|
121
|
+
* request is returned. The signature algorithm is resolved from the subject key (RSA PKCS#1 v1.5 or PSS,
|
|
122
|
+
* ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). Returns DER, or a PEM `CERTIFICATE REQUEST` with
|
|
123
|
+
* `opts.pem`. Malformed input throws a typed `CsrError`. Certificate-request parsing is `pki.schema.csr.parse`.
|
|
124
|
+
*
|
|
125
|
+
* @opts
|
|
126
|
+
* - `pem` (boolean) -- return a PEM `CERTIFICATE REQUEST` string instead of DER.
|
|
127
|
+
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
|
|
128
|
+
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
129
|
+
* @example
|
|
130
|
+
* var req = await pki.csr.sign(
|
|
131
|
+
* { subject: "req.example.com", subjectPublicKey: signerSpki,
|
|
132
|
+
* extensionRequest: { subjectAltName: [{ dNSName: "req.example.com" }] } },
|
|
133
|
+
* { key: signerKeyPkcs8 });
|
|
134
|
+
* pki.schema.csr.parse(req).subject.dn; // "CN=req.example.com"
|
|
135
|
+
*/
|
|
136
|
+
function sign(spec, key, opts) {
|
|
137
|
+
return Promise.resolve().then(function () { return _sign(spec, key, opts); });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _sign(spec, key, opts) {
|
|
141
|
+
opts = opts || {};
|
|
142
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("csr/bad-input", "the certification-request spec must be an object");
|
|
143
|
+
// Reject a typo'd spec field at config-time rather than silently dropping it (a misspelled `subjcet`
|
|
144
|
+
// would otherwise yield an empty-subject request).
|
|
145
|
+
Object.keys(spec).forEach(function (k) {
|
|
146
|
+
if (k !== "subject" && k !== "subjectPublicKey" && k !== "extensionRequest" && k !== "challengePassword" && k !== "attributes") throw _err("csr/bad-input", "unknown spec field " + JSON.stringify(k));
|
|
147
|
+
});
|
|
148
|
+
// The signing key is the second argument (a PKCS#8 key / CryptoKey / composite key pair), or { key }.
|
|
149
|
+
var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
|
|
150
|
+
if (signingKey == null) throw _err("csr/bad-input", "a signing key (the subject's private key) is required");
|
|
151
|
+
|
|
152
|
+
var subjectSpki = _b.reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the requested key)");
|
|
153
|
+
_b.assertValidSpki(subjectSpki, "spec.subjectPublicKey");
|
|
154
|
+
var subjectDer = _b.encodeName(spec.subject == null ? [] : spec.subject);
|
|
155
|
+
|
|
156
|
+
// attributes: extensionRequest + challengePassword + pre-encoded opaque Attribute DER (the escape
|
|
157
|
+
// hatch). A recognized attribute may appear at most once (RFC 2985 SINGLE VALUE + producer hygiene).
|
|
158
|
+
var attrs = [], seenAttr = {};
|
|
159
|
+
function addAttr(attrType, valueTlv) {
|
|
160
|
+
if (seenAttr[attrType]) throw _err("csr/bad-input", "duplicate " + (oid.name(attrType) || attrType) + " attribute");
|
|
161
|
+
seenAttr[attrType] = true;
|
|
162
|
+
attrs.push(b.sequence([b.oid(attrType), b.set([valueTlv])])); // Attribute ::= SEQUENCE { type, SET OF value }
|
|
163
|
+
}
|
|
164
|
+
if (spec.extensionRequest != null) addAttr(O("extensionRequest"), _buildRequestedExtensions(spec.extensionRequest, subjectSpki));
|
|
165
|
+
if (spec.challengePassword != null) addAttr(O("challengePassword"), _challengePassword(spec.challengePassword));
|
|
166
|
+
if (spec.attributes != null) {
|
|
167
|
+
if (!Array.isArray(spec.attributes)) throw _err("csr/bad-input", "spec.attributes must be an array of pre-encoded Attribute DER");
|
|
168
|
+
spec.attributes.forEach(function (a, i) {
|
|
169
|
+
var der = _b.reqDer(a, "attribute [" + i + "]");
|
|
170
|
+
var n;
|
|
171
|
+
try { n = asn1.decode(der); }
|
|
172
|
+
catch (e) { throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] is not valid DER", e); }
|
|
173
|
+
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2 || n.children[1].tagNumber !== asn1.TAGS.SET) throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] must be an Attribute SEQUENCE { type OID, SET OF value }");
|
|
174
|
+
var at;
|
|
175
|
+
try { at = asn1.read.oid(n.children[0]); }
|
|
176
|
+
catch (e) { throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] type is not an OBJECT IDENTIFIER", e); }
|
|
177
|
+
// A type with a dedicated typed spec field must go through that field so its value syntax is always
|
|
178
|
+
// enforced (RFC 2985 sec. 5.4): the opaque escape hatch is for UNRECOGNIZED attribute types only and
|
|
179
|
+
// must never carry a recognized type with an unvalidated value the strict parser would then reject.
|
|
180
|
+
if (at === O("extensionRequest")) throw _err("csr/bad-input", "pass requested extensions via spec.extensionRequest, not a pre-encoded extensionRequest attribute");
|
|
181
|
+
if (at === O("challengePassword")) throw _err("csr/bad-input", "pass the challenge password via spec.challengePassword, not a pre-encoded challengePassword attribute");
|
|
182
|
+
// Attribute ::= SEQUENCE { type, values SET SIZE(1..MAX) }: an empty value SET is malformed and the
|
|
183
|
+
// strict parser rejects it, so reject at build time -- a signed request must always round-trip.
|
|
184
|
+
if (!n.children[1].children || n.children[1].children.length === 0) throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] value SET must contain at least one value (RFC 2986 SET SIZE(1..MAX))");
|
|
185
|
+
if (seenAttr[at]) throw _err("csr/bad-input", "duplicate " + (oid.name(at) || at) + " attribute");
|
|
186
|
+
seenAttr[at] = true;
|
|
187
|
+
attrs.push(b.raw(der));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Resolve the signature scheme from the SUBJECT public key (the request proves possession of its private half).
|
|
192
|
+
var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(subjectSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
193
|
+
|
|
194
|
+
// CertificationRequestInfo ::= SEQUENCE { version INTEGER(0), subject, subjectPKInfo, attributes [0] }.
|
|
195
|
+
var criDer = b.sequence([b.integer(0n), subjectDer, b.raw(subjectSpki), _implicitSetOf0(attrs)]);
|
|
196
|
+
|
|
197
|
+
return signScheme.signOverTbs(scheme, signingKey, criDer, _signE).then(function (sig) {
|
|
198
|
+
// Proof of possession: the signature MUST verify under the subject public key (what openssl req -verify checks).
|
|
199
|
+
return Promise.resolve(_b.assertSignatureVerifies(criDer, sig, subjectSpki, scheme)).then(function () {
|
|
200
|
+
var der = b.sequence([criDer, scheme.sigAlgId, b.bitString(sig, 0)]);
|
|
201
|
+
return opts.pem ? csr.pemEncode(der, "CERTIFICATE REQUEST") : der;
|
|
202
|
+
});
|
|
203
|
+
}, function (e) {
|
|
204
|
+
if (e instanceof CsrError) throw e;
|
|
205
|
+
throw _err("csr/bad-input", "signing the certification request failed -- the signing key does not match the subject public key or is invalid", e);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { sign: sign };
|
package/lib/pki-build.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the ENCODE-direction sibling of schema-pkix.js (which is decode-only). `makeBuilder(ctx)`
|
|
6
|
+
// binds a domain namespace once and returns the shared PKIX producing primitives every signer module
|
|
7
|
+
// composes: the distinguished-name encoder, the GeneralName + RFC 5280 sec. 4.2.1 extension-value
|
|
8
|
+
// encoders, the embedded-input validators (a raw Name / a SubjectPublicKeyInfo / a pre-encoded Extension
|
|
9
|
+
// run through the SAME parser the decoder uses), the sign-scheme bridge, and the post-sign signature
|
|
10
|
+
// self-check (the key-match / proof-of-possession verify). Each is parameterized on the CALLER's error
|
|
11
|
+
// class + code prefix, so pki.x509.sign keeps x509/* codes and pki.csr.sign keeps csr/*.
|
|
12
|
+
|
|
13
|
+
var asn1 = require("./asn1-der");
|
|
14
|
+
var schema = require("./schema-engine");
|
|
15
|
+
var compositeSig = require("./composite-sig");
|
|
16
|
+
var nodeCrypto = require("crypto");
|
|
17
|
+
|
|
18
|
+
var b = asn1.build;
|
|
19
|
+
|
|
20
|
+
// KeyUsage named-bit positions (RFC 5280 sec. 4.2.1.3); contentCommitment is the RFC 5280 rename of the
|
|
21
|
+
// X.509 nonRepudiation bit (1).
|
|
22
|
+
var KU_BIT = {
|
|
23
|
+
digitalSignature: 0, nonRepudiation: 1, contentCommitment: 1, keyEncipherment: 2,
|
|
24
|
+
dataEncipherment: 3, keyAgreement: 4, keyCertSign: 5, cRLSign: 6, encipherOnly: 7, decipherOnly: 8,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// makeBuilder(ctx) -> the bound producing primitives. ctx = { ErrorClass, prefix, O, NS, NAME_SCHEMA,
|
|
28
|
+
// SPKI_SCHEMA, EXT_DECODERS }. `E(kind, msg, cause)` builds a `<prefix>/<kind>` typed error.
|
|
29
|
+
function makeBuilder(ctx) {
|
|
30
|
+
var ErrorClass = ctx.ErrorClass, O = ctx.O, NS = ctx.NS;
|
|
31
|
+
var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
|
|
32
|
+
function E(kind, message, cause) { return new ErrorClass(ctx.prefix + "/" + kind, message, cause); }
|
|
33
|
+
function code(kind) { return ctx.prefix + "/" + kind; }
|
|
34
|
+
|
|
35
|
+
// ---- distinguished name encoding (RFC 5280 sec. 4.1.2.4) ----
|
|
36
|
+
// countryName is a PrintableString SIZE(2), emailAddress an IA5String; every other new-name attribute
|
|
37
|
+
// is a UTF8String (Teletex/BMP/Universal are backward-compat only and never emitted).
|
|
38
|
+
function atvString(attrName, value) {
|
|
39
|
+
if (attrName === "countryName") {
|
|
40
|
+
if (String(value).length !== 2) throw E("bad-name", "countryName must be a two-letter ISO 3166 code (PrintableString SIZE(2))");
|
|
41
|
+
return b.printable(value);
|
|
42
|
+
}
|
|
43
|
+
if (attrName === "emailAddress") return b.ia5(value);
|
|
44
|
+
return b.utf8(value);
|
|
45
|
+
}
|
|
46
|
+
function encodeAtv(attrName, value) {
|
|
47
|
+
if (value == null || value === "") throw E("bad-name", "the " + attrName + " attribute value must be a non-empty string");
|
|
48
|
+
// oid.byName returns undefined (does not throw) for an unrecognized name -- reject it explicitly.
|
|
49
|
+
var typeOid = O(attrName);
|
|
50
|
+
if (typeOid == null) throw E("bad-name", "unknown distinguished-name attribute " + JSON.stringify(attrName));
|
|
51
|
+
var valueTlv;
|
|
52
|
+
try { valueTlv = atvString(attrName, value); }
|
|
53
|
+
catch (e) { if (e instanceof ErrorClass) throw e; throw E("bad-name", "the " + attrName + " value has characters invalid for its string type", e); }
|
|
54
|
+
return b.sequence([b.oid(typeOid), valueTlv]);
|
|
55
|
+
}
|
|
56
|
+
function encodeRdn(rdnSpec) {
|
|
57
|
+
if (!rdnSpec || typeof rdnSpec !== "object" || Buffer.isBuffer(rdnSpec)) throw E("bad-name", "each RDN must be an object of { attributeName: value }");
|
|
58
|
+
var keys = Object.keys(rdnSpec);
|
|
59
|
+
if (!keys.length) throw E("bad-name", "an RDN must carry at least one attribute");
|
|
60
|
+
return b.set(keys.map(function (k) { return encodeAtv(k, rdnSpec[k]); }));
|
|
61
|
+
}
|
|
62
|
+
// A DN spec -> RDNSequence DER. A string is shorthand for a single commonName RDN; a Buffer is raw
|
|
63
|
+
// pre-encoded Name DER (validated through the parser). An empty array yields an empty RDNSequence.
|
|
64
|
+
function encodeName(spec) {
|
|
65
|
+
if (Buffer.isBuffer(spec)) { assertValidNameDer(spec); return spec; }
|
|
66
|
+
if (typeof spec === "string") spec = [{ commonName: spec }];
|
|
67
|
+
if (!Array.isArray(spec)) throw E("bad-name", "a name must be a string, an array of RDNs, or raw Name DER");
|
|
68
|
+
return b.sequence(spec.map(encodeRdn));
|
|
69
|
+
}
|
|
70
|
+
// Full validation of a raw Name DER: walk it through the exact RDNSequence parser the decoder uses.
|
|
71
|
+
function assertValidNameDer(der) {
|
|
72
|
+
var node;
|
|
73
|
+
try { node = asn1.decode(der); }
|
|
74
|
+
catch (e) { throw E("bad-name", "the raw Name DER is not valid DER", e); }
|
|
75
|
+
try { schema.walk(NAME_SCHEMA, node, NS); }
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (e instanceof ErrorClass || (e && e.name === "Asn1Error")) throw e;
|
|
78
|
+
throw E("bad-name", "the raw Name DER is not a well-formed distinguished name", e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isEmptyName(nameDer) { return asn1.decode(nameDer).children.length === 0; }
|
|
82
|
+
|
|
83
|
+
// ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) ----
|
|
84
|
+
function ia5Content(s) {
|
|
85
|
+
s = String(s);
|
|
86
|
+
for (var i = 0; i < s.length; i++) {
|
|
87
|
+
if (s.charCodeAt(i) > 0x7F) throw E("bad-input", "value requires 7-bit ASCII (IA5String): " + JSON.stringify(s));
|
|
88
|
+
}
|
|
89
|
+
return Buffer.from(s, "latin1");
|
|
90
|
+
}
|
|
91
|
+
function encodeGeneralName(entry) {
|
|
92
|
+
if (!entry || typeof entry !== "object" || Buffer.isBuffer(entry)) throw E("bad-input", "a GeneralName must be an object with exactly one name form");
|
|
93
|
+
var keys = Object.keys(entry);
|
|
94
|
+
if (keys.length !== 1) throw E("bad-input", "a GeneralName entry must have exactly one form, got " + keys.length);
|
|
95
|
+
var k = keys[0], v = entry[k];
|
|
96
|
+
if (v == null || v === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
|
|
97
|
+
switch (k) {
|
|
98
|
+
case "rfc822Name": return b.contextPrimitive(1, ia5Content(v));
|
|
99
|
+
case "dNSName": return b.contextPrimitive(2, ia5Content(v));
|
|
100
|
+
case "uniformResourceIdentifier": case "uri": return b.contextPrimitive(6, ia5Content(v));
|
|
101
|
+
case "iPAddress":
|
|
102
|
+
if (!Buffer.isBuffer(v) || (v.length !== 4 && v.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer");
|
|
103
|
+
return b.contextPrimitive(7, v);
|
|
104
|
+
case "directoryName": return b.explicit(4, encodeName(v)); // Name is a CHOICE -> the context tag is EXPLICIT
|
|
105
|
+
default: throw E("bad-input", "unsupported GeneralName form " + JSON.stringify(k) + " (supported: rfc822Name, dNSName, uniformResourceIdentifier, iPAddress, directoryName)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- extension-value encoders (the inverse of certExtensionDecoders) ----
|
|
110
|
+
function extKeyUsage(names) {
|
|
111
|
+
if (!Array.isArray(names) || !names.length) throw E("bad-input", "keyUsage must assert at least one bit (RFC 5280 sec. 4.2.1.3)");
|
|
112
|
+
var positions = names.map(function (n) {
|
|
113
|
+
var pos = KU_BIT[n];
|
|
114
|
+
if (pos == null) throw E("bad-input", "unknown keyUsage bit " + JSON.stringify(n));
|
|
115
|
+
return pos;
|
|
116
|
+
});
|
|
117
|
+
return b.namedBitString(positions);
|
|
118
|
+
}
|
|
119
|
+
function extExtKeyUsage(names) {
|
|
120
|
+
if (!Array.isArray(names) || !names.length) throw E("bad-input", "extendedKeyUsage must list at least one KeyPurposeId");
|
|
121
|
+
return b.sequence(names.map(function (n) {
|
|
122
|
+
var purposeOid = O(n);
|
|
123
|
+
if (purposeOid == null) throw E("bad-input", "unknown extendedKeyUsage purpose " + JSON.stringify(n));
|
|
124
|
+
return b.oid(purposeOid);
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
function validateBcSpec(bc) {
|
|
128
|
+
if (bc.cA != null && typeof bc.cA !== "boolean") throw E("bad-input", "basicConstraints cA must be a boolean");
|
|
129
|
+
if (bc.critical != null && typeof bc.critical !== "boolean") throw E("bad-input", "basicConstraints critical must be a boolean");
|
|
130
|
+
if (bc.pathLen != null) pathLen(bc.pathLen);
|
|
131
|
+
Object.keys(bc).forEach(function (k) {
|
|
132
|
+
if (k !== "cA" && k !== "pathLen" && k !== "critical") throw E("bad-input", "unknown basicConstraints field " + JSON.stringify(k));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function extBasicConstraints(spec) {
|
|
136
|
+
var children = [];
|
|
137
|
+
if (spec.cA === true) children.push(b.boolean(true)); // cA=FALSE omitted (DER DEFAULT)
|
|
138
|
+
if (spec.pathLen != null) children.push(b.integer(pathLen(spec.pathLen)));
|
|
139
|
+
return b.sequence(children);
|
|
140
|
+
}
|
|
141
|
+
function pathLen(v) {
|
|
142
|
+
if (typeof v !== "number" || !isFinite(v) || v < 0 || (v | 0) !== v) throw E("bad-input", "basicConstraints pathLenConstraint must be a non-negative integer");
|
|
143
|
+
return BigInt(v);
|
|
144
|
+
}
|
|
145
|
+
function extSki(keyid) { return b.octetString(keyid); }
|
|
146
|
+
function extAki(keyid) { return b.sequence([b.contextPrimitive(0, keyid)]); } // keyIdentifier [0] IMPLICIT OCTET STRING
|
|
147
|
+
// GeneralNames ::= SEQUENCE SIZE(1..MAX) OF GeneralName. With no implicitTag it is a universal SEQUENCE
|
|
148
|
+
// (subjectAltName / issuerAltName / IssuerSerial.issuer). With an implicitTag it is a context-constructed
|
|
149
|
+
// [n] whose tag REPLACES the SEQUENCE tag (RFC 5755 IMPLICIT TAGS -- Holder.entityName [1],
|
|
150
|
+
// RoleSyntax.roleAuthority [0], IetfAttrSyntax.policyAuthority [0]); the [n] node's children ARE the
|
|
151
|
+
// GeneralName members, no inner SEQUENCE wrapper.
|
|
152
|
+
function encodeGeneralNames(entries, implicitTag) {
|
|
153
|
+
if (!Array.isArray(entries) || !entries.length) throw E("bad-input", "a GeneralNames must carry at least one GeneralName");
|
|
154
|
+
var members = entries.map(encodeGeneralName);
|
|
155
|
+
if (implicitTag == null) return b.sequence(members);
|
|
156
|
+
return b.contextConstructed(implicitTag, Buffer.concat(members));
|
|
157
|
+
}
|
|
158
|
+
function extSan(entries) { return encodeGeneralNames(entries); }
|
|
159
|
+
function extCertPolicies(names) {
|
|
160
|
+
if (!Array.isArray(names) || !names.length) throw E("bad-input", "certificatePolicies must list at least one policy OID");
|
|
161
|
+
var seen = {};
|
|
162
|
+
return b.sequence(names.map(function (n) {
|
|
163
|
+
var pOid = O(n);
|
|
164
|
+
if (pOid == null) throw E("bad-input", "unknown certificate policy " + JSON.stringify(n));
|
|
165
|
+
if (seen[pOid]) throw E("bad-input", "duplicate certificate policy " + JSON.stringify(n) + " (RFC 5280 sec. 4.2.1.4)");
|
|
166
|
+
seen[pOid] = true;
|
|
167
|
+
return b.sequence([b.oid(pOid)]);
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
// Wrap a value in Extension ::= SEQUENCE { extnID, critical?, extnValue }; a FALSE critical is omitted.
|
|
171
|
+
function ext(oidStr, critical, valueDer) {
|
|
172
|
+
var children = [b.oid(oidStr)];
|
|
173
|
+
if (critical) children.push(b.boolean(true));
|
|
174
|
+
children.push(b.octetString(valueDer));
|
|
175
|
+
return b.sequence(children);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The SHA-1 subjectKeyIdentifier (RFC 5280 sec. 4.2.1.2 method 1): SHA-1 of the subjectPublicKey BIT
|
|
179
|
+
// STRING CONTENT (past the unused-bits octet), NOT the whole SPKI or the BIT STRING TLV.
|
|
180
|
+
function spkiKeyId(spkiDer) {
|
|
181
|
+
var keyBytes = asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
|
|
182
|
+
// RFC 5280 sec. 4.2.1.2 method 1 DEFINES the subjectKeyIdentifier as the SHA-1 of the
|
|
183
|
+
// subjectPublicKey; this is a key identifier, not a signature or a collision-resistance use,
|
|
184
|
+
// and the algorithm is fixed by the standard.
|
|
185
|
+
// nosemgrep: pki-weak-hash-md5-sha1
|
|
186
|
+
return nodeCrypto.createHash("sha1").update(keyBytes).digest();
|
|
187
|
+
}
|
|
188
|
+
function skiKeyId(val, spkiDer) {
|
|
189
|
+
if (Buffer.isBuffer(val)) return val;
|
|
190
|
+
if (val === true) return spkiKeyId(spkiDer);
|
|
191
|
+
throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- embedded-input validators ----
|
|
195
|
+
function reqDer(v, what) {
|
|
196
|
+
if (Buffer.isBuffer(v)) return v;
|
|
197
|
+
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
198
|
+
throw E("bad-input", what + " must be a DER Buffer");
|
|
199
|
+
}
|
|
200
|
+
// Full validation of an embedded SubjectPublicKeyInfo via the SAME parser the decoder uses.
|
|
201
|
+
function assertValidSpki(spkiDer, what) {
|
|
202
|
+
var node;
|
|
203
|
+
try { node = asn1.decode(spkiDer); }
|
|
204
|
+
catch (e) { throw E("bad-input", what + " is not valid DER", e); }
|
|
205
|
+
try { schema.walk(SPKI_SCHEMA, node, NS); }
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (e instanceof ErrorClass || (e && e.name === "Asn1Error")) throw e;
|
|
208
|
+
throw E("bad-input", what + " is not a well-formed SubjectPublicKeyInfo", e);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// A pre-encoded Extension ::= SEQUENCE { extnID OID, critical BOOLEAN OPTIONAL, extnValue OCTET STRING };
|
|
212
|
+
// an explicit critical=FALSE is non-canonical (DER DEFAULT) and rejected.
|
|
213
|
+
function assertValidExtension(der, idx) {
|
|
214
|
+
var n;
|
|
215
|
+
try { n = asn1.decode(der); }
|
|
216
|
+
catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] is not valid DER", e); }
|
|
217
|
+
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length < 2 || n.children.length > 3) throw E("bad-input", "pre-encoded extension [" + idx + "] must be an Extension SEQUENCE { extnID, critical?, extnValue }");
|
|
218
|
+
try { asn1.read.oid(n.children[0]); }
|
|
219
|
+
catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] extnID is not an OBJECT IDENTIFIER", e); }
|
|
220
|
+
if (n.children.length === 3) {
|
|
221
|
+
var crit;
|
|
222
|
+
try { crit = asn1.read.boolean(n.children[1]); }
|
|
223
|
+
catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] critical must be a BOOLEAN", e); }
|
|
224
|
+
if (crit !== true) throw E("bad-input", "pre-encoded extension [" + idx + "] critical=FALSE must be omitted (DER DEFAULT)");
|
|
225
|
+
}
|
|
226
|
+
var last = n.children[n.children.length - 1];
|
|
227
|
+
if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
|
|
228
|
+
}
|
|
229
|
+
// A positive, non-zero INTEGER of at most 20 content octets (RFC 5280 sec. 4.1.2.2 / RFC 5755 sec.
|
|
230
|
+
// 4.2.5 -- the identical serial rule for a certificate and an attribute certificate). A random 20-octet
|
|
231
|
+
// positive serial is generated when none is supplied. Accepts a BigInt, a safe integer, a decimal /
|
|
232
|
+
// 0x-hex string, or a raw magnitude Buffer.
|
|
233
|
+
function serialInteger(serial) {
|
|
234
|
+
var v;
|
|
235
|
+
if (serial == null) {
|
|
236
|
+
var rnd = nodeCrypto.randomBytes(20);
|
|
237
|
+
rnd[0] &= 0x7f; // keep the top bit clear so the magnitude stays <= 20 octets and positive
|
|
238
|
+
if (rnd[0] === 0) rnd[0] = 0x01; // never all-zero leading -> non-zero and no redundant sign octet
|
|
239
|
+
v = BigInt("0x" + rnd.toString("hex"));
|
|
240
|
+
} else if (typeof serial === "bigint") { v = serial; }
|
|
241
|
+
else if (typeof serial === "number") { if (!Number.isSafeInteger(serial)) throw E("bad-serial", "serialNumber number must be a safe integer (pass a BigInt, hex string, or Buffer for a value above 2^53-1)"); v = BigInt(serial); }
|
|
242
|
+
else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw E("bad-serial", "serialNumber string must be a decimal or 0x-hex integer", e); } }
|
|
243
|
+
else if (Buffer.isBuffer(serial)) { v = serial.length ? BigInt("0x" + serial.toString("hex")) : 0n; }
|
|
244
|
+
else { throw E("bad-serial", "serialNumber must be a BigInt, integer, hex string, or Buffer"); }
|
|
245
|
+
if (v <= 0n) throw E("bad-serial", "serialNumber must be a positive integer (RFC 5280 sec. 4.1.2.2)");
|
|
246
|
+
var tlv = b.integer(v);
|
|
247
|
+
if (asn1.decode(tlv).content.length > 20) throw E("bad-serial", "serialNumber must not exceed 20 octets (RFC 5280 sec. 4.1.2.2)");
|
|
248
|
+
return tlv;
|
|
249
|
+
}
|
|
250
|
+
// Synthesize the parsed-cert shape resolveSignScheme reads (subjectPublicKeyInfo.algorithm) from a raw SPKI.
|
|
251
|
+
function certLikeFromSpki(spkiDer) {
|
|
252
|
+
var spki = asn1.decode(spkiDer);
|
|
253
|
+
if (!spki.children || !spki.children.length) throw E("bad-input", "the signing key SPKI is not a SubjectPublicKeyInfo");
|
|
254
|
+
var alg = spki.children[0];
|
|
255
|
+
var keyOid;
|
|
256
|
+
try { keyOid = asn1.read.oid(alg.children[0]); }
|
|
257
|
+
catch (e) { throw E("bad-input", "the signing key SPKI algorithm is not an OID", e); }
|
|
258
|
+
return { subjectPublicKeyInfo: { algorithm: { oid: keyOid, parameters: alg.children.length > 1 ? alg.children[1].bytes : undefined } } };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Confirm the produced signature verifies under `spki` -- key-type-agnostic (a PKCS#8 key, a WebCrypto
|
|
262
|
+
// CryptoKey, or any signer), where deriving-and-comparing the public key cannot (a non-extractable
|
|
263
|
+
// CryptoKey has no exportable public half). This is the x509 chain self-check AND the CSR proof of
|
|
264
|
+
// possession. Composite verifies both components. Returns a promise for composite, sync-throws for classical.
|
|
265
|
+
function assertSignatureVerifies(preimage, sig, spki, scheme) {
|
|
266
|
+
if (scheme.composite) {
|
|
267
|
+
return compositeSig.compositeVerify(spki, sig, preimage, scheme.composite, ErrorClass, code("unsupported-algorithm"), code("bad-input")).then(function (r) {
|
|
268
|
+
if (!r.ok) throw E("bad-input", "the composite signing key does not correspond to the public key -- the signature would not verify");
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
var pub;
|
|
272
|
+
try { pub = nodeCrypto.createPublicKey({ key: spki, format: "der", type: "spki" }); }
|
|
273
|
+
catch (e) { throw E("bad-input", "the public key could not be imported for the signature self-check", e); }
|
|
274
|
+
var s = scheme.sign, ok;
|
|
275
|
+
try {
|
|
276
|
+
if (s.name === "ECDSA") ok = nodeCrypto.verify(scheme.digest, preimage, { key: pub, dsaEncoding: "der" }, sig);
|
|
277
|
+
else if (s.name === "RSA-PSS") ok = nodeCrypto.verify(scheme.digest, preimage, { key: pub, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: s.saltLength }, sig);
|
|
278
|
+
else if (s.name === "RSASSA-PKCS1-v1_5") ok = nodeCrypto.verify(scheme.digest, preimage, pub, sig);
|
|
279
|
+
// allow:eddsa-verify-without-loworder-gate -- a self-check that OUR just-produced signature verifies
|
|
280
|
+
// under the key the caller controls, not a security verify of an untrusted EdDSA signature, so the
|
|
281
|
+
// low-order-point gate (a forged-signature defense) does not apply.
|
|
282
|
+
else ok = nodeCrypto.verify(null, preimage, pub, sig); // Ed25519 / Ed448 / ML-DSA / SLH-DSA
|
|
283
|
+
} catch (e) { throw E("bad-input", "the signature self-check could not run against the public key", e); }
|
|
284
|
+
if (!ok) throw E("bad-input", "the signing key does not correspond to the public key -- the signature would not verify");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
E: E, code: code, KU_BIT: KU_BIT,
|
|
289
|
+
encodeName: encodeName, isEmptyName: isEmptyName, encodeGeneralName: encodeGeneralName,
|
|
290
|
+
encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger,
|
|
291
|
+
extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
|
|
292
|
+
extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki,
|
|
293
|
+
extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
|
|
294
|
+
spkiKeyId: spkiKeyId, skiKeyId: skiKeyId,
|
|
295
|
+
reqDer: reqDer, assertValidSpki: assertValidSpki, assertValidExtension: assertValidExtension,
|
|
296
|
+
certLikeFromSpki: certLikeFromSpki, assertSignatureVerifies: assertSignatureVerifies,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The raw issuer / subject Name TLV of a parsed X.509 certificate (byte-identical). tbs field layout:
|
|
301
|
+
// [version?] serial(0) signature(1) issuer(2) validity(3) subject(4). Used where a producer must chain a
|
|
302
|
+
// name to an existing certificate exactly -- an issued certificate's issuer, a CMS SignerInfo issuer, an
|
|
303
|
+
// attribute certificate's issuerName / holder baseCertificateID.
|
|
304
|
+
function tbsNameField(cert, which) {
|
|
305
|
+
var tbs = asn1.decode(cert.tbsBytes);
|
|
306
|
+
var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
|
|
307
|
+
return tbs.children[(hasVersion ? 1 : 0) + (which === "subject" ? 4 : 2)].bytes;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT, tbsNameField: tbsNameField };
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -666,6 +666,21 @@ _AC_EXT_DECODERS[_Oav("noRevAvail")] = _noRevAvail;
|
|
|
666
666
|
_AC_EXT_DECODERS[_Oav("aaControls")] = _aaControls;
|
|
667
667
|
_AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
|
|
668
668
|
|
|
669
|
+
// @internal -- validate a pre-encoded AC attribute value / AC extension value against the SAME decoder
|
|
670
|
+
// the parser runs, so the attrcert-sign escape hatches (a caller's pre-encoded Attribute / Extension DER)
|
|
671
|
+
// cannot embed a structurally-malformed recognized value the strict parser would then reject. `type` /
|
|
672
|
+
// `extnOid` are dotted-OID strings; `valueDer` / `extnValueDer` are the raw AttributeValue / extnValue
|
|
673
|
+
// content Buffers (exactly what the parse loop passes). An unrecognized type returns null (stays opaque,
|
|
674
|
+
// no validation); a recognized-but-malformed value throws the frozen attrcert/bad-* code.
|
|
675
|
+
function validateAttributeValue(type, valueDer) {
|
|
676
|
+
var dec = _ATTR_VALUE_DECODERS[type];
|
|
677
|
+
return dec ? dec(valueDer) : null;
|
|
678
|
+
}
|
|
679
|
+
function validateExtensionValue(extnOid, extnValueDer) {
|
|
680
|
+
var dec = _AC_EXT_DECODERS[extnOid];
|
|
681
|
+
return dec ? dec(extnValueDer) : null;
|
|
682
|
+
}
|
|
683
|
+
|
|
669
684
|
module.exports = {
|
|
670
685
|
parse: parse,
|
|
671
686
|
parseV1: parseV1,
|
|
@@ -673,4 +688,6 @@ module.exports = {
|
|
|
673
688
|
pemEncode: pemEncode,
|
|
674
689
|
matches: matches,
|
|
675
690
|
matchesV1: matchesV1,
|
|
691
|
+
validateAttributeValue: validateAttributeValue,
|
|
692
|
+
validateExtensionValue: validateExtensionValue,
|
|
676
693
|
};
|