@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 CHANGED
@@ -4,6 +4,26 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.14 — 2026-07-06
8
+
9
+ An RFC 5755 attribute-certificate parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.attrcert — an RFC 5755 attribute-certificate parser. pki.schema.attrcert.parse turns a DER Buffer or PEM into a structured v2 attribute certificate ({ version, holder, issuer, signatureAlgorithm, serialNumber, serialNumberHex, validity, attributes, issuerUniqueID, extensions, tbsBytes, signatureValue }). The holder (baseCertificateID / entityName / objectDigestInfo) and issuer (v1Form / v2Form) identities come back as validated GeneralNames; the validity window is real Dates; the privilege attributes (id-at-role, id-aca-group, id-at-clearance, and any others) resolve by name where the registry knows them. The outer-equals-inner signatureAlgorithm agreement (RFC 5755 4.2.4), the positive-and-at-most-20-octet serialNumber (4.2.5), the GeneralizedTime-only validity (4.2.6), the non-empty unique-typed attribute list (4.2.7), and the digestedObjectType enumeration are all enforced fail-closed. pki.schema.parse detect-and-routes an attribute certificate; the obsolete v1 form is recognized and deferred with a precise attrcert/legacy-v1-not-supported.
14
+ - pki.schema.pkix gains a shared GeneralNames validator that the attribute-certificate parser composes for its four GeneralNames-bearing fields — validating every element as a well-formed GeneralName (rejecting a bad tag, a wrong primitive/constructed form, a non-IA5 string, or a mis-sized iPAddress) rather than surfacing the sequence as opaque bytes. It handles both a bare universal SEQUENCE OF GeneralName and a context-tagged IMPLICIT GeneralNames.
15
+ - The OID registry gains the RFC 5755 attribute-certificate object identifiers: the id-aca attribute-type family (authenticationInfo / accessIdentity / chargingIdentity / group), id-at-role and id-at-clearance, the id-ce-targetInformation and id-ce-noRevAvail extensions, and the id-pe-ac-auditIdentity / id-pe-aaControls / id-pe-ac-proxying private extensions — so a parsed attribute certificate's attributes and extensions resolve by name.
16
+
17
+ ## v0.1.13 — 2026-07-05
18
+
19
+ An RFC 3161 timestamp parser joins the pki.schema family.
20
+
21
+ ### Added
22
+
23
+ - pki.schema.tsp — an RFC 3161 timestamp parser. pki.schema.tsp.parse turns a DER Buffer or PEM into a TimeStampResp ({ status, statusString, failInfo, timeStampToken }) with the granted-carries-token / rejected-carries-none coupling enforced (tsp/missing-token, tsp/unexpected-token) and the PKIFailureInfo named bits decoded. pki.schema.tsp.parseToken parses a TimeStampToken by composing pki.schema.cms.parse and asserting the id-ct-TSTInfo content type (tsp/wrong-econtent-type), attached content (tsp/detached-token), and the single (TSA) signer (tsp/multi-signer), returning the decoded TSTInfo plus the signer material. pki.schema.tsp.parseTstInfo decodes a bare TSTInfo. The TSTInfo mandatory version-1, the GeneralizedTime-only genTime, the accuracy 1..999 range, the ordering DEFAULT FALSE omission, and the PKIStatus 0..5 range are all enforced fail-closed. pki.schema.parse detect-and-routes a TimeStampResp.
24
+ - The codec and schema engine gain three composable primitives that TSP required: pki.asn1.read.integerImplicit / pki.schema.engine.implicitInteger(tag) (a context-tagged IMPLICIT INTEGER, for the Accuracy millis / micros fields); pki.schema.engine.implicitSeqOf(tag, item) (an order-preserving IMPLICIT SEQUENCE OF, the sibling of implicitSetOf without the SET ordering rule, for the extensions field); and RFC 3161 / X.690 §11.7 fractional-seconds GeneralizedTime support in pki.asn1.read.time (a '.'-separated, trailing-zero-free, Z-terminated fraction, surfaced at millisecond precision).
25
+ - The OID registry gains the id-kp extended-key-purpose family (including id-kp-timeStamping) and the id-aa S/MIME authenticated-attribute family (signingCertificate / signingCertificateV2 / timeStampToken), so a parsed TSA certificate's key purpose and a signer's ESS binding attribute resolve by name.
26
+
7
27
  ## v0.1.12 — 2026-07-05
