@blamejs/pki 0.1.12 → 0.1.14
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 +20 -0
- package/README.md +32 -22
- package/lib/asn1-der.js +41 -4
- package/lib/framework-error.js +12 -0
- package/lib/oid.js +36 -5
- package/lib/schema-all.js +43 -3
- package/lib/schema-attrcert.js +383 -0
- package/lib/schema-engine.js +16 -2
- package/lib/schema-ocsp.js +4 -53
- package/lib/schema-pkix.js +78 -0
- package/lib/schema-tsp.js +411 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.attrcert
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title Attribute Cert
|
|
8
|
+
* @order 175
|
|
9
|
+
* @slug attrcert
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* X.509 Attribute Certificate handling per RFC 5755. An attribute certificate
|
|
13
|
+
* binds a holder to a set of privilege attributes (role, group, clearance)
|
|
14
|
+
* without carrying a public key — the authorization counterpart to an identity
|
|
15
|
+
* certificate. `parse` decodes a v2 `AttributeCertificate` into its holder,
|
|
16
|
+
* issuer, validity window (real `Date`s), attributes, and extensions, reusing
|
|
17
|
+
* the shared signed-envelope so the raw `tbsBytes` (the exact signed region) and
|
|
18
|
+
* the `signatureValue` are surfaced for a downstream verifier.
|
|
19
|
+
*
|
|
20
|
+
* The holder and issuer identities are `GeneralName`s, validated on the way in
|
|
21
|
+
* (each alternative's form and content per RFC 5280 §4.2.1.6) and surfaced with
|
|
22
|
+
* their raw bytes; the profile MUSTs above structural scope (a single
|
|
23
|
+
* non-empty `directoryName` issuer, the holder-to-PKC binding, targeting and
|
|
24
|
+
* revocation) are verification-layer concerns. The obsolete X.509-1997
|
|
25
|
+
* `AttributeCertificateV1` is recognized and deferred, not parsed. DER-only,
|
|
26
|
+
* fail-closed.
|
|
27
|
+
*
|
|
28
|
+
* @card
|
|
29
|
+
* Parse DER / PEM RFC 5755 attribute certificates into holder, issuer,
|
|
30
|
+
* validity, attributes, and extensions — validated GeneralNames, raw verifier
|
|
31
|
+
* inputs, legacy v1 recognize-and-defer, fail-closed.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
var asn1 = require("./asn1-der");
|
|
35
|
+
var schema = require("./schema-engine");
|
|
36
|
+
var pkix = require("./schema-pkix");
|
|
37
|
+
var oid = require("./oid");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var AttrCertError = frameworkError.AttrCertError;
|
|
41
|
+
var PemError = frameworkError.PemError;
|
|
42
|
+
|
|
43
|
+
var NS = pkix.makeNS("attrcert", AttrCertError, oid);
|
|
44
|
+
|
|
45
|
+
var TAGS = asn1.TAGS;
|
|
46
|
+
|
|
47
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
48
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
49
|
+
|
|
50
|
+
// AttCertVersion ::= INTEGER { v2(1) } — a bare, mandatory INTEGER (not the x509
|
|
51
|
+
// [0] EXPLICIT DEFAULT shape). The only legal value is v2, surfaced as 2.
|
|
52
|
+
var VERSION = pkix.versionReader(NS, { "1": 2 });
|
|
53
|
+
|
|
54
|
+
// ---- shared leaves ---------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// AttCertValidityPeriod times are GeneralizedTime (RFC 5755 §4.2.6) — a UTCTime is
|
|
57
|
+
// rejected. Non-fractional YYYYMMDDHHMMSSZ is enforced by the codec.
|
|
58
|
+
var GENERALIZED_TIME = schema.decode(function (n, ctx) {
|
|
59
|
+
if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
|
|
60
|
+
throw ctx.E("attrcert/bad-time", "AttCertValidityPeriod times must be GeneralizedTime (RFC 5755 §4.2.6)");
|
|
61
|
+
}
|
|
62
|
+
return asn1.read.time(n);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// digestedObjectType ::= ENUMERATED { publicKey(0), publicKeyCert(1),
|
|
66
|
+
// otherObjectTypes(2) } (RFC 5755 §4.1) — an undefined value is rejected; a
|
|
67
|
+
// non-ENUMERATED tag fails at the codec (asn1/*).
|
|
68
|
+
var ODT_NAMES = { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" };
|
|
69
|
+
var DIGESTED_OBJECT_TYPE = schema.decode(function (n, ctx) {
|
|
70
|
+
var v = asn1.read.enumerated(n);
|
|
71
|
+
var k = v.toString();
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(ODT_NAMES, k)) {
|
|
73
|
+
throw ctx.E("attrcert/bad-digested-object-type", "digestedObjectType " + k + " is not a defined value (RFC 5755 §4.1)");
|
|
74
|
+
}
|
|
75
|
+
// RFC 5755 §7.3 — a conformant AC MUST NOT use otherObjectTypes(2); the digested
|
|
76
|
+
// object is limited to a public key or a public-key certificate, so reject it
|
|
77
|
+
// fail-closed rather than parse a digest whose object type is unidentifiable.
|
|
78
|
+
if (k === "2") {
|
|
79
|
+
throw ctx.E("attrcert/bad-digested-object-type", "otherObjectTypes(2) MUST NOT be used (RFC 5755 §7.3)");
|
|
80
|
+
}
|
|
81
|
+
return { code: Number(v), name: ODT_NAMES[k] };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---- IssuerSerial / ObjectDigestInfo ---------------------------------
|
|
85
|
+
|
|
86
|
+
// IssuerSerial ::= SEQUENCE { issuer GeneralNames, serial CertificateSerialNumber,
|
|
87
|
+
// issuerUID UniqueIdentifier OPTIONAL } (RFC 5755 §4.1). Reached as an IMPLICIT
|
|
88
|
+
// [n] node (Holder [0] / V2Form [0]) whose tag replaces the SEQUENCE tag, so the
|
|
89
|
+
// shape assertion is "constructed" (the tag is pinned by the enclosing trailing);
|
|
90
|
+
// a context [n] PRIMITIVE node has no children and fails closed here.
|
|
91
|
+
var ISSUER_SERIAL = schema.seq([
|
|
92
|
+
schema.field("issuer", pkix.generalNames(NS, { code: "attrcert/bad-issuer-serial" })),
|
|
93
|
+
schema.field("serial", schema.integerLeaf()),
|
|
94
|
+
schema.optional("issuerUID", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
|
|
95
|
+
], {
|
|
96
|
+
assert: "constructed", code: "attrcert/bad-issuer-serial", what: "IssuerSerial",
|
|
97
|
+
build: function (m) {
|
|
98
|
+
return {
|
|
99
|
+
issuer: m.fields.issuer.value.result,
|
|
100
|
+
serial: m.fields.serial.value,
|
|
101
|
+
serialHex: m.fields.serial.node.content.toString("hex"),
|
|
102
|
+
issuerUID: m.fields.issuerUID.present ? m.fields.issuerUID.value : null,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ObjectDigestInfo ::= SEQUENCE { digestedObjectType ENUMERATED, otherObjectTypeID
|
|
108
|
+
// OBJECT IDENTIFIER OPTIONAL, digestAlgorithm AlgorithmIdentifier, objectDigest BIT
|
|
109
|
+
// STRING } (RFC 5755 §4.1). otherObjectTypeID is present only for otherObjectTypes(2).
|
|
110
|
+
var OBJECT_DIGEST_INFO = schema.seq([
|
|
111
|
+
schema.field("digestedObjectType", DIGESTED_OBJECT_TYPE),
|
|
112
|
+
schema.optional("otherObjectTypeID", schema.oidLeaf(), { whenUniversal: [TAGS.OBJECT_IDENTIFIER] }),
|
|
113
|
+
schema.field("digestAlgorithm", ALGORITHM_IDENTIFIER),
|
|
114
|
+
schema.field("objectDigest", schema.bitString()),
|
|
115
|
+
], {
|
|
116
|
+
assert: "constructed", code: "attrcert/bad-object-digest-info", what: "ObjectDigestInfo",
|
|
117
|
+
build: function (m, ctx) {
|
|
118
|
+
var t = m.fields.digestedObjectType.value;
|
|
119
|
+
// otherObjectTypeID identifies a non-certificate object type, meaningful only with
|
|
120
|
+
// otherObjectTypes(2) — which RFC 5755 §7.3 forbids (rejected at the leaf above) —
|
|
121
|
+
// so a present otherObjectTypeID is never valid here and is rejected fail-closed.
|
|
122
|
+
if (m.fields.otherObjectTypeID.present) {
|
|
123
|
+
throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 §7.3 forbids");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
digestedObjectType: t,
|
|
127
|
+
otherObjectTypeID: null,
|
|
128
|
+
digestAlgorithm: m.fields.digestAlgorithm.value.result,
|
|
129
|
+
objectDigest: m.fields.objectDigest.value,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---- Holder / AttCertIssuer / V2Form ---------------------------------
|
|
135
|
+
|
|
136
|
+
// Holder ::= SEQUENCE { baseCertificateID [0] IMPLICIT IssuerSerial OPTIONAL,
|
|
137
|
+
// entityName [1] IMPLICIT GeneralNames OPTIONAL, objectDigestInfo [2] IMPLICIT
|
|
138
|
+
// ObjectDigestInfo OPTIONAL } (RFC 5755 §4.1). All three are IMPLICIT + OPTIONAL;
|
|
139
|
+
// the profile RECOMMENDS exactly one, but the parser surfaces all three (null when
|
|
140
|
+
// absent) and does not enforce the cardinality.
|
|
141
|
+
var HOLDER = schema.seq([
|
|
142
|
+
schema.trailing([
|
|
143
|
+
{ tag: 0, name: "baseCertificateID", schema: ISSUER_SERIAL },
|
|
144
|
+
{ tag: 1, name: "entityName", schema: pkix.generalNames(NS, { implicitTag: 1, code: "attrcert/bad-entity-name" }) },
|
|
145
|
+
{ tag: 2, name: "objectDigestInfo", schema: OBJECT_DIGEST_INFO },
|
|
146
|
+
], { minTag: 0, maxTag: 2, unexpectedCode: "attrcert/bad-holder", orderCode: "attrcert/bad-holder" }),
|
|
147
|
+
], {
|
|
148
|
+
assert: "sequence", code: "attrcert/bad-holder", what: "Holder",
|
|
149
|
+
build: function (m) {
|
|
150
|
+
return {
|
|
151
|
+
baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
|
|
152
|
+
entityName: m.fields.entityName.present ? m.fields.entityName.value.result : null,
|
|
153
|
+
objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// V2Form ::= SEQUENCE { issuerName GeneralNames OPTIONAL, baseCertificateID [0]
|
|
159
|
+
// IMPLICIT IssuerSerial OPTIONAL, objectDigestInfo [1] IMPLICIT ObjectDigestInfo
|
|
160
|
+
// OPTIONAL } (RFC 5755 §4.1). Reached via the AttCertIssuer CHOICE on a context [0]
|
|
161
|
+
// node, so the shape is "constructed".
|
|
162
|
+
var V2_FORM = schema.seq([
|
|
163
|
+
schema.optional("issuerName", pkix.generalNames(NS, { code: "attrcert/bad-issuer-name" }), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
164
|
+
schema.trailing([
|
|
165
|
+
{ tag: 0, name: "baseCertificateID", schema: ISSUER_SERIAL },
|
|
166
|
+
{ tag: 1, name: "objectDigestInfo", schema: OBJECT_DIGEST_INFO },
|
|
167
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "attrcert/bad-v2form", orderCode: "attrcert/bad-v2form" }),
|
|
168
|
+
], {
|
|
169
|
+
assert: "constructed", code: "attrcert/bad-v2form", what: "V2Form",
|
|
170
|
+
build: function (m) {
|
|
171
|
+
return {
|
|
172
|
+
issuerName: m.fields.issuerName.present ? m.fields.issuerName.value.result : null,
|
|
173
|
+
baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
|
|
174
|
+
objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// AttCertIssuer ::= CHOICE { v1Form GeneralNames, v2Form [0] IMPLICIT V2Form } (RFC
|
|
180
|
+
// 5755 §4.1). A universal SEQUENCE is the (obsolete but structurally legal) v1Form;
|
|
181
|
+
// a context [0] is v2Form. A conforming AC uses v2Form; the parser surfaces which.
|
|
182
|
+
var ATT_CERT_ISSUER = schema.choice([
|
|
183
|
+
{ when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: pkix.generalNames(NS, { code: "attrcert/bad-issuer" }) },
|
|
184
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: V2_FORM },
|
|
185
|
+
], { code: "attrcert/bad-issuer" });
|
|
186
|
+
|
|
187
|
+
// ---- validity + attributes -------------------------------------------
|
|
188
|
+
|
|
189
|
+
// AttCertValidityPeriod ::= SEQUENCE { notBeforeTime GeneralizedTime, notAfterTime
|
|
190
|
+
// GeneralizedTime } (RFC 5755 §4.2.6).
|
|
191
|
+
var VALIDITY = schema.seq([
|
|
192
|
+
schema.field("notBeforeTime", GENERALIZED_TIME),
|
|
193
|
+
schema.field("notAfterTime", GENERALIZED_TIME),
|
|
194
|
+
], {
|
|
195
|
+
assert: "sequence", arity: { exact: 2 }, code: "attrcert/bad-validity", what: "AttCertValidityPeriod",
|
|
196
|
+
build: function (m) { return { notBeforeTime: m.fields.notBeforeTime.value, notAfterTime: m.fields.notAfterTime.value }; },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// attributes ::= SEQUENCE OF Attribute (RFC 5755 §4.2.7) — MUST be non-empty and
|
|
200
|
+
// each AttributeType OID unique. Order-preserving (SEQUENCE OF, not SET OF).
|
|
201
|
+
var ATTRIBUTES = schema.seqOf(pkix.attribute(NS), {
|
|
202
|
+
assert: "sequence", min: 1, code: "attrcert/bad-attributes", what: "attributes",
|
|
203
|
+
unique: function (it) { return it.value.result.type; }, dupCode: "attrcert/duplicate-attribute",
|
|
204
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---- AttributeCertificateInfo / AttributeCertificate -----------------
|
|
208
|
+
|
|
209
|
+
// AttributeCertificateInfo ::= SEQUENCE { version, holder, issuer, signature
|
|
210
|
+
// AlgorithmIdentifier, serialNumber, attrCertValidityPeriod, attributes,
|
|
211
|
+
// issuerUniqueID OPTIONAL, extensions OPTIONAL } (RFC 5755 §4.1). The tbs body the
|
|
212
|
+
// signed envelope wraps; the cross-field invariants live in the envelope build.
|
|
213
|
+
var ACINFO = schema.seq([
|
|
214
|
+
schema.field("version", VERSION),
|
|
215
|
+
schema.field("holder", HOLDER),
|
|
216
|
+
schema.field("issuer", ATT_CERT_ISSUER),
|
|
217
|
+
schema.field("signature", ALGORITHM_IDENTIFIER),
|
|
218
|
+
schema.field("serialNumber", schema.integerLeaf()),
|
|
219
|
+
schema.field("attrCertValidityPeriod", VALIDITY),
|
|
220
|
+
schema.field("attributes", ATTRIBUTES),
|
|
221
|
+
schema.optional("issuerUniqueID", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
|
|
222
|
+
schema.optional("extensions", EXTENSIONS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
223
|
+
], { assert: "sequence", code: "attrcert/bad-acinfo", what: "AttributeCertificateInfo" });
|
|
224
|
+
|
|
225
|
+
// AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue
|
|
226
|
+
// BIT STRING } — the shared SIGNED envelope (RFC 5755 §4.1). The AC-specific
|
|
227
|
+
// invariants (§4.2.4 sig-alg agreement, §4.2.5 positive-and-<=20-octet serialNumber)
|
|
228
|
+
// run in the build; the envelope owns the SEQUENCE-of-3 shape + signature extraction.
|
|
229
|
+
var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
230
|
+
code: "attrcert/not-an-attribute-certificate", what: "AttributeCertificate",
|
|
231
|
+
build: function (e, ctx) {
|
|
232
|
+
var acinfo = e.tbsMatch;
|
|
233
|
+
|
|
234
|
+
// RFC 5755 §4.2.4 — the outer signatureAlgorithm MUST equal acinfo.signature.
|
|
235
|
+
if (!e.outerSignatureAlgorithmBytes.equals(acinfo.fields.signature.node.bytes)) {
|
|
236
|
+
throw ctx.E("attrcert/bad-signature-algorithm", "signatureAlgorithm must match AttributeCertificateInfo.signature (RFC 5755 §4.2.4)");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// RFC 5755 §4.2.5 — serialNumber MUST be a positive INTEGER of at most 20 content
|
|
240
|
+
// octets. Positive excludes both a negative (DER sign bit set) and zero (a minimal
|
|
241
|
+
// INTEGER 0 is a single 0x00 octet — the codec already rejected any non-minimal
|
|
242
|
+
// zero). Surface it lossless (BigInt) + as hex.
|
|
243
|
+
var serialNode = acinfo.fields.serialNumber.node;
|
|
244
|
+
var sc = serialNode.content;
|
|
245
|
+
if (sc.length === 0 || (sc[0] & 0x80) !== 0 || (sc.length === 1 && sc[0] === 0)) {
|
|
246
|
+
throw ctx.E("attrcert/bad-serial-number", "serialNumber must be a positive INTEGER (RFC 5755 §4.2.5)");
|
|
247
|
+
}
|
|
248
|
+
if (sc.length > 20) {
|
|
249
|
+
throw ctx.E("attrcert/bad-serial-number", "serialNumber must not exceed 20 content octets (RFC 5755 §4.2.5)");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// AttCertIssuer is a CHOICE — the chosen arm is read off the issuer node's tag
|
|
253
|
+
// (a universal SEQUENCE is v1Form; a context [0] is v2Form).
|
|
254
|
+
var issuerNode = acinfo.fields.issuer.node;
|
|
255
|
+
var issuerVal = acinfo.fields.issuer.value;
|
|
256
|
+
var issuer = issuerNode.tagClass === "context"
|
|
257
|
+
? { form: "v2Form", v2Form: issuerVal.result, v1Form: null }
|
|
258
|
+
: { form: "v1Form", v1Form: issuerVal.result, v2Form: null };
|
|
259
|
+
|
|
260
|
+
var extField = acinfo.fields.extensions;
|
|
261
|
+
return {
|
|
262
|
+
version: acinfo.fields.version.value, // 2
|
|
263
|
+
holder: acinfo.fields.holder.value.result,
|
|
264
|
+
issuer: issuer,
|
|
265
|
+
signatureAlgorithm: e.signatureAlgorithm,
|
|
266
|
+
tbsSignatureAlgorithm: acinfo.fields.signature.value.result,
|
|
267
|
+
serialNumber: acinfo.fields.serialNumber.value,
|
|
268
|
+
serialNumberHex: sc.toString("hex"),
|
|
269
|
+
validity: acinfo.fields.attrCertValidityPeriod.value.result,
|
|
270
|
+
attributes: acinfo.fields.attributes.value.result,
|
|
271
|
+
issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
|
|
272
|
+
extensions: (extField && extField.present) ? extField.value.result : [],
|
|
273
|
+
tbsBytes: e.tbsBytes,
|
|
274
|
+
signatureValue: e.signatureValue,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---- parse -----------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @primitive pki.schema.attrcert.parse
|
|
283
|
+
* @signature pki.schema.attrcert.parse(input) -> attributeCertificate
|
|
284
|
+
* @since 0.1.14
|
|
285
|
+
* @status experimental
|
|
286
|
+
* @spec RFC 5755, X.509
|
|
287
|
+
* @related pki.schema.parse, pki.schema.x509.parse
|
|
288
|
+
*
|
|
289
|
+
* Parse a DER `Buffer` or a PEM string/Buffer into a structured v2 attribute
|
|
290
|
+
* certificate: `{ version, holder, issuer, signatureAlgorithm, serialNumber,
|
|
291
|
+
* serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes,
|
|
292
|
+
* signatureValue }`. The holder / issuer identities come back as validated
|
|
293
|
+
* `GeneralName`s (each element `{ bytes, tagClass, tagNumber }`); the validity
|
|
294
|
+
* window is real `Date`s; `tbsBytes` is the exact signed byte range for a verifier.
|
|
295
|
+
*
|
|
296
|
+
* Throws `AttrCertError` when the bytes are not a well-formed attribute certificate
|
|
297
|
+
* (an obsolete `AttributeCertificateV1` throws `attrcert/legacy-v1-not-supported`)
|
|
298
|
+
* and `Asn1Error` when the underlying DER is malformed.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* var ac = pki.schema.attrcert.parse(der);
|
|
302
|
+
* ac.attributes[0].name; // "role"
|
|
303
|
+
* ac.validity.notAfterTime;// Date
|
|
304
|
+
*/
|
|
305
|
+
var PARSE_OPTS = { pemLabel: null, PemError: PemError, ErrorClass: AttrCertError, prefix: "attrcert", what: "attribute certificate" };
|
|
306
|
+
|
|
307
|
+
function _legacyV1Error() {
|
|
308
|
+
return new AttrCertError("attrcert/legacy-v1-not-supported", "AttributeCertificateV1 (X.509-1997) is obsolete and not parsed by this build (RFC 5755 §1)");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parse(input) {
|
|
312
|
+
var root = pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
|
|
313
|
+
// A well-formed legacy AttributeCertificateV1 gets the advertised, stable
|
|
314
|
+
// attrcert/legacy-v1-not-supported on the DIRECT path too — not a low-level asn1/*
|
|
315
|
+
// tag error from attempting the v2 walk — so a direct caller of this entry sees the
|
|
316
|
+
// same error family for the recognized-but-deferred form as the orchestrator does.
|
|
317
|
+
if (matchesV1(root)) throw _legacyV1Error();
|
|
318
|
+
return schema.walk(ATTRIBUTE_CERTIFICATE, root, NS).result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The obsolete X.509-1997 AttributeCertificateV1 (RFC 5652 §10.2) is recognized and
|
|
322
|
+
// deferred: decode the envelope (so a malformed input still fails as bad DER), then
|
|
323
|
+
// throw the precise diagnostic rather than routing it to a wrong format.
|
|
324
|
+
function parseV1(input) {
|
|
325
|
+
pkix.decodeRoot(pkix.coerceToDer(input, PARSE_OPTS), PARSE_OPTS);
|
|
326
|
+
throw _legacyV1Error();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @primitive pki.schema.attrcert.pemDecode
|
|
331
|
+
* @signature pki.schema.attrcert.pemDecode(text, label?) -> Buffer
|
|
332
|
+
* @since 0.1.14
|
|
333
|
+
* @status experimental
|
|
334
|
+
* @spec RFC 7468, RFC 5755
|
|
335
|
+
* @related pki.schema.attrcert.parse
|
|
336
|
+
*
|
|
337
|
+
* Extract the DER bytes from a PEM block (OpenSSL emits
|
|
338
|
+
* `-----BEGIN ATTRIBUTE CERTIFICATE-----`; the first block is taken unless `label`
|
|
339
|
+
* is given). Throws `PemError` on a missing envelope or a non-base64 body.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* var der = pki.schema.attrcert.pemDecode(pemText);
|
|
343
|
+
*/
|
|
344
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
345
|
+
|
|
346
|
+
// matches(root): a v2 AttributeCertificate is the signed-envelope trio whose acinfo
|
|
347
|
+
// LEADS WITH a bare universal INTEGER version (== v2 = 1, NOT the x509 [0] EXPLICIT
|
|
348
|
+
// wrapper) and whose attrCertValidityPeriod at children[5] is a SEQUENCE of exactly
|
|
349
|
+
// two GeneralizedTime (RFC 5755 §4.2.6 forbids UTCTime) — a marker no cert / CRL /
|
|
350
|
+
// CSR presents at that position. Disjoint from matchesV1 by the children[0] tag class.
|
|
351
|
+
function matches(root) {
|
|
352
|
+
var acinfo = pkix.signedEnvelopeTbs(root);
|
|
353
|
+
if (!acinfo) return false;
|
|
354
|
+
var k = acinfo.children;
|
|
355
|
+
if (!k || k.length < 7) return false;
|
|
356
|
+
if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
|
|
357
|
+
if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE && k[1].children)) return false;
|
|
358
|
+
var v = k[5];
|
|
359
|
+
if (!(v && v.tagClass === "universal" && v.tagNumber === TAGS.SEQUENCE && v.children && v.children.length === 2)) return false;
|
|
360
|
+
return v.children.every(function (t) { return t.tagClass === "universal" && t.tagNumber === TAGS.GENERALIZED_TIME; });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// matchesV1(root): the obsolete AttributeCertificateV1 — its acInfo (version DEFAULT
|
|
364
|
+
// v1 OMITTED) LEADS WITH the subject CHOICE (a context [0]/[1]), and its children[1]
|
|
365
|
+
// is the issuer GeneralNames (a universal SEQUENCE). The children[1] == SEQUENCE test
|
|
366
|
+
// keeps it disjoint from a v3 certificate (whose [0] EXPLICIT version is followed by
|
|
367
|
+
// an INTEGER serialNumber), so v1-vs-cert dispatch is order-independent.
|
|
368
|
+
function matchesV1(root) {
|
|
369
|
+
var acinfo = pkix.signedEnvelopeTbs(root);
|
|
370
|
+
if (!acinfo) return false;
|
|
371
|
+
var k = acinfo.children;
|
|
372
|
+
if (!k || k.length < 6) return false;
|
|
373
|
+
if (!(k[0].tagClass === "context" && (k[0].tagNumber === 0 || k[0].tagNumber === 1) && k[0].children)) return false;
|
|
374
|
+
return !!k[1] && k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
parse: parse,
|
|
379
|
+
parseV1: parseV1,
|
|
380
|
+
pemDecode: pemDecode,
|
|
381
|
+
matches: matches,
|
|
382
|
+
matchesV1: matchesV1,
|
|
383
|
+
};
|
package/lib/schema-engine.js
CHANGED
|
@@ -109,6 +109,11 @@ function implicitOctetString(tag) { return { kind: "leaf", read: function (n) {
|
|
|
109
109
|
// unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
|
|
110
110
|
// non-empty [tag] node fail-closed.
|
|
111
111
|
function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
|
|
112
|
+
// A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
|
|
113
|
+
// implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
|
|
114
|
+
// (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
|
|
115
|
+
// node fails asn1/* at the codec.
|
|
116
|
+
function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
|
|
112
117
|
function any() { return { kind: "any" }; }
|
|
113
118
|
function decode(fn) { return { kind: "decode", fn: fn }; }
|
|
114
119
|
|
|
@@ -196,6 +201,15 @@ function implicitSetOf(tag, item, opts) {
|
|
|
196
201
|
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
|
|
197
202
|
min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
198
203
|
}
|
|
204
|
+
// [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
|
|
205
|
+
// tag, so the node is a context-class constructed [tag] whose direct children are the
|
|
206
|
+
// items. Order-preserving: the SEQUENCE-OF sibling of implicitSetOf WITHOUT the SET
|
|
207
|
+
// ascending-DER-order rule (RFC 3161 extensions [1] IMPLICIT Extensions).
|
|
208
|
+
function implicitSeqOf(tag, item, opts) {
|
|
209
|
+
opts = opts || {};
|
|
210
|
+
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
|
|
211
|
+
min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
212
|
+
}
|
|
199
213
|
|
|
200
214
|
// ---- the walk engine -------------------------------------------------
|
|
201
215
|
|
|
@@ -377,11 +391,11 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
377
391
|
module.exports = {
|
|
378
392
|
// structural
|
|
379
393
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
380
|
-
seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
|
|
394
|
+
seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, implicitSeqOf: implicitSeqOf, choice: choice,
|
|
381
395
|
// leaves
|
|
382
396
|
oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
|
|
383
397
|
bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
|
|
384
|
-
implicitNull: implicitNull,
|
|
398
|
+
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
385
399
|
any: any, decode: decode, time: time,
|
|
386
400
|
// engine
|
|
387
401
|
walk: walk,
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -105,59 +105,10 @@ function certificateBytes() {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// requestorName [1] EXPLICIT GeneralName —
|
|
109
|
-
//
|
|
110
|
-
// the
|
|
111
|
-
|
|
112
|
-
// alternatives (otherName [0], x400Address [3], directoryName [4], ediPartyName [5])
|
|
113
|
-
// must be constructed; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier [6]
|
|
114
|
-
// are primitive IA5String (7-bit); iPAddress [7] is a 4- or 16-octet OCTET STRING;
|
|
115
|
-
// registeredID [8] is a primitive OBJECT IDENTIFIER.
|
|
116
|
-
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
117
|
-
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
118
|
-
function _validateGeneralName(n, ctx) {
|
|
119
|
-
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
120
|
-
throw ctx.E("ocsp/bad-requestor-name", "requestorName must be a GeneralName (context tag [0]..[8]) (RFC 6960 §4.1.1)");
|
|
121
|
-
}
|
|
122
|
-
var t = n.tagNumber;
|
|
123
|
-
var constructed = !!n.children;
|
|
124
|
-
if (GN_CONSTRUCTED[t]) {
|
|
125
|
-
if (!constructed || n.children.length < 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
126
|
-
if (t === 0) {
|
|
127
|
-
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY } —
|
|
128
|
-
// both fields mandatory: exactly two children, a type-id OID and a [0] EXPLICIT
|
|
129
|
-
// value wrapper carrying exactly one inner value.
|
|
130
|
-
if (n.children.length !== 2) throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] } (RFC 5280 §4.2.1.6)");
|
|
131
|
-
try { asn1.read.oid(n.children[0]); }
|
|
132
|
-
catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
133
|
-
var v = n.children[1];
|
|
134
|
-
if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
|
|
135
|
-
throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
136
|
-
}
|
|
137
|
-
} else if (t === 4) {
|
|
138
|
-
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
139
|
-
if (n.children.length !== 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName directoryName [4] must wrap exactly one Name (RFC 5280 §4.2.1.6)");
|
|
140
|
-
schema.walk(NAME, n.children[0], ctx);
|
|
141
|
-
}
|
|
142
|
-
// x400Address [3] / ediPartyName [5]: validated to non-empty constructed form
|
|
143
|
-
// (their full ORAddress / EDIPartyName bodies are above the requestorName altitude).
|
|
144
|
-
} else {
|
|
145
|
-
if (constructed) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be primitive (RFC 5280 §4.2.1.6)");
|
|
146
|
-
if (GN_IA5[t]) {
|
|
147
|
-
if (n.content.length === 0) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
148
|
-
for (var i = 0; i < n.content.length; i++) {
|
|
149
|
-
if (n.content[i] > 0x7f) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be an IA5String (7-bit)");
|
|
150
|
-
}
|
|
151
|
-
} else if (t === 7) {
|
|
152
|
-
if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E("ocsp/bad-requestor-name", "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
153
|
-
} else if (t === 8) {
|
|
154
|
-
try { asn1.decodeOidContent(n.content); }
|
|
155
|
-
catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
159
|
-
}
|
|
160
|
-
var GENERAL_NAME_RAW = schema.decode(_validateGeneralName);
|
|
108
|
+
// requestorName [1] EXPLICIT GeneralName (RFC 6960 §4.1.1) — validated + surfaced raw
|
|
109
|
+
// via the shared pkix.generalName primitive (RFC 5280 §4.2.1.6), so a malformed
|
|
110
|
+
// GeneralName fails closed and the OCSP + TSP parsers cannot drift on this grammar.
|
|
111
|
+
var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "ocsp/bad-requestor-name" });
|
|
161
112
|
|
|
162
113
|
// An OCSP signature BIT STRING is a byte string handed to a cryptographic verifier,
|
|
163
114
|
// so it MUST be octet-aligned (0 unused bits); a non-octet-aligned signature is
|
package/lib/schema-pkix.js
CHANGED
|
@@ -208,6 +208,82 @@ function name(ns) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
|
|
212
|
+
// caller that keeps the value RAW but needs it to be a well-formed GeneralName: it
|
|
213
|
+
// checks the chosen alternative's tag, form (constructed vs primitive per X.690
|
|
214
|
+
// §10.2), and content — otherName [0] is a SEQUENCE { type-id OID, value [0] EXPLICIT
|
|
215
|
+
// }; x400Address [3] / ediPartyName [5] are non-empty constructed; directoryName [4]
|
|
216
|
+
// EXPLICIT wraps a valid Name; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier
|
|
217
|
+
// [6] are primitive non-empty IA5String (7-bit); iPAddress [7] is a 4- or 16-octet
|
|
218
|
+
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
|
|
219
|
+
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
220
|
+
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
221
|
+
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
222
|
+
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
223
|
+
function generalName(ns, opts) {
|
|
224
|
+
opts = opts || {};
|
|
225
|
+
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
226
|
+
var NAME = name(ns);
|
|
227
|
+
return schema.decode(function (n, ctx) {
|
|
228
|
+
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
229
|
+
throw ctx.E(code, "value must be a GeneralName (context tag [0]..[8]) (RFC 5280 §4.2.1.6)");
|
|
230
|
+
}
|
|
231
|
+
var t = n.tagNumber;
|
|
232
|
+
var constructed = !!n.children;
|
|
233
|
+
if (GN_CONSTRUCTED[t]) {
|
|
234
|
+
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
235
|
+
if (t === 0) {
|
|
236
|
+
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
237
|
+
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
238
|
+
try { asn1.read.oid(n.children[0]); }
|
|
239
|
+
catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
240
|
+
var v = n.children[1];
|
|
241
|
+
if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
|
|
242
|
+
throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
243
|
+
}
|
|
244
|
+
} else if (t === 4) {
|
|
245
|
+
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
246
|
+
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
247
|
+
schema.walk(NAME, n.children[0], ctx);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
|
|
251
|
+
if (GN_IA5[t]) {
|
|
252
|
+
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
253
|
+
for (var i = 0; i < n.content.length; i++) {
|
|
254
|
+
if (n.content[i] > 0x7f) throw ctx.E(code, "GeneralName [" + t + "] must be an IA5String (7-bit)");
|
|
255
|
+
}
|
|
256
|
+
} else if (t === 7) {
|
|
257
|
+
if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
258
|
+
} else if (t === 8) {
|
|
259
|
+
try { asn1.decodeOidContent(n.content); }
|
|
260
|
+
catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 §4.2.1.6), SIZE (1..MAX). Every
|
|
268
|
+
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
269
|
+
// surfaced raw, so a caller carrying a GeneralNames field cannot accept a malformed
|
|
270
|
+
// element by treating the whole sequence as opaque bytes. Returns { names: [{ bytes,
|
|
271
|
+
// tagClass, tagNumber }], bytes } (the decoded element list + the raw outer DER).
|
|
272
|
+
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames — the context tag REPLACES
|
|
273
|
+
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
274
|
+
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
275
|
+
// attribute-certificate / (future) CRMF parsers validate a GeneralNames identically.
|
|
276
|
+
function generalNames(ns, opts) {
|
|
277
|
+
opts = opts || {};
|
|
278
|
+
var code = opts.code || (ns.prefix + "/bad-general-names");
|
|
279
|
+
var gn = generalName(ns, { code: code });
|
|
280
|
+
function build(m) { return { names: m.items.map(function (it) { return it.value; }), bytes: m.node.bytes }; }
|
|
281
|
+
if (opts.implicitTag != null) {
|
|
282
|
+
return schema.implicitSeqOf(opts.implicitTag, gn, { min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
283
|
+
}
|
|
284
|
+
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
285
|
+
}
|
|
286
|
+
|
|
211
287
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
212
288
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
213
289
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -362,6 +438,8 @@ module.exports = {
|
|
|
362
438
|
attributeTypeAndValue: attributeTypeAndValue,
|
|
363
439
|
relativeDistinguishedName: relativeDistinguishedName,
|
|
364
440
|
name: name,
|
|
441
|
+
generalName: generalName,
|
|
442
|
+
generalNames: generalNames,
|
|
365
443
|
attribute: attribute,
|
|
366
444
|
extension: extension,
|
|
367
445
|
extensions: extensions,
|