8
28
 
9
29
  SLH-DSA object identifiers corrected and completed to all twelve FIPS 205 parameter sets.
package/README.md CHANGED
@@ -197,9 +197,11 @@ is callable today; nothing below is a stub.
197
197
  | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
198
198
  | `pki.schema.cms` | Parse DER / PEM CMS SignedData per RFC 5652 — encapsulated content, signer infos, raw signed-attribute bytes for external verification, certificates / CRLs kept raw, fail-closed; non-SignedData content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
199
199
  | `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode` |
200
+ | `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
201
+ | `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
200
202
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
201
203
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
202
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError`, each carrying a stable `code` in `domain/reason` form |
204
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError`, each carrying a stable `code` in `domain/reason` form |
203
205
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
204
206
 
205
207
  ### CLI
@@ -233,26 +235,34 @@ reintroduce the bug class it prevents. Adding a format is a schema declaration
233
235
  plus a documentation comment block, not new parse logic.
234
236
 
235
237
  ```
236
- Detect + route pki.schema.parse ── detects the format, routes to a sibling
237
-
238
- Format parsers x509 crl csr pkcs8 cms ocsp (siblings)
239
- (one schema each) └──────┴─────┴──────┴──────┴─────┘
240
- │ every sibling COMPOSES
241
- Shared structure pki.schema.engine PKIX sub-schemas
242
- walk + combinators · AlgorithmIdentifier · Name ·
243
- (positional reads, Extension · version reader ·
244
- tag ordering, SET-OF the one coerce → decode → walk
245
- uniqueness, arity, parse-entry (PEM cap + DER
246
- fail-closed errors) wrapping shared, never copied)
247
- │ built on
248
- Foundation pki.asn1 pki.oid pki.errors pki.C
249
- DER codec · OID registry · PkiError · constants
250
- (strict, (two-way, taxonomy (LIMITS,
251
- bounded) PQC-seeded) (domain/reason) scale)
252
-
253
- Crypto pki.webcrypto ──▶ node:crypto
254
- W3C SubtleCrypto over the platform: the classical set +
255
- post-quantum ML-DSA / SLH-DSA signatures + ML-KEM keygen
238
+ ┌─ Detect + route ────────────────────────────────────────────────────────┐
239
+ pki.schema.parse — inspect the DER root, route to the matching sibling │
240
+ └─────────────────────────────────────────────────────────────────────────┘
241
+
242
+ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ Format
243
+ x509 │ │ crl │ csr │ │ pkcs8 │ parsers
244
+ └────────┘ └────────┘ └────────┘ └────────┘ (siblings)
245
+ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
246
+ │ cms │ │ ocsp │ │ tsp │ │attrcert│
247
+ └────────┘ └────────┘ └────────┘ └────────┘
248
+ │ every sibling composes
249
+ ┌─ Shared structure ──────────────────────────────────────────────────────┐
250
+ pki.schema.engine — walk + combinators (positional reads, tag order, │
251
+ │ SET-OF uniqueness, arity, typed errors) · the PKIX sub-schemas — │
252
+ │ AlgorithmIdentifier · Name · Extension · a bounded version reader — │
253
+ │ and the one coerce → decode → walk parse-entry (PEM cap + DER wrap).
254
+ └─────────────────────────────────────────────────────────────────────────┘
255
+ built on
256
+ ┌─ Foundation ────────────────────────────────────────────────────────────┐
257
+ │ pki.asn1 strict, bounded DER codec · pki.oid — two-way, PQC-seeded │
258
+ │ OID registry · pki.errors — the PkiError domain/reason taxonomy · │
259
+ │ pki.C — version-stable LIMITS + scale constants. │
260
+ └─────────────────────────────────────────────────────────────────────────┘
261
+
262
+ ┌─ Crypto ────────────────────────────────────────────────────────────────┐
263
+ │ pki.webcrypto ──▶ node:crypto — a W3C SubtleCrypto engine: the │
264
+ │ classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation. │
265
+ └─────────────────────────────────────────────────────────────────────────┘
256
266
  ```
257
267
 
258
268
  **Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
@@ -266,7 +276,7 @@ bounded version reader, and the single coerce → decode → walk parse-entry th
266
276
  every format's `parse` is bound to. Because input coercion, the PEM size cap, and
267
277
  the DER-decode wrapping live here once, a format cannot diverge on a guard.
268
278
 
269
- **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, and `ocsp` are siblings:
279
+ **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, `tsp`, and `attrcert` are siblings:
270
280
  each is a schema declaration composed from the shared pieces, emitting its own
271
281
  typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
272
282
  detect-and-routes to the matching sibling; the detectors are mutually exclusive by
package/lib/asn1-der.js CHANGED
@@ -318,6 +318,18 @@ function readEnumerated(node) {
318
318
  return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
319
319
  }
320
320
 
321
+ // Read a [tag] IMPLICIT INTEGER — a context-class PRIMITIVE node whose content is an
322
+ // integer body (the IMPLICIT tag replaces the universal INTEGER tag). Same minimal
323
+ // two's-complement content rules as a universal INTEGER. The integer counterpart of
324
+ // readBitStringImplicit, for the RFC 3161 Accuracy millis [0] / micros [1] fields.
325
+ function readIntegerImplicit(node, tag) {
326
+ if (node.tagClass !== "context" || node.tagNumber !== tag) {
327
+ throw new Asn1Error("asn1/unexpected-tag", "readIntegerImplicit: expected context tag [" + tag +
328
+ "], got " + node.tagClass + "/" + node.tagNumber);
329
+ }
330
+ return _readIntegerLikeContent(node, "INTEGER", "readIntegerImplicit");
331
+ }
332
+
321
333
  // The BIT STRING content decoder (the leading unused-bit octet + body, with the
322
334
  // DER zero-pad rule). Shared by the universal reader and the IMPLICIT
323
335
  // context-tagged reader — the caller asserts the tag first.
@@ -533,8 +545,17 @@ function _decodeUtf32be(buf) {
533
545
 
534
546
  var UTC_RE = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
535
547
  var GEN_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
536
-
537
- function readTime(node) {
548
+ // The X.690 §11.7 DER fractional-seconds GeneralizedTime profile: seconds present,
549
+ // a `.` decimal separator (never `,`), a non-empty fraction, Z-terminated. The
550
+ // trailing-zeros rule (§11.7.3) is enforced separately. Used by RFC 3161 genTime.
551
+ var GEN_FRAC_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d+)Z$/;
552
+
553
+ // readTime(node, opts?) — opts.allowFractional enables the RFC 3161 / X.690 §11.7
554
+ // fractional-seconds GeneralizedTime profile. Fractional is OFF by default because
555
+ // most profiles (RFC 5280 certificate/CRL Validity, §4.1.2.5.2) require the integer-
556
+ // second YYYYMMDDHHMMSSZ form and MUST NOT include a fraction; only callers that
557
+ // permit sub-second precision (TSP genTime) pass allowFractional.
558
+ function readTime(node, opts) {
538
559
  if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-time", "readTime: not a universal time type");
539
560
  _expectPrimitive(node, "readTime");
540
561
  var s = node.content.toString("latin1");
@@ -547,7 +568,19 @@ function readTime(node) {
547
568
  year += (year < 50) ? 2000 : 1900;
548
569
  } else if (node.tagNumber === TAGS.GENERALIZED_TIME) {
549
570
  m = GEN_RE.exec(s);
550
- if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
571
+ if (!m) {
572
+ if (!(opts && opts.allowFractional)) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
573
+ m = GEN_FRAC_RE.exec(s);
574
+ if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSS[.fraction]Z, got " + JSON.stringify(s));
575
+ // X.690 §11.7.3 — the fractional part MUST NOT end in a zero (and is non-empty
576
+ // by the regex). ".5Z" is valid; ".50Z" and ".500Z" are non-canonical. X.690
577
+ // §11.7 places NO cap on the fraction length (RFC 3161 §2.4.2 gives the 5-digit
578
+ // example 19990609001326.34352Z), so a fraction of any length is accepted; the
579
+ // returned Date is millisecond-precision, and a caller needing the exact fraction
580
+ // reads it from node.content (the raw bytes, surfaced losslessly like the OID /
581
+ // serial paths).
582
+ if (m[7].charAt(m[7].length - 1) === "0") throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime fraction must not have trailing zeros, got " + JSON.stringify(s));
583
+ }
551
584
  year = parseInt(m[1], 10);
552
585
  } else {
553
586
  throw new Asn1Error("asn1/expected-time", "readTime: tag " + node.tagNumber + " is not a time type");
@@ -562,9 +595,12 @@ function readTime(node) {
562
595
  // GeneralizedTime year below 100 (0099 -> 1999). setUTCFullYear(year, ...)
563
596
  // takes the year literally and uses that year's own calendar, so years
564
597
  // below 100 and proleptic leap years are handled correctly.
598
+ // A fractional GeneralizedTime carries sub-second precision; surface it on the
599
+ // Date at millisecond resolution (the first three fraction digits, zero-padded).
600
+ var ms = (node.tagNumber === TAGS.GENERALIZED_TIME && m[7]) ? parseInt((m[7] + "000").slice(0, 3), 10) : 0;
565
601
  var d = new Date(0);
566
602
  d.setUTCFullYear(year, month - 1, day);
567
- d.setUTCHours(hour, min, sec, 0);
603
+ d.setUTCHours(hour, min, sec, ms);
568
604
  if (isNaN(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
569
605
  // Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
570
606
  // month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
@@ -818,6 +854,7 @@ module.exports = {
818
854
  read: {
819
855
  boolean: readBoolean,
820
856
  integer: readInteger,
857
+ integerImplicit: readIntegerImplicit,
821
858
  enumerated: readEnumerated,
822
859
  bitString: readBitString,
823
860
  bitStringImplicit: readBitStringImplicit,
@@ -145,6 +145,16 @@ var CmsError = defineClass("CmsError", { withCause: true });
145
145
  // (OCSPRequest / OCSPResponse — RFC 6960 §4). Carries the leaf fault as `.cause`.
146
146
  var OcspError = defineClass("OcspError", { withCause: true });
147
147
 
148
+ // TspError — a byte sequence that is not a well-formed RFC 3161 timestamp token,
149
+ // TSTInfo, or TimeStampResp. Carries the underlying leaf fault as `.cause`.
150
+ var TspError = defineClass("TspError", { withCause: true });
151
+
152
+ // AttrCertError — a byte sequence that is not a well-formed X.509 Attribute
153
+ // Certificate (AttributeCertificate / AttributeCertificateInfo — RFC 5755 §4),
154
+ // or a recognized-and-deferred legacy AttributeCertificateV1. Carries the
155
+ // underlying leaf fault as `.cause`.
156
+ var AttrCertError = defineClass("AttrCertError", { withCause: true });
157
+
148
158
  module.exports = {
149
159
  PkiError: PkiError,
150
160
  defineClass: defineClass,
@@ -159,4 +169,6 @@ module.exports = {
159
169
  Pkcs8Error: Pkcs8Error,
160
170
  CmsError: CmsError,
161
171
  OcspError: OcspError,
172
+ TspError: TspError,
173
+ AttrCertError: AttrCertError,
162
174
  };
package/lib/oid.js CHANGED
@@ -49,7 +49,9 @@ var FAMILIES = {
49
49
  attributeType: { base: [2, 5, 4], of: {
50
50
  commonName: 3, surname: 4, serialNumber: 5, countryName: 6,
51
51
  localityName: 7, stateOrProvinceName: 8, streetAddress: 9,
52
- organizationName: 10, organizationalUnitName: 11, title: 12, givenName: 42 } },
52
+ organizationName: 10, organizationalUnitName: 11, title: 12, givenName: 42,
53
+ // RFC 5755 §4.4 attribute-certificate attribute types.
54
+ clearance: 55, role: 72 } },
53
55
 
54
56
  // RFC 5280 certificate extensions.
55
57
  certExtension: { base: [2, 5, 29], of: {
@@ -60,10 +62,28 @@ var FAMILIES = {
60
62
  issuingDistributionPoint: 28, certificateIssuer: 29,
61
63
  nameConstraints: 30, cRLDistributionPoints: 31,
62
64
  certificatePolicies: 32, authorityKeyIdentifier: 35, extKeyUsage: 37,
63
- freshestCRL: 46 } },
64
-
65
- // PKIX private extensions (authorityInfoAccess et al).
66
- pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: { authorityInfoAccess: 1 } },
65
+ freshestCRL: 46,
66
+ // RFC 5755 attribute-certificate extensions (§4.3.2, §4.3.6).
67
+ targetInformation: 55, noRevAvail: 56 } },
68
+
69
+ // PKIX private extensions on the id-pe arc (authorityInfoAccess et al, plus the
70
+ // RFC 5755 attribute-certificate id-pe extensions: ac-auditIdentity §4.3.1,
71
+ // aaControls §7.4, ac-proxying §7.2).
72
+ pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
73
+ authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10 } },
74
+
75
+ // id-aca — RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
76
+ // authenticationInfo §4.4.1 .. group §4.4.4, plus encAttrs §7.1 (the encrypted-
77
+ // attribute wrapper, syntax ContentInfo). { id-aca 5 } is reserved.
78
+ idAca: { base: [1, 3, 6, 1, 5, 5, 7, 10], of: {
79
+ authenticationInfo: 1, accessIdentity: 2, chargingIdentity: 3, group: 4, encAttrs: 6 } },
80
+
81
+ // The legacy RFC 3281 id-at-clearance on the X.501 selected-attribute-types arc.
82
+ // RFC 5755 §4.4.6 says implementations MUST NOT OUTPUT this form but SHOULD ACCEPT
83
+ // it for decoding, so it is registered as an alias of "clearance". The canonical
84
+ // name -> OID reverse stays the RFC 5755 attributeType-arc 2.5.4.55 (registered
85
+ // first under attributeType, so it wins the reverse mapping).
86
+ selectedAttrType: { base: [2, 5, 1, 5], of: { clearance: 55 } },
67
87
 
68
88
  // PKIX Access Descriptor methods (id-ad, RFC 5280 §4.2.2.1/§4.2.2.2). id-ad-ocsp
69
89
  // is the arc the OCSP responder OIDs hang under; id-ad-caIssuers names the AIA
@@ -79,6 +99,11 @@ var FAMILIES = {
79
99
  ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
80
100
  ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
81
101
 
102
+ // PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
103
+ // — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
104
+ pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
105
+ serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9 } },
106
+
82
107
  // PKCS#1 RSA public-key + RSASSA signature algorithms.
83
108
  rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
84
109
  rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
@@ -98,6 +123,12 @@ var FAMILIES = {
98
123
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
99
124
  smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
100
125
 
126
+ // S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
127
+ // signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
128
+ // signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 §2.2.1).
129
+ smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
130
+ signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
131
+
101
132
  // ANSI X9.62 EC public key, named curve, and ECDSA signatures.
102
133
  ansiX962: { base: [1, 2, 840, 10045], of: {
103
134
  ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
package/lib/schema-all.js CHANGED
@@ -38,6 +38,8 @@ var csr = require("./schema-csr");
38
38
  var pkcs8 = require("./schema-pkcs8");
39
39
  var cms = require("./schema-cms");
40
40
  var ocsp = require("./schema-ocsp");
41
+ var tsp = require("./schema-tsp");
42
+ var attrcert = require("./schema-attrcert");
41
43
  var frameworkError = require("./framework-error");
42
44
 
43
45
  var SchemaError = frameworkError.SchemaError;
@@ -66,12 +68,27 @@ var FORMATS = [
66
68
  detect: cms.matches,
67
69
  parse: function (input) { return cms.parse(input); },
68
70
  },
71
+ {
72
+ // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL } —
73
+ // a SEQUENCE of 1-2 whose first child is a PKIStatusInfo SEQUENCE whose own first
74
+ // child is an INTEGER status. This detector is a strict REFINEMENT of the
75
+ // ocsp-request one (both are SEQUENCE-of-1-2 with a SEQUENCE first child), so it
76
+ // MUST be checked first: a tokenless TimeStampResp (a bare PKIStatusInfo) would
77
+ // otherwise be shadowed by ocsp-request, whereas an OCSPRequest's tbsRequest never
78
+ // leads with a universal INTEGER and so never matches here (RFC 3161 §2.4.2). A
79
+ // bare timestamp token is a CMS ContentInfo and routes to cms; a bare TSTInfo is
80
+ // an internal payload (reached via pki.schema.tsp.parseTstInfo).
81
+ name: "tsp",
82
+ module: tsp,
83
+ detect: tsp.matches,
84
+ parse: function (input) { return tsp.parse(input); },
85
+ },
69
86
  {
70
87
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
71
88
  // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
72
89
  // Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
73
- // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2), so it detects
74
- // unambiguously regardless of registry order (RFC 6960 §4.1.1).
90
+ // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
91
+ // whose detector is a strict refinement (RFC 6960 §4.1.1).
75
92
  name: "ocsp-request",
76
93
  module: ocsp,
77
94
  detect: ocsp.matchesRequest,
@@ -110,6 +127,27 @@ var FORMATS = [
110
127
  detect: csr.matches,
111
128
  parse: function (input) { return csr.parse(input); },
112
129
  },
130
+ {
131
+ // AttributeCertificate ::= SEQUENCE { acinfo, signatureAlgorithm, signatureValue }
132
+ // — the signed-envelope trio, distinguished from a certificate / CRL by an acinfo
133
+ // that LEADS WITH a bare INTEGER version and carries a 2-GeneralizedTime validity
134
+ // at children[5]. Registered ahead of crl / x509 (most-specific-first): a legacy v1
135
+ // AC would otherwise satisfy x509.matches's Validity probe (RFC 5755 §4.1).
136
+ name: "attrcert",
137
+ module: attrcert,
138
+ detect: attrcert.matches,
139
+ parse: function (input) { return attrcert.parse(input); },
140
+ },
141
+ {
142
+ // AttributeCertificateV1 (X.509-1997, RFC 5652 §10.2) — the obsolete predecessor
143
+ // whose acInfo LEADS WITH the subject CHOICE (context [0]/[1]). Recognized and
144
+ // deferred with a precise attrcert/legacy-v1-not-supported rather than routed to a
145
+ // wrong format. children[1] == SEQUENCE keeps it disjoint from a v3 certificate.
146
+ name: "attrcert-v1",
147
+ module: attrcert,
148
+ detect: attrcert.matchesV1,
149
+ parse: function (input) { return attrcert.parseV1(input); },
150
+ },
113
151
  {
114
152
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
115
153
  // signatureValue } — the same outer shape as a certificate, distinguished
@@ -141,7 +179,7 @@ var FORMATS = [
141
179
  * The names of every registered format, in detection order.
142
180
  *
143
181
  * @example
144
- * pki.schema.all(); // → ["cms", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
182
+ * pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
145
183
  */
146
184
  function all() { return FORMATS.map(function (f) { return f.name; }); }
147
185
 
@@ -185,6 +223,8 @@ module.exports = {
185
223
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
186
224
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
187
225
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
226
+ tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
227
+ attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
188
228
  all: all,
189
229
  parse: parse,
190
230
  };