@blamejs/pki 0.9.0 → 0.9.3
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 +36 -0
- package/MIGRATING.md +103 -0
- package/README.md +5 -5
- package/lib/alt-sig.js +3 -3
- package/lib/attrcert-sign.js +5 -5
- package/lib/cmc-verify.js +13 -9
- package/lib/cmp-build.js +2 -2
- package/lib/cmp-verify.js +1 -1
- package/lib/cms-sign.js +21 -0
- package/lib/cms-verify.js +5 -3
- package/lib/constants.js +3 -0
- package/lib/crl-sign.js +1 -1
- package/lib/crmf-sign.js +1 -1
- package/lib/csr-sign.js +4 -2
- package/lib/est.js +93 -21
- package/lib/guard-identifier.js +110 -0
- package/lib/guard-name.js +91 -4
- package/lib/inspect.js +4 -4
- package/lib/lint.js +2 -2
- package/lib/mime.js +124 -17
- package/lib/ocsp-verify.js +1 -1
- package/lib/pki-build.js +14 -0
- package/lib/possession.js +20 -14
- package/lib/related-cert.js +72 -11
- package/lib/schema-attrcert.js +5 -5
- package/lib/schema-c509.js +42 -6
- package/lib/schema-cmp.js +3 -3
- package/lib/schema-pkix.js +62 -3
- package/lib/schema-trustanchor.js +4 -4
- package/lib/sigstore.js +1 -1
- package/lib/smime.js +18 -7
- package/lib/trust.js +1 -1
- package/lib/trustanchor-build.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ 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.9.3 — 2026-10-09
|
|
8
|
+
|
|
9
|
+
Carry out the signature a caller asked for, compare a name by its own matching rule, and answer a proof of possession with which check failed.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.schema.c509.parse` reports `signatureValueMinimalWidth` on a type-3 certificate. A fixed-width `r || s` names no curve, so the same signature padded to a wider accepted width reconstructs byte-identical DER: 64, 96 and 132 are three spellings of one certificate, and a C509 byte string is therefore not a unique identifier for what it encodes. The field is the narrowest width that represents the value, so `signatureValueMinimalWidth === signatureValue.length` answers whether this encoding is the canonical one, and re-narrowing to it gives a key that is the same for every spelling. Nothing is refused, and a genuine P-384 or P-521 signature reports its own width.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- A signer descriptor is held to the fields the verb reads. `pki.cms.sign` and `pki.cms.countersign` refuse a field they do not recognize, which `pki.smime.sign` inherits by signing through them, and which `pki.pkcs12.build` already applied to the same descriptors. Previously `{ cert, key, digestAlgoritm: "sha384" }` was accepted and signed under SHA-256, so a caller who believed they had configured SHA-384 got the default with nothing reported, while the identical misspelling one argument along in `opts` was already a hard `cms/bad-input`. A signer takes `{ cert, key }` or, where the signer is a key the message itself identifies, `{ spki, keyIdentifier, key }`; a countersigner takes `{ cert, key }`. Either form takes `pss`, `digestAlgorithm` and `combinedRsaSig` beside it, and anything else raises `cms/bad-input` naming the field and the form it was held to. A descriptor may be a function carrying those fields, of any callable kind, and the names a function carries by construction are not fields the caller named.
|
|
18
|
+
- A name field takes a common name, and a distinguished name written there is refused rather than certified whole. `subject: "CN=Example CA, O=Example"` produced one RDN whose commonName was that entire string, escaped comma and all: the subject read back as `CN=CN=Example CA\, O=Example`. The refusal is in the shared name encoder, so it covers `pki.x509.sign`, `pki.csr.sign`, `pki.crl.sign`, the CMP, CRMF, attribute-certificate and trust-anchor builders, and a `directoryName` in a GeneralName. A common name is unaffected wherever RFC 4514 sec. 3 reads no attribute type at its opening: one that contains an `=` further along, such as `Example CA (v=2)`; one whose numeric opening is not an object identifier, `attributeType` being `descr / numericoid` and a numericoid carrying at least one dot, so `1=1` is a common name while `2.5.4.3=Example CA` is a name; and one opening with whitespace, which the grammar admits at no position. Pass `pki.x509.parseDn(s).bytes` for a distinguished name, or `[{ commonName: s }]` to certify the text as one common name. OpenSSL refuses the same string for the same reason, and the message names both forms.
|
|
19
|
+
- `pki.relatedCert.verifyRequest` returns a verdict object rather than a bare boolean: `{ valid, verified, signerMaySign, signatureAlgorithm }`, plus `code` and `reason` when it did not pass. Read `res.valid` where you read the boolean. A bare boolean could not say which check failed, and there are now two: `verified` is the proof's own signature, and `signerMaySign` is whether the related certificate's keyUsage authorizes that signature at all, which RFC 5280 sec. 4.2.1.3 decides. `signatureAlgorithm` names the algorithm the proof was checked under, which the request does not carry.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- An S/MIME signer descriptor that carries its fields on a prototype is read the way a property read reads it. A method on the descriptor's class is not a field, which `pki.smime.sign` already held, and neither is a same-named value that method shadows: a class defining a `digestAlgorithm` method over a base class that holds a `digestAlgorithm` string sent the string through as the field while the readable value was the method, and signing failed as `cms/unsupported-algorithm` naming an undefined digest. Such a descriptor now signs under the digest it asked for, or the default where it asked for none.
|
|
24
|
+
- A distinguished name is compared under the matching rule its own attribute carries. Every attribute value was compared case-insensitively, which is correct for every attribute RFC 5280 sec. 4.1.2.4 requires support for and wrong for the ones whose rule is case-exact: `challengePassword` (RFC 2985 sec. 5.4.1), `localKeyId` (sec. 5.5.2) and `role` (RFC 5755 sec. 4.4.5) are all resolvable by name, mintable through `pki.x509.parseDn`, and compared EQUAL for values differing only in case. Name chaining, revocation scope and name constraints all rest on that comparison. Everything else keeps the case-insensitive rule, so a conforming PKIX name compares exactly as before.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- A signature verdict is assembled through the continuation the toolkit captured when it loaded, so a `Promise.prototype.then` replaced afterwards cannot substitute the result the verdict rests on. `p.then(f)` reads `then` off the promise at the call, and these verbs each computed `valid` inside such a continuation from the one boolean a cryptographic verify resolved: measured, a replacement substituting `true` for a `false` fulfillment value turned a tampered CSR signature, a CRL held against a key that did not sign it, a proof of possession over the wrong bytes and an attribute certificate held against a key other than its issuer's all into passing verdicts, each with the replacement reaching exactly that one continuation. `pki.csr.verify`, `pki.crl.verify`, `pki.attrcert.verify`, `pki.relatedCert.verifyRequest`, `pki.possession.verifyRequest`, `pki.cmc.verify`, `pki.est` enrollment and the Sigstore signed-certificate-timestamp check now take theirs from the capture, which `pki.cms.verify`, `pki.path.validate`, `pki.webauthn.verify` and `pki.crmf.verifyPop` already did. An OCSP CertID digest is taken the same way, a substituted digest being able to match a certificate the response does not name.
|
|
29
|
+
- An authorization gate can no longer be answered by the object prototype. `pki.possession.verifyRequest` and `pki.cms.verify` ask the decoded KeyUsage extension whether the certificate's key may make a signature, and one of the names they ask for is `contentCommitment`, the X.509 spelling of the `nonRepudiation` bit. The decoder emits the RFC 5280 sec. 4.2.1.3 names and never that one, and the record it returned inherited `Object.prototype`, so a value set there answered instead: a certificate confined to `cRLSign` went from unauthorized to authorized under a single assignment. The decoded record now inherits nothing, so a name the decoder does not define answers nothing, and the two gates ask only for names it does define.
|
|
30
|
+
- An establishment key usage authorizes a proof of possession only where the subject key's own family performs that establishment mechanism. RFC 9763 sec. 3.1 has a key establishment certificate's private key sign once for proof of possession, so `pki.relatedCert.verifyRequest` admits such a usage where it admits no other signature; it accepted either establishment bit from any key, and a bit the key cannot perform does not make an establishment certificate. Measured: an EC certificate asserting only `keyEncipherment`, which RFC 8813 sec. 3 makes a MUST NOT for `id-ecPublicKey`, and an RSA certificate asserting only `keyAgreement`, which RFC 3279 sec. 2.3.1 does not list, both authorized the proof. `keyEncipherment` now authorizes it for an RSA or ML-KEM key and `keyAgreement` for an EC, X25519, X448 or Diffie-Hellman key, a family in neither column needs a signature usage, and a certificate carrying no keyUsage extension stays unconstrained as sec. 4.2.1.3 leaves it.
|
|
31
|
+
|
|
32
|
+
## v0.9.1 — 2026-10-08
|
|
33
|
+
|
|
34
|
+
Refuse a MIME entity that two conforming readers would read differently, and hold one entity to a bounded number of parts, header lines and parameters.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `pki.smime.verify` and `pki.smime.decrypt` refuse an entity whose `Content-Type` declares one parameter twice, or whose header area declares one field twice. Both readings of such an entity are conforming and they differ, so the signature's own coverage is in question: `boundary` sets where the parts begin, `protocol` and `smime-type` decide whether the entity is accepted, and `micalg` decides which digest is reported back. Previously the verdict followed the order the two occurrences appeared in. `boundary="DECOY"; boundary=<real>` returned `valid: true` while the same two values swapped was refused, and a second `Content-Type: text/plain` placed after the real field returned `valid: true` while the same field placed before it was refused, which left a reader taking the last field seeing `text/plain`, finding no signature part, and reading the message as unsigned. Both orders of both shapes now raise `smime/bad-mime`. An entity naming each parameter and each field once is unaffected, and every occurrence of a field that may legally repeat, such as `Received`, is still readable from `headers`: only resolving a repeated name to a single value is refused.
|
|
39
|
+
- One MIME entity is bounded at 1024 body parts (`pki.constants.LIMITS.MIME_MAX_PARTS`), 1024 header lines (`MIME_MAX_HEADERS`) and 256 `Content-Type` parameters (`MIME_MAX_PARAMS`), and each count is charged as the entity is scanned rather than after it has been taken apart. A 1.4 MiB body declaring 200,000 empty parts previously split into all 200,000, and a 16 MiB header area took 1114 ms and 653 MiB of resident memory to read. A folded field spends one of the 1024 lines per physical line it occupies. `pki.est.splitMultipartMixed` and `pki.est.parseServerKeygenResponse` are held to the same part and header-line bounds, and raise `est/bad-multipart`. From `pki.smime.verify`, an entity over the part bound raises `smime/bad-multipart` and one over the header-line or parameter bound raises `smime/bad-mime`. The 16 MiB entity cap is unchanged and is still applied before any of the bytes are copied.
|
|
40
|
+
- The record `pki.smime.verify` returns, and its `content`, no longer share the buffer the caller passed in. The parsed entity, its header area and its body were views into the caller's own bytes, so a write into that buffer after the verdict returned changed what the caller read back as verified. The entity is copied before it is read, the copy is byte-for-byte the input, and the octets a signature covers are unchanged.
|
|
41
|
+
- The reference at [pkijs.com](https://pkijs.com) reads on a phone. At a 375px viewport the page scrolled 771px wide, and the nineteen navigation groups stood 659px tall even collapsed, so the first screen held navigation and no documentation. The content now starts under a compact header carrying the brand and a menu button, the navigation opens from that button, and tables and code blocks scroll inside their own box instead of widening the page. The menu is a label and a checkbox, so it works with JavaScript off, as the search form does. The layout at 721px and above is unchanged.
|
|
42
|
+
|
|
7
43
|
## v0.9.0 — 2026-10-08
|
|
8
44
|
|
|
9
45
|
Import any namespace by name from an ESM caller, and give every record the toolkit builds for an omitted argument no prototype.
|
package/MIGRATING.md
CHANGED
|
@@ -14,6 +14,109 @@ The toolkit has no `deprecate()`-marked surface awaiting removal.
|
|
|
14
14
|
|
|
15
15
|
Listed newest-first.
|
|
16
16
|
|
|
17
|
+
### v0.9.3 — `pki.relatedCert.verifyRequest, the pki.cms signer descriptor, and a bare name string`
|
|
18
|
+
|
|
19
|
+
verifyRequest returns a verdict object instead of a bare boolean; a signer descriptor and a bare name string are both held to what the verb actually reads.
|
|
20
|
+
|
|
21
|
+
Three changes, each refusing or re-shaping something that was previously accepted and then
|
|
22
|
+
quietly read as something else.
|
|
23
|
+
|
|
24
|
+
**`pki.relatedCert.verifyRequest` returns a verdict object.** Read `res.valid` where you read
|
|
25
|
+
the boolean:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
if (await pki.relatedCert.verifyRequest(attr, cert, opts)) // an always-true object now
|
|
29
|
+
|
|
30
|
+
var res = await pki.relatedCert.verifyRequest(attr, cert, opts);
|
|
31
|
+
if (res.valid) { /* ... */ }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This is a SILENT break: the old return was a boolean, so an unmigrated `if (await ...)` reads
|
|
35
|
+
the new object as truthy and accepts what it used to reject. The verdict is
|
|
36
|
+
`{ valid, verified, signerMaySign, signatureAlgorithm }`, plus `code` and `reason` when it did
|
|
37
|
+
not pass. `verified` is the proof's own signature and `signerMaySign` is whether the related
|
|
38
|
+
certificate's keyUsage authorizes that signature, which RFC 5280 sec. 4.2.1.3 decides; a bare
|
|
39
|
+
boolean could not tell the two apart. A key-establishment certificate still passes, because
|
|
40
|
+
RFC 9763 sec. 3.1 has such a key sign once for proof of possession, so `keyEncipherment` and
|
|
41
|
+
`keyAgreement` authorize this one signature. A certificate confined to `keyCertSign`,
|
|
42
|
+
`cRLSign`, `encipherOnly` or `dataEncipherment` is now refused with
|
|
43
|
+
`relatedcert/signer-may-not-sign`.
|
|
44
|
+
|
|
45
|
+
**A signer descriptor is held to the fields the verb reads.** `pki.cms.sign`,
|
|
46
|
+
`pki.cms.countersign`, `pki.cms.authenticate` and `pki.smime.sign` raise `cms/bad-input`
|
|
47
|
+
naming any field they do not recognize, as `pki.pkcs12.build` already did:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
pki.cms.sign(content, [{ cert, key, digestAlgoritm: "sha384" }]) // signed under sha256
|
|
51
|
+
pki.cms.sign(content, [{ cert, key, digestAlgorithm: "sha384" }]) // signs under sha384
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A descriptor takes `{ cert, key }` or `{ spki, keyIdentifier, key }`, plus `pss`,
|
|
55
|
+
`digestAlgorithm` and `combinedRsaSig`. Passing a wider object through, such as a test helper's
|
|
56
|
+
whole bag, now names the extra field rather than ignoring it.
|
|
57
|
+
|
|
58
|
+
**A bare name string is a common name, and one written as a distinguished name is refused.**
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
pki.x509.sign({ subject: "CN=Example CA, O=Example", ... }) // certified CN=CN=Example CA\, O=Example
|
|
62
|
+
pki.x509.sign({ subject: pki.x509.parseDn("CN=Example CA, O=Example").bytes, ... })
|
|
63
|
+
pki.x509.sign({ subject: "Example CA", ... }) // unchanged: one common name
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The refusal is in the shared name encoder, so it covers every builder that takes a name and a
|
|
67
|
+
`directoryName` in a GeneralName. A string whose opening characters are not an attribute type
|
|
68
|
+
followed by `=` is a common name and is unaffected, including one containing an `=` later on.
|
|
69
|
+
The message names both ways forward.
|
|
70
|
+
|
|
71
|
+
If you issue from a verified request, take the request's own `subject.bytes` rather than its
|
|
72
|
+
`subject.dn`: the DER is what was requested, and the text form is read as a common name.
|
|
73
|
+
|
|
74
|
+
### v0.9.1 — `pki.smime.verify, pki.smime.decrypt, pki.est.splitMultipartMixed, and pki.est.parseServerKeygenResponse`
|
|
75
|
+
|
|
76
|
+
A MIME entity that names one Content-Type parameter twice, or one header field twice, is refused rather than resolved by position, and one entity is bounded at 1024 body parts, 1024 header lines, and 256 Content-Type parameters.
|
|
77
|
+
|
|
78
|
+
No API shape changed, so there is nothing to rewrite. What changes is which messages produce a
|
|
79
|
+
verdict.
|
|
80
|
+
|
|
81
|
+
Two shapes are refused outright, in either order of the two occurrences:
|
|
82
|
+
|
|
83
|
+
- A `Content-Type` naming one parameter twice, such as `boundary="a"; boundary="b"`, or two
|
|
84
|
+
`micalg`, `protocol`, or `smime-type` parameters. Raises `smime/bad-mime`.
|
|
85
|
+
- A header area naming one field twice, such as two `Content-Type` lines. Raises
|
|
86
|
+
`smime/bad-mime`.
|
|
87
|
+
|
|
88
|
+
RFC 2045 sec. 5.1 gives a parameter one occurrence and RFC 5322 sec. 3.6 gives a singleton field
|
|
89
|
+
one, so an entity naming either twice has two conforming readings that disagree about what the
|
|
90
|
+
entity is: the `boundary` decides which octets a detached signature covers. Before this release
|
|
91
|
+
the verdict followed the order the two occurrences appeared in, so the same two values swapped
|
|
92
|
+
flipped between `valid: true` and a refusal.
|
|
93
|
+
|
|
94
|
+
A field that may legally repeat, such as `Received`, is unaffected, and every occurrence stays
|
|
95
|
+
readable from `headers`. Only resolving a repeated name to a single value is refused.
|
|
96
|
+
|
|
97
|
+
Three bounds apply to one entity:
|
|
98
|
+
|
|
99
|
+
| bound | value | `pki.constants.LIMITS` row |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| body parts | 1024 | `MIME_MAX_PARTS` |
|
|
102
|
+
| header lines | 1024 | `MIME_MAX_HEADERS` |
|
|
103
|
+
| `Content-Type` parameters | 256 | `MIME_MAX_PARAMS` |
|
|
104
|
+
|
|
105
|
+
A folded field spends one of the 1024 header lines for each physical line it occupies, so a field
|
|
106
|
+
folded over three lines costs three. From `pki.smime`, an entity over the part bound raises
|
|
107
|
+
`smime/bad-multipart` and one over the header-line or parameter bound raises `smime/bad-mime`;
|
|
108
|
+
the `pki.est` readers raise `est/bad-multipart` for all three. The 16 MiB `MIME_MAX_BYTES` entity
|
|
109
|
+
cap is unchanged.
|
|
110
|
+
|
|
111
|
+
If a message you must keep reading carries one of the refused shapes, it is ambiguous to every
|
|
112
|
+
other S/MIME implementation as well, and the sender has to emit each parameter and each field
|
|
113
|
+
once. No option restores the previous behavior, because a precedence rule no other reader is
|
|
114
|
+
obliged to share cannot settle which octets a signature covers.
|
|
115
|
+
|
|
116
|
+
Separately, the entity record and the `content` a verdict is read with no longer share the buffer
|
|
117
|
+
passed in. If you relied on mutating your own input buffer to change what a returned record reads
|
|
118
|
+
back, that no longer works, and the record now holds the bytes the verdict was computed over.
|
|
119
|
+
|
|
17
120
|
### v0.6.12 — `pki.crl.verify, pki.pkcs12.verifyMac, and pki.ct.verifySctWithLogList`
|
|
18
121
|
|
|
19
122
|
Each returned a bare boolean and now returns a verdict object; read `res.valid` where you read the boolean.
|
package/README.md
CHANGED
|
@@ -251,7 +251,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
251
251
|
| `pki.tls` | RFC 8879 certificate compression and the RFC 8446 §4.4.2 Certificate message it carries (RFC 8446 is obsoleted by RFC 9846), which is the handshake's largest payload. `decompressCertificate` decodes a `CompressedCertificate` and returns the algorithm, the declared uncompressed length, the recovered message raw, and its per-entry certificate DER. Decompression carries the two-sided bound §5 requires: capped at the message's own declared length so a bomb is refused mid-stream, then compared to that declaration exactly, which catches the under-length direction a cap alone cannot see. An algorithm outside the RFC 8879 registry, one the runtime cannot decompress, or one the receiver never advertised is refused before any decompressor is handed the bytes; an empty compressed body is a framing violation (`tls/bad-framing`); and trailing bytes, after the message or after the compressed frame, are refused (`tls/trailing-data`), so one chain has exactly one encoding. `compressCertificate` is the inverse and round-trips its own output before returning. `parseCertificateMessage` decodes the RFC 8446 §4.4.2 message on its own, surfacing each entry's certificate DER ready for `pki.schema.x509.parse` alongside its raw extensions; a separate extension negotiates `certificate_type`, so it is declared, never guessed. All three registered algorithms (zlib, brotli, zstd) are implemented, each offered only where the running Node decompresses it safely — one whose decompressor answers a truncated frame with a short result instead of a fault is dropped at startup rather than advertised with a truncation it cannot detect. Structure only: no handshake is spoken and no certificate verified — `decompressCertificate`, `compressCertificate`, `parseCertificateMessage` |
|
|
252
252
|
| `pki.schema` | The schema family. `parse` detects which PKI format a DER or PEM input encodes and routes it to the right parser; `all` enumerates the registered formats; `pem.decodeBundle` reads every object in a multi-object PEM file (a `fullchain.pem`, a `ca-certificates.crt`, a key beside its certificate), carrying each object's label, DER and offset into the text, with explanatory text around the blocks read as text and the file bounded in both the number of objects and the bytes they decode to; `pem.encodeBundle` writes that list back; `pki.schema.pkcs1` reads and writes the RFC 8017 `RSA PRIVATE KEY` and `RSA PUBLIC KEY` encodings, with every component relation the specification states checked; `pki.schema.sec1` reads and writes the RFC 5915 `EC PRIVATE KEY` encoding, holding the scalar to its curve's width and the parameters field to a named curve; the engine and per-format members are grouped here |
|
|
253
253
|
| `pki.schema.x509` | Certificates (RFC 5280) parsed into structured, validated fields. Each extension is surfaced by name with its `extnValue` bytes; the shared extension decoders that read those bytes are what `pki.lint.certificate`, `pki.inspect` and `pki.path.validate` consume, and they cover the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification), the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker. Unknown statements stay opaque, fail-closed. `decodeExtensions` is the read door onto that table: it returns one row per extension carrying the decoded value, the raw `extnValue` octets, and a `state` of `decoded`, `unrecognized` or `undecodable`, so a reader can tell an extension nothing decodes from one whose value does not read. The RFC 3779 address and AS number blocks, their RFC 8360 identifiers, and the RFC 7633 TLS feature extension decode here too Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
|
|
254
|
-
| `pki.schema.c509` | C509 certificates (draft-ietf-cose-cbor-encoded-cert), the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR. `encode` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte, so the original signature still verifies. A `parse` result re-emits its native array in canonical deterministic CBOR, with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms: the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, inhibitAnyPolicy, OCSP No Check, TLS Feature), the general-name-bearing extensions (subjectAltName, issuer alternative name, name constraints, CRL distribution points, freshest CRL, authority and subject information access, and the full authority key identifier) over one shared GeneralNames codec, certificate policies with their CPS-URI and UserNotice qualifiers, policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers) with their RFC 8360 v2 twins, whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets. A value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact; a certificate outside the invertible set throws a typed `C509Error`. Called explicitly, since it is CBOR rather than DER and so is not auto-routed |
|
|
254
|
+
| `pki.schema.c509` | C509 certificates (draft-ietf-cose-cbor-encoded-cert), the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR. `encode` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte, so the original signature still verifies. A `parse` result re-emits its native array in canonical deterministic CBOR, with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms: the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, inhibitAnyPolicy, OCSP No Check, TLS Feature), the general-name-bearing extensions (subjectAltName, issuer alternative name, name constraints, CRL distribution points, freshest CRL, authority and subject information access, and the full authority key identifier) over one shared GeneralNames codec, certificate policies with their CPS-URI and UserNotice qualifiers, policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers) with their RFC 8360 v2 twins, whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets. A value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact; a certificate outside the invertible set throws a typed `C509Error`. A type-3 `parse` also reports `signatureValueMinimalWidth`, the narrowest width that represents the fixed-width `r || s` value: the same signature padded to a wider accepted width reconstructs byte-identical DER, so three C509 encodings can name one certificate, and comparing the field with `signatureValue.length` answers whether the encoding in hand is the canonical one. Called explicitly, since it is CBOR rather than DER and so is not auto-routed |
|
|
255
255
|
| `pki.schema.crl` | X.509 CRLs (RFC 5280 §5): revoked serials with real-`Date` revocation times, named and partly decoded extensions, each record carrying the raw `extnValue` octets as `valueBytes`. `decodeExtensions` returns the CRL's own extensions and every revoked entry's in one array, each row naming the scope it was read at, and covers the whole §5.2 and §5.3 profile including the issuing distribution point and the delta CRL indicator. `decodeExtension` takes the scope explicitly, since RFC 6960 §4.4.5 carries a CRL entry extension into an OCSP response Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
|
|
256
256
|
| `pki.schema.csr` | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature. `decodeExtensions` is what a CA reads to see what a request asked for, returning the rows of its one `extensionRequest` attribute; a request carrying more than one is refused, and `groups: true` reads each separately Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
|
|
257
257
|
| `pki.schema.trustanchor` | RFC 5914 trust anchor lists: a trust anchor as a structure rather than as a certificate, carrying the constraints a root program otherwise states out of band. `parse` reads a `TrustAnchorList` into `{ anchors }`, each naming which of the three `TrustAnchorChoice` arms it came from: a `certificate`, a bare `tbsCert` (an anchor has no signature to check), or a `taInfo`. A `taInfo` carries the public key, the key identifier, an optional title, and a `certPath` stating the name the anchor is known by plus the policies it may assert, the namespace it is trusted for, and how far below it a path may run. Every rule RFC 5914 states is enforced: an empty `taName`, a policy set carrying qualifiers, a negative path length, and a `requireExplicitPolicy` with no policy set are each refused, and where the anchor carries a certificate its subject, public key and `subjectKeyIdentifier` must match the anchor's own. The four extension types the RFC excludes from `exts` are reported and left out rather than refused, as it says to ignore them. `parseInfo` reads a bare `TrustAnchorInfo`. `pki.trust.parseTrustAnchorList` turns a list into anchors `pki.path.validate` consumes, and `pki.trustanchor.build` writes one Fail-closed — `parse`, `parseInfo`, `pemDecode`, `pemEncode` |
|
|
@@ -275,7 +275,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
275
275
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk`, `encode`, `embeddedDer`, and the schema combinators |
|
|
276
276
|
| `pki.path` | Certification-path validation (RFC 5280 §6). `validate` runs the §6.1 state machine over an ordered path and a trust anchor: signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA (a composite is accepted only when both its post-quantum and traditional components verify), validity windows, name chaining, basic constraints and path length, key usage, name constraints, the RFC 7633 §4.2.2 constraint that a certificate issued under a signing certificate advertising TLS features must offer the same set or a superset, and the certificate policies, processed as the RFC 9618 `valid_policy_graph` in place of the RFC 5280 §6.1 policy tree. The graph holds one node per depth and policy identifier and gives it several parents where the tree made a separate node under each, so its size is linear in the policies and mappings on the path rather than exponential in the path length: a six-certificate chain whose policies map to each other needs 25 nodes where the tree needed 5461, and chains the tree could not hold inside `maxPolicyNodes` now validate. It returns a structured verdict with per-check reason codes and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes through `checkPurpose` — an anchor that carries either one with no purpose named to select by is refused as a configuration fault rather than validated as though it carried none. The verdict says what was established. `revocationChecked` takes the weakest outcome on the path: `false` with no checker supplied, `"determined"` when every certificate got an explicit good or revoked answer, `"waived"` when `softFail` turned an undetermined one into a pass, and `"undetermined"` when one could not be answered at all — which includes a checker that throws, a fault `softFail` does not waive because `softFail` opts into an undetermined answer, not into a broken checker. `anchorConstraints` names the purpose the anchor was judged under and whether each of its two constraints applied. `crlChecker` supplies CRL-based revocation, covering partitioned and sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets corresponding shards accumulate reason coverage until all eight revocation reasons are covered, and delta CRLs, merged onto the complete CRL they may be combined with (§5.2.4) so a held certificate its delta releases reads good, while a delta that merges with nothing still reports what it lists and still withholds good. `ocspChecker` supplies OCSP-based revocation (RFC 6960: CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf-to-anchor path `validate` accepts, using name chaining plus the RFC 4158 §3.5 sort hints (an AKI/SKI match, an anchor-adjacent issuer, CA plus keyCertSign, validity at the check time — ordering hints, never filters), a depth-first search with backtracking so the first accepted path wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically. Every accept flows through `validate`, and its verdict is cross-checked against `openssl verify`. Opt-in AIA `caIssuers` fetching (`opts.fetchAia: true`) discovers a missing intermediate from a certificate's Authority Information Access URL (§4.2.2.1) over `pki.transport`, triggered only on a pool miss and bounded against SSRF and amplification: HTTPS only, a total fetch budget that caps fetching silently rather than throwing, a per-cert URL cap, a build-wide URL dedupe, a response-size and certificate-count cap, no redirect following, and every fault a silent skip. The TLS trust (`opts.tls`) stays distinct from the PKI `trustAnchors`, and every fetched certificate remains untrusted pool material that still flows through `validate` (never a trust anchor). It is off by default, so the default build is byte-identical offline. An `opts.direction` of `"reverse"` searches from a trust anchor toward the leaf (RFC 4158 §3.1), narrower when the anchor set is small and the leaf's issuer options fan out, and `"auto"` picks the direction by first-hop fan-out; reverse building reuses the same pool, visited set, caps, and `validate` gate, so the direction changes only search order, and the anchor is excluded from the returned path. Reverse building is pool-only and does not combine with `fetchAia`. When `opts.userInitialPolicySet` names acceptable certificate-policy OIDs and more than one path validates, build returns the accepted path whose user-constrained policy set best satisfies that set (RFC 4158 §4) rather than the first accepted path, the ranking bounded by the same candidate cap and a malformed set refused with `path/bad-input`; an `anyPolicy` entry is unconstrained and returns the first accepted path. In a mesh of cross-certified domains, where many candidates chain by name to the same place, a candidate is ranked by how many leading relative distinguished names it shares with the name the search is heading for (§3.5.16 and the §3.5.19 reverse method), so a certificate in the target's own domain is tried before one from a cross-certified domain, and forward building sorts a candidate under every candidate that can complete the path when its path length constraint cannot cover the certificates already below it (§3.5.7), an ordering that outranks the other hints rather than competing with them. No candidate is removed and an unbounded search still reaches every path the pool holds; a bounded one spends `maxCandidatesConsidered` on whichever branches the order steers it to. `fetchingChecker(opts)` is the checker that goes and gets what it needs: it reads the cRLDistributionPoints and id-ad-ocsp locations out of the certificate and asks for them over `opts.transport`, OCSP first and a CRL after (RFC 5019 §3.1). It is off unless an operator builds it and passes it, so the default validation touches no network, and it runs during `validate` and not during `build`, because §3.2 has a status check requested only after the chain signatures are validated. A distribution point is fetched only as a `fullName` carrying a URI whose scheme this client speaks; ldap, ftp, a `nameRelativeToCRLIssuer` and a point naming a cRLIssuer and no location are counted as skips, so a certificate naming nothing fetchable reads as that rather than as a failed fetch. Nothing is trusted by having been fetched: a CRL goes to `crlChecker` and a response to `verifyOcspResponse`, and the structure a body parses as is decided by which extension named the URL, so a Content-Type mismatch is reported and never selects a parser. Every fault is an undetermined status carrying its reason and `check` throws nothing, which is what leaves `softFail` able to waive one. Plaintext `http` is accepted by default, these being signed objects the toolkit verifies, with `allowPlaintextHttp: false` to narrow it; the SSRF guards hold either way (no private, loopback or link-local destination, no redirect following, a response-size cap, a per-certificate URL cap, a total fetch count, and a wall-clock deadline across the validation). A verified response is cached in process on its CertID and responder URL and a CRL on its request URL and issuer name, with reuse decided from the signed thisUpdate and nextUpdate alone. Pure and re-entrant — `validate`, `build`, `crlChecker`, `ocspChecker`, `fetchingChecker` |
|
|
277
277
|
| `pki.identity` | Service identity (RFC 9525, which obsoletes RFC 6125). `match(cert, references, opts)` says whether a certificate presents a name the client was trying to reach, which is a separate answer from whether it chains: §1.2 has an application need both, and `pki.path.validate` takes `opts.identity` to run them together and report each. A reference identity is a domain name (DNS-ID), a textual IP address or a 4- or 16-octet `BufferSource` (IP-ID), `{ type: "srv", service: "_imap", value: "example.com" }` (SRV-ID, RFC 4985), or `{ type: "uri", scheme: "https", value: "www.example.com" }` (URI-ID). A record carries its service type and its domain together, so no comparison can pair the service of one reference with the domain of another (§6.5). The subject `commonName` is never read, under any option or fallback, including when the certificate carries no subjectAltName at all (§2). Names are compared label by label under an ASCII-only fold with one trailing dot normalized on both sides; an A-label is compared as the ASCII it is and never decoded, so a reference carrying a character outside ASCII is refused rather than guessed at, this toolkit shipping no IDNA implementation. An IP-ID is compared octet for octet against an `iPAddress` entry, never against a `dNSName` holding the same address as text, and no prefix or mask applies (§6.2, §6.4). Wildcards are on and `opts.wildcards: false` turns them off, which §3 requires stating either way: one wildcard, the whole of the left-most label, reaching exactly one reference label, and a presented name failing those rules is ignored with the search continuing rather than refusing the certificate (§6.3). A wildcard spanning an administrative boundary such as `*.co.uk` is out of scope as it is in §7.1, since no public-suffix list ships here. The verdict names which reference matched, so a caller can use it as the validated identity (§6.6), and lists every entry that was skipped with the reason — `match` |
|
|
278
|
-
| `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
|
|
278
|
+
| `pki.x509` | Certificate issuance (RFC 5280 §4). `sign(spec, issuer, opts)` builds and signs a certificate from a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object. `randomSerial()` draws the same 20-octet CSPRNG serial the signer uses when the spec names none, so a serial can be reserved or logged before the certificate is issued. `parseDn(dn)` reads a distinguished-name string into `{ rdns, dn, bytes }`, the inverse of the `dn` every parsed name carries, with `bytes` being the raw Name DER `spec.subject` and `spec.issuer` take. A name string that opens as a distinguished name is refused rather than certified as one commonName holding the whole text, so `subject: "CN=Example CA, O=Example"` names the two forms that do work instead of issuing `CN=CN=Example CA\, O=Example`; a common name that contains an `=` further along, such as `Example CA (v=2)`, is unaffected. The `issuer` is a key alone (self-signed: issuer equals subject, signed with that key), a name plus public key plus key, or an issuing certificate plus key. The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI derived by SHA-1 of the subject key; the AKI optionally naming the issuing certificate by name and serial; both emitted without being named, the SKI on every certificate and the AKI on every certificate that is not self-signed, which is what RFC 5280 §4.2.1.1 and §4.2.1.2 place on the issuing CA, and `false` declines each only where the RFC allows the omission), subject alternative names, certificate policies with their RFC 5280 section 4.2.1.4 qualifiers (a CPS pointer and a user notice), name constraints, authority information access, CRL distribution points, the freshest CRL, issuer alternative names, the policy machinery path validation acts on (policy constraints, inhibit anyPolicy, policy mappings), the RFC 3739 qualified-certificate statements including the ETSI EN 319 412-5 set, the RFC 6962 certificate-transparency pair (the precertificate poison and the embedded SCT list), the Active Directory Certificate Services enrollment extensions (certificate template, legacy template name, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, the RFC 6960 OCSP no-check responder marker, and the RFC 7633 TLS Feature extension that declares must-staple, whose values are TLS ExtensionType numbers or the registry names for them, from the spec. That is every extension the shared certificate-extension decoders read, so any certificate `pki.lint.certificate` and `pki.inspect` can read in full the toolkit can also issue; any other extension is taken as pre-encoded DER. `extensions.nameConstraints` takes `{ permitted, excluded }` lists of GeneralName-form objects, restricting what the CA being issued may itself issue; each base is held to the rule for a constraint base, and the extension is emitted critical as §4.2.1.10 requires. It derives the version from the field set and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Before the signing key is reached it lints the certificate the spec describes against the RFC 5280 profile and refuses an error-severity finding with `x509/profile-violation` naming the rule and its clause, so a non-conforming certificate is never signed rather than signed and discarded; `opts.profile` names a further profile from `pki.lint.profiles()` to hold it to at build time, and `opts.profile: "none"` runs no rules and emits what the spec describes. `pki.csr.sign` and `pki.crl.sign` carry the same gate against RFC 2986 and the RFC 5280 §5 profile. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`. Every arm is independently verified by OpenSSL. `extension(name, value, opts)` encodes one Extension DER from the same plain value form, for the pre-encoded array form on the certificate, CSR, and CRL signers: the criticality follows what the signer emits, `opts.critical` reaches only the extensions whose criticality is the issuer's choice, and an unregistered dotted OID takes its extnValue as bytes. Parsing stays at `pki.schema.x509.parse` — `sign`, `randomSerial`, `extension`, `parseDn` |
|
|
279
279
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985). `sign(spec, key, opts)` builds and signs a `CertificationRequest` from a `spec` of subject (which may be empty), the public key being certified, an optional `extensionRequest` carrying the requested v3 extensions a CA copies into the issued certificate (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, the RFC 3739 qualified-certificate statements, the Microsoft certificate template and application policies, subject information access, subject directory attributes, or an array of pre-encoded Extension DER; an extension the issuing CA assigns, such as the authority key identifier or the OCSP no-check marker, is refused by name rather than requested), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned, which is what `openssl req -verify` checks. The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. `verify(request)` asks the same question of a request someone else produced, over its exact signed `certificationRequestInfo` bytes under the `subjectPKInfo` inside them, through the one path-validation signature engine. It answers `{ verified, subject, subjectPublicKeyInfo, attributes, certificationRequestInfoBytes }`, all re-derived from the signed bytes, so a CA issues from the fields the signature covers rather than from the object it passed in. A `verified: true` says the producer held that private key and that the subject and requested extensions are the ones they signed; who they are is the enrollment protocol's question, not this one. Parsing stays at `pki.schema.csr.parse` — `sign`, `verify` |
|
|
280
280
|
| `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest; a `baseCertificateID` names its issuer as one non-empty directoryName, which sec. 4.2.2 requires in the same words sec. 4.2.3 uses for the AC issuer, and `pki.schema.attrcert.parse` refuses any other shape), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, cRLDistributionPoints, authorityInfoAccess, or pre-encoded Extension DER) each with its RFC 5755 criticality. The authorityKeyIdentifier is emitted without being named (sec. 4.3.3) and holds the AA certificate's subject key identifier; both extension forms are held to the rules sec. 4.3 and 4.4 place on the issuer (an audit identity of 1 to 20 octets, one Targets element and no targetCert, an OCSP location that is an HTTP URL, one distribution point naming a single DN or HTTP / LDAP URL, a role name that is a URI). An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. `verify(ac, issuer, opts)` is the receiving side, performing the RFC 5755 sec. 5 checks an attribute certificate and a named issuer can settle between them: the signature over the exact `AttributeCertificateInfo` bytes through the one path-validation signature engine, the AC naming the issuer this verifier trusts (compared as a distinguished name), the evaluation instant lying within the validity with equality at either bound succeeding as the section states, the sec. 4.3.2 targeting rule, and rejection of any critical extension this verb does not process (section 5 defines support as parsing the value AND rejecting where the value would reject, so `aaControls` and `acProxying`, whose constraints it does not evaluate, are refused when critical). It answers `{ verified, signatureValid, validityChecked, targetingChecked, holderBindingChecked, issuerPathChecked, holder, issuer, attributes, extensions, notBefore, notAfter, serialNumberHex, reason }` with the fields re-derived from the signed bytes; the holder's own certificate chain and the AC issuer's chain need certificates the verb is not given, so those slots report `false` rather than letting their absence read as a pass. Parsing stays at `pki.schema.attrcert.parse` — `sign`, `verify` |
|
|
281
281
|
| `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. A key that cannot sign, which is where an ML-KEM enrollment sits, takes a `POPOPrivKey` proof instead (RFC 4211 sec. 4.2, sec. 4.3): `pop.type` of `keyEncipherment` or `keyAgreement` with `pop.method: 'subsequentMessage'` declares whether an encrypted certificate (`encrCert`) or a challenge (`challengeResp`) completes the proof over the enrolling protocol's next exchange, and `pop.method: 'encryptedKey'` encloses the requester's private key for the CA in a CMS `EnvelopedData` under `id-ct-encKeyWithID`, requiring `pop.privateKey`, `pop.recipients`, the `pop.identifier` sec. 4.2.1 makes mandatory, and `pop.archive: true` because RFC 9810 sec. 5.2.8.3.1 permits the method only where archival is intended. The enclosed key must be the private half of `certTemplate.publicKey`, or the proof demonstrates possession of a key the request never asked to have certified. The `thisMessage` and `dhMAC` alternatives the specification deprecates are refused with their successors named. `pop.method: 'agreeMAC'` completes the four: `pop.key` is the requester's finite-field Diffie-Hellman private key, as a PKCS#8 DER, a PEM `PRIVATE KEY` block or a `KeyObject`, and `pop.caCert` the authority's certificate it already holds; the two agree a static secret (RFC 2875 sec. 3, obsoleted by RFC 6955), and the key derived from it MACs the DER `certReq` under `id-dhPop-static-HMAC-SHA1`. Either key may name its group in the PKCS#3 or the X9.42 encoding. `pop.key` must be the private half of `certTemplate.publicKey`, and the template must name both a subject and a public key. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. `verifyPop(messages)` is the receiving side: for each `CertReqMsg` it verifies the `POPOSigningKey` signature over the bytes RFC 4211 sec. 4.1 names — the DER of `poposkInput` when present, of `certReq` when not — through the one path-validation signature engine, and answers `{ verified, messages: [{ verified, method, cryptographicallyVerified, certReqId, subject, subjectBound, publicKey, reason }] }` with the fields re-derived from the message's own bytes. Each verdict reports only what its preimage covers: `publicKey` is the key possession was proven for, and `subject` is the requested name when the `certReq` was signed. A `poposkInput` preimage covers the key and the sender alone, so any subject beside it is unsigned and is withheld with `subjectBound: false`. A `raVerified` proof is an RA's out-of-band assertion and a `keyEncipherment` or `keyAgreement` proof completes over a later exchange, so each is reported unverified with its `method` named instead of guessed. Parsing stays at `pki.schema.crmf.parse` — `build`, `verifyPop` |
|
|
@@ -284,8 +284,8 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
284
284
|
| `pki.key` | Key-material lifecycle (RFC 5958 / RFC 8018). `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 with AES-CBC-Pad), where `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`. The plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo`, re-validated through `pki.schema.pkcs8.parse`: only PBES2 / PBKDF2 / AES-CBC is accepted (PBES1, PBMAC1, and scrypt are refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), and a malformed parameter set or wrong-length IV is a distinct typed error. Because a MAC-less PBES2-CBC decrypt must not become a padding oracle (RFC 8018 §8), a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` and `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo. The key may come from the platform's WebCrypto or from a separately installed copy of this toolkit, and is exported through whichever holds its material; a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason. Encoding is delegated to WebCrypto, so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters, and an ambiguous RSA or EC import requires `opts.algorithm`. `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards and Montgomery curves, and the FIPS post-quantum ML-DSA and ML-KEM; `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM, with a typed `KeyError` on failure. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt`, `decrypt`, `export`, `import`, `generate`, `publicFromPrivate` |
|
|
285
285
|
| `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579, the latter obsoleted by RFC 9879). `build(spec, opts)` assembles a store from the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key, shroudedKey, cert, crl, secret, or nested `safeContents` bags. Keys and certs are validated before wrapping, a key paired with a certificate (the `{ key, cert }` form, or bags sharing a `localKeyId`) must be the private half of that certificate's public key, and `friendlyName` (BMPString) and `localKeyId` are single-value. Integrity is a classic Appendix B HMAC (the default, for maximum interoperability) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`) over SHA-256/384/512, with shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 — which is what OpenSSL and NSS consume, so a file it emits opens in both, cross-checked bidirectionally. The MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity (`opts.integrity.mode: "public-key"`) wraps the AuthenticatedSafe in a CMS SignedData instead of a MAC, signed by any `pki.cms.sign` signer and carrying no MacData (§4); privacy stays independent, so `password` still PBES2-encrypts the bags. Public-key privacy wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData`, never GCM) encrypted to recipient public keys through the `pki.cms.encrypt` recipient model, via per-safe `recipients` or the `opts.recipientCerts` convenience, restricted to certificate recipients (RSA-OAEP, ECDH, X25519, X448, ML-KEM) since a password or KEK recipient could not be reopened by `open`. All four integrity-by-privacy combinations are permitted (§3.1). `open(pfx, password, opts)` reads a store back: it verifies the MAC first, so a wrong password is the MAC verdict rather than a decrypt error, then PBES2-decrypts every privacy safe and shrouded key bag and returns `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` — keys as re-validated PKCS#8 DER, certs, CRLs and secrets as raw DER, all with `friendlyName` and `localKeyId`, nested safes recursively. A MAC-less store is refused unless `opts.allowUnauthenticated`. A public-key-integrity store is verified through its CMS SignedData signature first (`pkcs12/signature-invalid` on failure), with the signer surfaced in `signers` but never trust-chained, which remains the caller's `pki.path.validate` step. A legacy-PBE store's Appendix C 3DES and RC2 bags are decrypted, RC2 through an in-tree RFC 2268 cipher, so an `openssl pkcs12 -legacy` or NSS store opens; the legacy RC4 schemes are refused. An `id-envelopedData` safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` when absent), every recipient-side fault and every post-integrity decrypt failure collapsing to the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`. It reads what OpenSSL and NSS produce. Returns DER or a PEM `PKCS12`, with a typed `Pkcs12Error` on failure. Parsing stays at `pki.schema.pkcs12.parse` — `build`, `verifyMac`, `open` |
|
|
286
286
|
| `pki.pkcs11` | PKCS #11 URIs (RFC 7512), the syntax an operator names a token and an object on it with instead of a file path. `parseUri(uri)` reads one into `{ path, vendorPath, query, vendorQuery }`, each a prototype-less record: the path carries `token`, `manufacturer`, `serial`, `model`, the three `library-*` and three `slot-*` attributes, `object`, `type` and `id`, and the query carries `pin-source`, `pin-value`, `module-name` and `module-path`, with a vendor attribute in either component kept under its own name. A vendor query attribute carries a list of its values, which §2.4 permits to repeat; a vendor path attribute carries one value. `id` is a `Buffer`, since the attribute names bytes rather than text, and `slot-id` and `library-version` are numbers. `formatUri(record)` writes the record back, percent-encoding each value to what its own component admits unencoded and encoding `id` whole with uppercase hexadecimal, so two URIs naming one identifier compare equal. Parsing follows the §2.3 / §2.4 ABNF rather than a general URI reader and is fail-closed: a character the component admits only percent-encoded, a truncated or non-hexadecimal escape, a value that is not valid UTF-8, an attribute repeated where the RFC admits it once, a `type` outside the five the RFC enumerates, a URI carrying both `pin-source` and `pin-value`, and a relative `module-path` are each refused with their own `pkcs11/*` code. Syntax only: nothing here loads a PKCS #11 module or talks to a token — `parseUri`, `formatUri` |
|
|
287
|
-
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA pairing ML-DSA with a traditional RSA, ECDSA, or EdDSA key (accepted only when both components verify, draft-ietf-lamps-cms-composite-sigs). It builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. The content may also be an async iterable of byte chunks, which signs a large detached payload without holding it in memory: the payload is hashed incrementally under every signer's digest algorithm in one pass, and `verify` accepts the same streamed form as its `content` option. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: with signed attributes present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with the primary bytes preserved so it still verifies — attached as the id-countersignature unsigned attribute; `verify` returns each countersignature's verdict under `signers[i].countersignatures` and every unsigned attribute under `signers[i].unsignedAttrs`, surfaced unauthenticated. The CAdES signature timestamp has verbs of its own: `timestampImprint(der, opts)` returns `{ imprint, signerIndex }`, where `imprint` is the `{ hashAlgorithm, hashedMessage }` argument `pki.tsp.request` takes, the hash of a signer's signature value with neither tag nor length (ETSI EN 319 122-1 clause 5.3), `attachTimestamp(der, token, opts)` splices the returned RFC 3161 token in as the signature-time-stamp unsigned attribute and refuses one whose imprint covers other bytes, and `verify` reports a row per attached token under `signers[i].signatureTimeStamps`, each carrying the generation time, the authority, and what the authority's chain established. A row is refused when its time precedes the signature's own `signing-time` or falls after the signing certificate's `notAfter`. `signers[i].cadesBaseline` reports which CAdES baseline requirements the signature meets. The RFC 6211 `CMSAlgorithmProtection` attribute is emitted on request by `sign`, `countersign` and `authenticate` (`algorithmProtection: true`, or `"registry"` for the IANA arc identifier) and always compared where present: a copy of the digest and signature or MAC algorithm identifiers, placed where the signature or the MAC covers them, with a disagreement refused as `cms/algorithm-protection-mismatch` and the result reporting `algorithmProtection` per signer. It matters most on an `AuthenticatedData`, whose MAC input is the authenticated attributes alone, leaving the outer digest and MAC algorithm fields covered by nothing. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is never emitted), key agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key wrap, password (PBKDF2 with the PWRI-KEK of RFC 3211, carried forward by RFC 3370), or the post-quantum ML-KEM (and composite ML-KEM, draft-lamps-pq-composite-kem) KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. The content may also be an async iterable of byte chunks, streamed through the AES-GCM or AES-CBC content cipher so a large plaintext is encrypted without being buffered whole; the ciphertext is assembled before the definite-length encryptedContent is emitted, byte-for-byte the buffered form. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher, EFAIL, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. With `opts.stream: true` the recovered `content` is an async iterable of plaintext chunks instead of a single Buffer, for a payload too large to hold whole; the authenticated modes (AuthEnvelopedData, AuthenticatedData, AES-GCM) verify integrity before the iterable yields, so a forged message throws before any plaintext is exposed, while a CBC EnvelopedData or EncryptedData yields the plaintext as the cipher produces it. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and message-digest) re-tagged to the EXPLICIT SET OF (§9.2), or the content octets directly; `decrypt` recovers the MAC key, recomputes the MAC and independently the message-digest (§9.3), and releases the content only after both pass, with every secret-dependent failure collapsing to the uniform `cms/decrypt-failed`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). `certsOnly(certs, opts?)` builds a certs-only certificate-management message (RFC 8551 §3.8): a degenerate SignedData with an empty signerInfos and no eContent that conveys certificates and CRLs without signing anything, `parseCertsOnly(input, opts?)` reads one back to its raw certificate and CRL DER (a CRL-only message is valid here, unlike the cert-required RFC 5272 form that `pki.est.parseCertsOnly` reads), and `isCertsOnly(input)` recognizes one structurally. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress`, `certsOnly`, `parseCertsOnly`, `isCertsOnly` |
|
|
288
|
-
| `pki.smime` | S/MIME message assembly, verification, encryption, and compression over the CMS layer (RFC 8551). `sign(content, signers, opts)` wraps a MIME entity in either form: `multipart/signed`, where the content stays readable in any MUA and a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`, or `application/pkcs7-mime; smime-type=signed-data`, where the whole entity is a base64 CMS SignedData. The signed bytes are the entity's §3.1.1 canonical form with CRLF line endings, and `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies while a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one, as `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat). The `smime-type` is derived from the CMS body rather than the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt`, so it is algorithm-agnostic: any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through. As with `cms.verify`, `verify` returns the per-signer verdict plus the recovered content, and `valid` and `trusted` are separate claims: `valid` says the signature is sound under a certificate the message carried, `trusted` says every signer chained to a root named in `opts.trustAnchors`. Anchoring here is validated for email at both ends of the chain — the signer certificate must carry `emailProtection` (RFC 8550 §4.4.4) and the anchor's own trust metadata must permit that purpose, since a root can be distrusted for email while remaining a good TLS root. `sign` refuses a signer certificate a conforming reader would reject: a `keyUsage` extension without `digitalSignature` or `nonRepudiation` (RFC 8550 §4.4.2), or an `extendedKeyUsage` extension without `emailProtection` or `anyExtendedKeyUsage` (§4.4.4); a certificate carrying neither extension signs. Override either with `requiredEku` / `checkPurpose`. `compress(content, opts)` and `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (§3.6, RFC 3274), a size transform with no integrity or confidentiality (§2.4.5), bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Header protection (RFC 9788): `sign` and `encrypt` take `opts.protectHeaders`, which inlines the caller's `opts.headers` on the Cryptographic Payload root (its Content-Type gaining `hp="clear"` when signed or `hp="cipher"` when encrypted) so the CMS signature or encryption covers them, defeating a transport that rewrites or reads Subject, From, and the rest. `verify` and `decrypt` surface the authenticated inner set as `protectedHeaders` plus `headerProtection { present, mode, fromMismatch, confidential, legacy }`, so a tampered outer header cannot alter it and `fromMismatch` flags an outer From that disagrees. Encryption applies a Header Confidentiality Policy: the default `hcp_baseline` obscures the outer Subject to `[...]` and removes Comments and Keywords, so the real values live only in the ciphertext, and `decrypt` recovers them. Every emitted header routes through a fail-closed injection guard that rejects a CR, LF, or NUL value and a non-ftext name, and a malformed or contradictory `hp` wrap fails closed as `smime/bad-header-protection` rather than silently downgrading. The CMS crypto is unchanged. Inbound legacy RFC 8551 header protection is recognized opt-in: `verify` and `decrypt` with `opts.legacyHeaderProtection` detect a legacy `message/rfc822`-wrapped payload by the RFC 9788 §4.10.1 four-condition identification and surface the inner headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array retaining legally repeated fields such as `Received`. Those never appear in `protectedHeaders` and never set `present: true`. Because a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, this is an explicit heuristic (§4.10.2, "no strong end-to-end guarantees"): a caller keying trust off `present` or `protectedHeaders` is never misled, and only one that explicitly reads `headerProtection.legacy.headers` and cross-checks `legacy.fromMismatch` consumes it. It is off by default, and a nested crypto layer, an inner `hp=`, a non-`message/rfc822` payload, or a duplicate Content-Type reports `legacy: null`. Bidirectionally interoperable with `openssl smime` and `openssl cms`. `buildCertsOnly(certs, opts?)` wraps a `pki.cms.certsOnly` certificate-management message in one `application/pkcs7-mime; smime-type=certs-only; name=smime.p7c` entity, read back by OpenSSL's `pkcs7 -print_certs`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress`, `buildCertsOnly` |
|
|
287
|
+
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA pairing ML-DSA with a traditional RSA, ECDSA, or EdDSA key (accepted only when both components verify, draft-ietf-lamps-cms-composite-sigs). It builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. The content may also be an async iterable of byte chunks, which signs a large detached payload without holding it in memory: the payload is hashed incrementally under every signer's digest algorithm in one pass, and `verify` accepts the same streamed form as its `content` option. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. A signer descriptor is held to the fields the verb reads, by `sign` and `countersign` alike and by `pki.smime.sign` through them: `{ cert, key, digestAlgoritm: "sha384" }` raises `cms/bad-input` naming the field rather than signing under the default digest, which is what the same misspelling in `opts` already did. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: with signed attributes present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with the primary bytes preserved so it still verifies — attached as the id-countersignature unsigned attribute; `verify` returns each countersignature's verdict under `signers[i].countersignatures` and every unsigned attribute under `signers[i].unsignedAttrs`, surfaced unauthenticated. The CAdES signature timestamp has verbs of its own: `timestampImprint(der, opts)` returns `{ imprint, signerIndex }`, where `imprint` is the `{ hashAlgorithm, hashedMessage }` argument `pki.tsp.request` takes, the hash of a signer's signature value with neither tag nor length (ETSI EN 319 122-1 clause 5.3), `attachTimestamp(der, token, opts)` splices the returned RFC 3161 token in as the signature-time-stamp unsigned attribute and refuses one whose imprint covers other bytes, and `verify` reports a row per attached token under `signers[i].signatureTimeStamps`, each carrying the generation time, the authority, and what the authority's chain established. A row is refused when its time precedes the signature's own `signing-time` or falls after the signing certificate's `notAfter`. `signers[i].cadesBaseline` reports which CAdES baseline requirements the signature meets. The RFC 6211 `CMSAlgorithmProtection` attribute is emitted on request by `sign`, `countersign` and `authenticate` (`algorithmProtection: true`, or `"registry"` for the IANA arc identifier) and always compared where present: a copy of the digest and signature or MAC algorithm identifiers, placed where the signature or the MAC covers them, with a disagreement refused as `cms/algorithm-protection-mismatch` and the result reporting `algorithmProtection` per signer. It matters most on an `AuthenticatedData`, whose MAC input is the authenticated attributes alone, leaving the outer digest and MAC algorithm fields covered by nothing. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is never emitted), key agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key wrap, password (PBKDF2 with the PWRI-KEK of RFC 3211, carried forward by RFC 3370), or the post-quantum ML-KEM (and composite ML-KEM, draft-lamps-pq-composite-kem) KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. The content may also be an async iterable of byte chunks, streamed through the AES-GCM or AES-CBC content cipher so a large plaintext is encrypted without being buffered whole; the ciphertext is assembled before the definite-length encryptedContent is emitted, byte-for-byte the buffered form. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher, EFAIL, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. With `opts.stream: true` the recovered `content` is an async iterable of plaintext chunks instead of a single Buffer, for a payload too large to hold whole; the authenticated modes (AuthEnvelopedData, AuthenticatedData, AES-GCM) verify integrity before the iterable yields, so a forged message throws before any plaintext is exposed, while a CBC EnvelopedData or EncryptedData yields the plaintext as the cipher produces it. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and message-digest) re-tagged to the EXPLICIT SET OF (§9.2), or the content octets directly; `decrypt` recovers the MAC key, recomputes the MAC and independently the message-digest (§9.3), and releases the content only after both pass, with every secret-dependent failure collapsing to the uniform `cms/decrypt-failed`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). `certsOnly(certs, opts?)` builds a certs-only certificate-management message (RFC 8551 §3.8): a degenerate SignedData with an empty signerInfos and no eContent that conveys certificates and CRLs without signing anything, `parseCertsOnly(input, opts?)` reads one back to its raw certificate and CRL DER (a CRL-only message is valid here, unlike the cert-required RFC 5272 form that `pki.est.parseCertsOnly` reads), and `isCertsOnly(input)` recognizes one structurally. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress`, `certsOnly`, `parseCertsOnly`, `isCertsOnly` |
|
|
288
|
+
| `pki.smime` | S/MIME message assembly, verification, encryption, and compression over the CMS layer (RFC 8551). `sign(content, signers, opts)` wraps a MIME entity in either form: `multipart/signed`, where the content stays readable in any MUA and a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`, or `application/pkcs7-mime; smime-type=signed-data`, where the whole entity is a base64 CMS SignedData. The signed bytes are the entity's §3.1.1 canonical form with CRLF line endings, and `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies while a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one, as `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat). The `smime-type` is derived from the CMS body rather than the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt`, so it is algorithm-agnostic: any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through. As with `cms.verify`, `verify` returns the per-signer verdict plus the recovered content, and `valid` and `trusted` are separate claims: `valid` says the signature is sound under a certificate the message carried, `trusted` says every signer chained to a root named in `opts.trustAnchors`. Anchoring here is validated for email at both ends of the chain — the signer certificate must carry `emailProtection` (RFC 8550 §4.4.4) and the anchor's own trust metadata must permit that purpose, since a root can be distrusted for email while remaining a good TLS root. `sign` refuses a signer certificate a conforming reader would reject: a `keyUsage` extension without `digitalSignature` or `nonRepudiation` (RFC 8550 §4.4.2), or an `extendedKeyUsage` extension without `emailProtection` or `anyExtendedKeyUsage` (§4.4.4); a certificate carrying neither extension signs. Override either with `requiredEku` / `checkPurpose`. `compress(content, opts)` and `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (§3.6, RFC 3274), a size transform with no integrity or confidentiality (§2.4.5), bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Header protection (RFC 9788): `sign` and `encrypt` take `opts.protectHeaders`, which inlines the caller's `opts.headers` on the Cryptographic Payload root (its Content-Type gaining `hp="clear"` when signed or `hp="cipher"` when encrypted) so the CMS signature or encryption covers them, defeating a transport that rewrites or reads Subject, From, and the rest. `verify` and `decrypt` surface the authenticated inner set as `protectedHeaders` plus `headerProtection { present, mode, fromMismatch, confidential, legacy }`, so a tampered outer header cannot alter it and `fromMismatch` flags an outer From that disagrees. Encryption applies a Header Confidentiality Policy: the default `hcp_baseline` obscures the outer Subject to `[...]` and removes Comments and Keywords, so the real values live only in the ciphertext, and `decrypt` recovers them. Every emitted header routes through a fail-closed injection guard that rejects a CR, LF, or NUL value and a non-ftext name, and a malformed or contradictory `hp` wrap fails closed as `smime/bad-header-protection` rather than silently downgrading. The CMS crypto is unchanged. Inbound legacy RFC 8551 header protection is recognized opt-in: `verify` and `decrypt` with `opts.legacyHeaderProtection` detect a legacy `message/rfc822`-wrapped payload by the RFC 9788 §4.10.1 four-condition identification and surface the inner headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array retaining legally repeated fields such as `Received`. Those never appear in `protectedHeaders` and never set `present: true`. Because a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, this is an explicit heuristic (§4.10.2, "no strong end-to-end guarantees"): a caller keying trust off `present` or `protectedHeaders` is never misled, and only one that explicitly reads `headerProtection.legacy.headers` and cross-checks `legacy.fromMismatch` consumes it. It is off by default, and a nested crypto layer, an inner `hp=`, a non-`message/rfc822` payload, or a duplicate Content-Type reports `legacy: null`. Bidirectionally interoperable with `openssl smime` and `openssl cms`. `buildCertsOnly(certs, opts?)` wraps a `pki.cms.certsOnly` certificate-management message in one `application/pkcs7-mime; smime-type=certs-only; name=smime.p7c` entity, read back by OpenSSL's `pkcs7 -print_certs`. An entity whose Content-Type names one parameter twice, or whose header area names one field twice, is refused rather than resolved by position: RFC 2045 §5.1 and RFC 5322 §3.6 each give one occurrence, so the two readings disagree about which octets the signature covers. One entity is bounded at 1024 body parts, 1024 header lines and 256 Content-Type parameters (`MIME_MAX_PARTS`, `MIME_MAX_HEADERS`, `MIME_MAX_PARAMS`), counted as it is scanned, and the record a verdict is read with holds a copy of the entity rather than a view of the caller's buffer. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress`, `buildCertsOnly` |
|
|
289
289
|
| `pki.tsp` | Time-Stamp Protocol (RFC 3161). `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData over `pki.cms.sign` whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` with optional accuracy, nonce, and ordering, plus the §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). The TSA certificate is held to §2.3 before signing (one critical extendedKeyUsage naming timeStamping alone) with the codes `verify` answers, `genTime` keeps its milliseconds as the §2.4.2 fraction of a second, and an accuracy naming no field is refused. `request` and `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq); `response` and `parseResponse` handle the TimeStampResp a TSA returns, either a granted status wrapping a token or a rejection with PKIStatus and failure info, with the §2.4.2 status-to-token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and, with a trust anchor supplied, full certification-path validation of the TSA certificate at the token's `genTime` — judged under the `timeStamping` purpose, so an anchor's own per-purpose trust metadata reaches the decision rather than sitting inert. It returns `{ valid, trusted, genTime, serialNumber, tstInfo, ... }`, where `trusted` is the separate claim that the authority chained to an anchor you named: without one there is nothing to chain to and it is `false`, which is the distinction a timestamp re-read years later turns on — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
290
290
|
| `pki.ocsp` | Online Certificate Status Protocol (RFC 6960), both the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs, with the CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile (obsoleted by RFC 9919) or under SHA-2, plus an optional RFC 9654 nonce (32 to 128 octets, the requester floor that section sets) and an optional requestor signature, under which the signer certificate's subject is the requestorName unless one is stated (RFC 6960 §4.1.2). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER, from the issuing CA directly or a delegated responder, under any `pki.cms.sign` key including the post-quantum ML-DSA and SLH-DSA sets, with `good`, `revoked` (reason and time), or `unknown` per-certificate status, and per-response `singleExtensions` naming the §4.4.4 archive cutoff and the §4.4.2 CRL reference as fields, each read back by name by the parser. The signer refuses what its own lint would grade an error: a pre-encoded singleExtension that RFC 6960 places in requests or in responseExtensions, a CRL entry extension carried at the wrong criticality, and CRL references or entry extensions on a non-issued response, which itself places the §4.4.8 extended revoked definition in responseExtensions without being asked. `buildErrorResponse(status)` produces the unsigned §2.3 error (`tryLater`, `unauthorized`, and the rest). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA, or a CA-issued delegate bearing id-kp-OCSPSigning and id-pkix-ocsp-nocheck and passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency against `thisUpdate` and `nextUpdate`, and the request-nonce echo. It returns `{ status: "good" / "revoked" / "unknown", ... }` and never silently accepts. `verifyRequest(request, opts)` is the responder-side inverse of `buildRequest`: it verifies a client's signed request (RFC 6960 §4.1.1) under the requestor certificate the request carries, trying each embedded certificate since the field is unordered, and reports `signed`, `signatureValid`, every certificate whose key verified the signature and whose subject is the `requestorName` the request states (`signerCerts`, so a key carried under an expired certificate beside its renewal is not hidden behind ordering, while a same-key certificate under another name is not the requestor) with the first one's decoded subject, the full embedded certificate bag (`certs`, including intermediates for path building), and the request's CertIDs; the name check is reported as `requestorNamed` (RFC 6960 §4.1.2) and `valid` requires it, a request no verifying certificate names falls back to listing the verifying ones with `valid: false`, and an unsigned request is reported with `signed: false` rather than refused. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify`, `verifyRequest` |
|
|
291
291
|
| `pki.ct` | Certificate Transparency (RFC 6962). `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries, a TLS-presentation-language payload inside the §3.3 double DER wrap, into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature. `reconstructSignedData` rebuilds the exact `digitally-signed` preimage, and `verifySct` verifies an SCT signature against a log's public key, routing an ECDSA signature through the strict DER-conformance gate and verifying through the crypto engine, resolving true or false and throwing a typed error on a structural fault. On the producing side, `encodeSctList` builds the extension value byte for byte as the exact inverse of `parseSctList`, and `signSct` performs a log's signing step. For trust, `parseLogList` ingests the CT log-list JSON into constraint-carrying trusted logs, recomputing each log's id as SHA-256 of its key and refusing a disagreeing id (a swapped key, §3.2) and decoding the state and temporal-interval constraints; `verifySctWithLogList` resolves the log key from an SCT's log id, enforces the state (usable, qualified, and readonly trusted; retired only before retirement; pending and rejected refused) and the temporal-interval window, then delegates the signature check to `verifySct`. `verifyLogListSignature(json, signature, publicKey)` verifies the detached `log_list.sig` over the raw log-list bytes against a caller-pinned signer key (RSASSA-PKCS1-v1.5/SHA-256 and an EC P-256 arm, with forgeable-key defenses failing closed), cross-checked against `openssl dgst`. `fetchLogList(opts)` turns that chain into a live client: it GETs the `log_list.json` and its detached `log_list.sig` over `pki.transport`, verifies the detached signature over the raw fetched bytes against a caller-pinned distributor key before parsing, so an unverified document is never parsed, read, cached, or surfaced, then ingests the same bytes through `parseLogList` and returns the trusted-log set plus the surfaced `version` and `timestamp`. Above the per-SCT layer, The RFC 6962 sec. 4 log client is shipped: `getSth` fetches a signed tree head and verifies it under the caller-pinned log key, returning nothing when it does not verify, since every later check is measured against that root; `sthSignedData` builds the 50-byte sec. 3.5 preimage and `verifySth` checks a head held offline. `getProofByHash` and `getSthConsistency` fetch the audit and consistency proofs and fold them against tree heads a log signed, taking the query sizes from those heads so a size and a head cannot disagree. `getEntries` and `getRoots` fetch what the specification leaves unsigned and say so, with a short entry answer reported as truncation rather than a fault. `addChain` and `addPreChain` submit a chain and verify the SCT the log returns against the chain it was issued for. `verifySctList` renders a certificate-level verdict over every SCT a certificate carries (§3.3), reporting how many distinct trusted logs verified an SCT (so a duplicated SCT cannot inflate the count) and from how many distinct trusted operators against a caller's CT policy (`minScts` and `minOperators`, both defaulting to the RFC floor of one), where a policy shortfall is a verdict and a per-SCT failure is a recorded row rather than a thrown call; `x509CertEntry` reconstructs the precertificate log entry from a final certificate by removing the SCT-list extension from its TBSCertificate as byte surgery, so its embedded SCTs verify end to end (§3.2). The toolkit bakes in no vendor URL or key, TLS trust is explicit with `rejectUnauthorized` always on, each response is size-capped before the trust chain, and the transport is injectable so the whole path is testable offline — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct`, `parseLogList`, `verifySctWithLogList`, `verifySctList`, `x509CertEntry`, `verifyLogListSignature`, `fetchLogList` |
|
|
@@ -294,7 +294,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
294
294
|
| `pki.tuf` | The Update Framework metadata layer: the signed documents a repository publishes to say which keys may sign what, and the rules a client applies before believing any of it. This is the trust root a sigstore verifier would otherwise pin by hand. TUF signs the canonical JSON form of a document, so `canonicalJson` is the wire format rather than a convenience: keys sort by code point, no whitespace is emitted, and only backslash and quote are escaped, so a literal newline stays a newline where `JSON.stringify` would write an escape and sign different bytes. A float is refused rather than rounded, because the reference encoder cannot write one. `keyId` derives a key's identifier as the hex SHA-256 of its own canonical form, and `verifySignatures` recomputes it for every key a role names, so a document cannot file one key under another's identifier; a threshold counts one verified signature per distinct identifier, so repetition cannot meet it. `updateRoot` walks the root chain, requiring each new root to be signed by a threshold of the previous root's keys AND a threshold of its own and to carry exactly the next version, which is what makes a key rotation safe and a skipped intermediate impossible. `checkExpiry` is the freeze-attack check, judged at a caller instant rather than the system clock. Verifying only: nothing here fetches — `canonicalJson`, `keyId`, `parseMetadata`, `checkExpiry`, `verifySignatures`, `updateRoot` |
|
|
295
295
|
| `pki.possession` | Statement of possession (RFC 9883): how a key that cannot sign gets certified. PKCS#10 proves possession of the key being certified by signing the request with it, so an ML-KEM, X25519 or X448 key could not be certified through a certification request at all. RFC 9883 signs the request with a different key the requester already holds a certificate for, and names that certificate in an attribute. `pki.csr.sign` takes `spec.privateKeyPossessionStatement` and signs with the key given, which is the one case where it accepts a signing key that is not the subject's and a subject key that cannot sign; both stay refused for a request that declares no statement. `includeCertificate: false` emits the compact form the RFC allows, where the CA looks the certificate up. `pki.crmf.build` takes the same structure as `controls.statementOfPossession`, which the RFC gives the attribute's own OID. `pki.possession.verifyRequest` is the CA's side and enforces the two MUSTs: the request's signature is validated with the public key from the signature certificate, and that certificate's certification path is validated, so `opts.trustAnchors` is required and the verb throws without it rather than returning a verdict that skipped a MUST. The two name comparisons the RFC states as SHOULDs are reported and not enforced, because each ends in a question about certificate policy that no library can answer: `subjectMatches` and `subjectAltNamesMatch` carry the comparison and `valid` is `false` with a reason naming the decision. `requestsSignatureCertificate` reports the prohibition on using the attribute to obtain a signature certificate, which `pki.lint.csr` with `profile: "rfc9883"` grades as an error. A statement carrying a certificate its `signer` does not name is refused in both directions — `parse`, `verifyRequest` |
|
|
296
296
|
| `pki.altSig` | Alternative signatures (ITU-T X.509 (2019) clause 9.8), the shape widely called Catalyst: one certificate carrying two signatures, so a PKI migrating between algorithm sets does not have to reissue to every relying party at once. A relying party that has not migrated reads the native signature and ignores the extensions; one that has reads the alternative signature instead. `pki.x509.sign` takes `issuer.altKey` beside the issuing key and `extensions.subjectAltPublicKeyInfo` for the subject's alternative key, and emits all three extensions: clause 7.2.2 requires the alternative signature to be generated first, over the structure without it, and the native signature then to cover it, so the ordering is not the caller's to get right. `pki.crl.sign` takes `issuer.altKey` for the same mechanism on a CRL, which clause 7.10.3 gives the same procedure; `subjectAltPublicKeyInfo` is certificate-only. `signedData` builds the bytes that signature covers: the `PreTBSCertificate` of `draft-truskovsky-lamps-pq-hybrid-x509` section 4, which is the `tbsCertificate` without its `signature` field and without the `altSignatureValue` extension, and the `PreTBSCertList` of section 5 for a CRL. Those are the bytes Bouncy Castle emits and checks, so an alternative signature made here verifies there and one made there verifies here. Clause 7.2.2 requires a verifier to rebuild that encoding, and this is the one place the toolkit re-encodes what it parsed: every retained component keeps the bytes it had and only the two SEQUENCE headers whose lengths change are recomputed, so nothing is re-serialized from a decoded model. `verify` checks the signature under the issuer's alternative public key, which `subjectAltPublicKey` reads out of the issuer's own certificate; `SubjectAltPublicKeyInfo` has the same encoding as `SubjectPublicKeyInfo`, so that value is an SPKI and needs no conversion. The algorithm is not derived: the structure states it in `altSignatureAlgorithm`, and that stated algorithm is bound to the key through the engine path validation uses, so a key that does not match it resolves `false` rather than being read under some other algorithm; a structure stating no alternative signature throws instead, there being nothing to check. `pki.lint.certificate` with `profile: "x509-altsig"` grades the clause 9.8.3 pairing and the clause 7.2.2 full set as errors and a critical extension as a warning, criticality being stated only in a NOTE — `signedData`, `verify`, `subjectAltPublicKey` |
|
|
297
|
-
| `pki.relatedCert` | Related certificates (RFC 9763): the two structures that let one subject hold a classical certificate and a post-quantum one and have each say the other exists. `requestSignedData` builds the bytes a requester signs to prove it holds the certificate it names, which §3.1 fixes as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime` and nothing else; `locationInfo` travels in the attribute outside those bytes, so a verified proof says nothing about where the certificate can be fetched. `pki.csr.sign` takes the whole attribute as `relatedCertRequest` and encodes those two fields from that same function, so the bytes signed and the bytes emitted cannot drift apart; its `locationInfo` is a `SEQUENCE OF` IA5String URIs rather than one URI, and it keeps the order the requester wrote. `verifyRequest` checks the proof through the engine the certificate path uses, so the algorithm it derives is bound to the key. The key decides the algorithm, resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, so every algorithm those verbs sign with is reached, including an RSASSA-PSS key whose parameters belong to its identifier; the digest comes from the hash the certificate's signature OID indicates, with `digestAlgorithm` or a DER `signatureAlgorithm` naming either outright. A key whose algorithm fixes its own digest refuses `digestAlgorithm` rather than ignoring it, and a key that cannot sign is refused. A certificate other than the one `certID` names throws rather than returning `false`. `certificateHash` and `matchesCertificate` are the extension side, over the whole related certificate: the algorithm defaults as §4.1 directs, to the hash the related certificate's own signature OID indicates, and a certificate that indicates none throws rather than falling back to one the document does not name. `sha1` is refused. `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the certificate itself computes the digest rather than trusting a caller to name the algorithm it used; supplying both a `hashValue` and a certificate is refused, the two being able to disagree. The extension is emitted non-critical, which §4.1 asks at SHOULD NOT, and a critical one still parses: `pki.lint.certificate` with the `rfc9763` profile grades it, beside the MUST that it appear only in a chain's end-entity certificate — `requestSignedData`, `verifyRequest`, `certificateHash`, `matchesCertificate` |
|
|
297
|
+
| `pki.relatedCert` | Related certificates (RFC 9763): the two structures that let one subject hold a classical certificate and a post-quantum one and have each say the other exists. `requestSignedData` builds the bytes a requester signs to prove it holds the certificate it names, which §3.1 fixes as the DER `IssuerAndSerialNumber` followed by the DER `BinaryTime` and nothing else; `locationInfo` travels in the attribute outside those bytes, so a verified proof says nothing about where the certificate can be fetched. `pki.csr.sign` takes the whole attribute as `relatedCertRequest` and encodes those two fields from that same function, so the bytes signed and the bytes emitted cannot drift apart; its `locationInfo` is a `SEQUENCE OF` IA5String URIs rather than one URI, and it keeps the order the requester wrote. `verifyRequest` checks the proof through the engine the certificate path uses, so the algorithm it derives is bound to the key, and returns `{ valid, verified, signerMaySign, signatureAlgorithm }` with a `code` and `reason` when it did not pass. The two checks are reported apart because they fail for different reasons: `verified` is the proof's own signature, and `signerMaySign` is whether the named certificate's keyUsage authorizes that signature at all, which §4.2.1.3 of RFC 5280 decides. A key-establishment certificate passes it, RFC 9763 §3.1 having such a key sign once for proof of possession. The key decides the algorithm, resolved through the same resolver `pki.x509.sign` and `pki.csr.sign` use, so every algorithm those verbs sign with is reached, including an RSASSA-PSS key whose parameters belong to its identifier; the digest comes from the hash the certificate's signature OID indicates, with `digestAlgorithm` or a DER `signatureAlgorithm` naming either outright. A key whose algorithm fixes its own digest refuses `digestAlgorithm` rather than ignoring it, and a key that cannot sign is refused. A certificate other than the one `certID` names throws rather than returning `false`. `certificateHash` and `matchesCertificate` are the extension side, over the whole related certificate: the algorithm defaults as §4.1 directs, to the hash the related certificate's own signature OID indicates, and a certificate that indicates none throws rather than falling back to one the document does not name. `sha1` is refused. `pki.x509.sign` takes `extensions.relatedCertificate`, and the form handed the certificate itself computes the digest rather than trusting a caller to name the algorithm it used; supplying both a `hashValue` and a certificate is refused, the two being able to disagree. The extension is emitted non-critical, which §4.1 asks at SHOULD NOT, and a critical one still parses: `pki.lint.certificate` with the `rfc9763` profile grades it, beside the MUST that it appear only in a chain's end-entity certificate — `requestSignedData`, `verifyRequest`, `certificateHash`, `matchesCertificate` |
|
|
298
298
|
| `pki.trust` | Mozilla and CCADB trust-store ingestion. `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export, both into one constraint-carrying anchor shape: the per-purpose trust bits, where only `CKT_NSS_TRUSTED_DELEGATOR` grants, and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer and serial rather than adjacency and are cross-checked against the parsed DER, so metadata cannot attach to the wrong root. `anchor()` hands an entry to `pki.path.validate({ trustAnchors, checkPurpose })`, and `anchor(entry, { nameConstraints })` attaches the namespace a root program trusts a root for when that is narrower than the root certificate states, a `{ permitted, excluded }` pair of `{ tag, base }` subtrees that `validate` seeds as the RFC 5280 §6.1.1(h)(i) initial value, intersecting with each certificate's own constraints. `parseTrustAnchorList` reads the IETF's own interchange format for the same thing, an RFC 5914 `TrustAnchorList`, into the same entries, so a list a root program publishes drives `validate` without being mapped by hand. An RFC 5914 anchor's constraints are applied as RFC 5937 §3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policy set intersects `userInitialPolicySet`, each of its policy flags may turn the matching `initial*` option on and never off, and its path length bounds the path. `pki.trustanchor.build` writes the list. `systemAnchors` reads the host's own CA bundle into the same entries and names which set it read, `"ca-bundle"` with the path or `"node-bundled"` for the Mozilla set compiled into Node, since the two are not the same trust. A host whose roots are not in a readable PEM file is refused with the command that exports them, and a block that is not a certificate or a certificate the strict reader refuses is listed in `skipped` with its reason. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `parseTrustAnchorList`, `systemAnchors`, `anchor` |
|
|
299
299
|
| `pki.shbs` | Stateful hash-based signature verification: HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST SP 800-208 for CNSA 2.0 firmware signing. `verify` checks an HSS signature, where every level must pass, and `verifyLms` a single-tree LMS, over the raw public-key and signature blobs the parsers already surface. Pure public-input SHA-256 and SHAKE256 hashing, a data-driven typecode registry, and bounds-before-slice reads; a malformed blob throws a typed `ShbsError` while a well-formed but wrong signature returns `false`. Verification only by design, since stateful signing needs atomic one-time-key state that belongs in an HSM. `pki.path.validate` verifies an HSS-signed certificate on its own, so a chain reaching one needs no separate call: the signature covers the whole tbsCertificate rather than a digest of it (RFC 9802 §7.1), and a CertificateList naming the algorithm takes the same route. It is proven against the self-signed certificate RFC 9802 Appendix A publishes — `verify`, `verifyLms` |
|
|
300
300
|
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180), the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS` and `setupR` establish a sender or recipient context (KEM encapsulation plus the key schedule); the context's `seal` and `open` AEAD-encrypt with a sequence-counter nonce, and `export` derives further secrets; the module-level `seal` and `open` are single-shot wrappers. DHKEM (P-256, P-384, P-521, X25519, X448) in all four modes, proven against the RFC 9180 Appendix A vectors, the post-quantum ML-KEM-512/768/1024 KEMs (IANA 0x0040 to 0x0042) in the base and psk modes, proven against the draft-ietf-hpke-pq Appendix A vectors, and the PQ/T hybrid KEMs MLKEM768-P256 (0x0050), MLKEM1024-P384 (0x0051) and MLKEM768-X25519 (0x647a, X-Wing), each pairing an ML-KEM with a nominal group under the CFRG C2PRI combiner and proven against the draft-irtf-cfrg-concrete-hybrid-kems Appendix B vectors; the two-stage HKDF-SHA256/SHA384/SHA512 and the single-stage SHAKE128/SHAKE256 key schedules, proven against the draft-ietf-hpke-pq Appendix A.7/A.8/A.11/A.12 vectors, by AES-GCM / ChaCha20Poly1305 / export-only. On a single-stage KDF `info`, `psk`, `psk_id` and an export are each capped at 65535 bytes; TurboSHAKE128/256 are not offered while no released OpenSSL carries the XOF. `deriveKeyPair`, `generateKeyPair`, `encap` and `decap` reach a KEM on its own. Pure composition over `node:crypto` — `suites`, `setupS`, `setupR`, `seal`, `open`, `encap`, `decap` |
|
package/lib/alt-sig.js
CHANGED
|
@@ -222,7 +222,7 @@ function _findExt(items, wanted) {
|
|
|
222
222
|
* var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
|
|
223
223
|
* var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
|
|
224
224
|
* var certDer = await pki.x509.sign({
|
|
225
|
-
* subject: "
|
|
225
|
+
* subject: "Catalyst", subjectPublicKey: nativeSpki, serialNumber: 1n,
|
|
226
226
|
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
227
227
|
* extensions: { subjectAltPublicKeyInfo: altSpki },
|
|
228
228
|
* }, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
|
|
@@ -330,7 +330,7 @@ function _decodeExt(item, label) {
|
|
|
330
330
|
* var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
|
|
331
331
|
* var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
|
|
332
332
|
* var caDer = await pki.x509.sign({
|
|
333
|
-
* subject: "
|
|
333
|
+
* subject: "Catalyst CA", subjectPublicKey: nativeSpki, serialNumber: 1n,
|
|
334
334
|
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
335
335
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectAltPublicKeyInfo: altSpki },
|
|
336
336
|
* }, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
|
|
@@ -393,7 +393,7 @@ function subjectAltPublicKey(certificate) {
|
|
|
393
393
|
* var nativeSpki = await pki.key.export(native.publicKey, { format: "der" });
|
|
394
394
|
* var altSpki = await pki.key.export(alt.publicKey, { format: "der" });
|
|
395
395
|
* var caDer = await pki.x509.sign({
|
|
396
|
-
* subject: "
|
|
396
|
+
* subject: "Catalyst CA", subjectPublicKey: nativeSpki, serialNumber: 1n,
|
|
397
397
|
* notBefore: new Date("2027-01-01T00:00:00Z"), notAfter: new Date("2028-01-01T00:00:00Z"),
|
|
398
398
|
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectAltPublicKeyInfo: altSpki },
|
|
399
399
|
* }, { key: native.privateKey, altKey: alt.privateKey, altPublicKey: altSpki });
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -550,7 +550,7 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
550
550
|
* var signerCertDer = await pki.x509.sign({ subject: "Attribute Authority", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
551
551
|
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: signerKeyPkcs8 });
|
|
552
552
|
* var ac = await pki.attrcert.sign(
|
|
553
|
-
* { holder: { entityName: { directoryName: "
|
|
553
|
+
* { holder: { entityName: { directoryName: "Alice" } },
|
|
554
554
|
* notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
|
|
555
555
|
* attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
|
|
556
556
|
* { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
@@ -840,14 +840,14 @@ function _unsupportedCritical(ac) {
|
|
|
840
840
|
* var aaSpki = await pki.key.export(pair.publicKey);
|
|
841
841
|
* var aaKey = await pki.key.export(pair.privateKey);
|
|
842
842
|
* var acDer = await pki.attrcert.sign({
|
|
843
|
-
* holder: { entityName: { directoryName: "
|
|
843
|
+
* holder: { entityName: { directoryName: "Alice" } },
|
|
844
844
|
* notBeforeTime: new Date("2026-01-01T00:00:00Z"),
|
|
845
845
|
* notAfterTime: new Date("2027-01-01T00:00:00Z"),
|
|
846
846
|
* attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } },
|
|
847
847
|
* // RFC 5755 sec. 6: the issuer states no revocation information will exist for this AC.
|
|
848
848
|
* extensions: { noRevAvail: true },
|
|
849
|
-
* }, { name: "
|
|
850
|
-
* var r = await pki.attrcert.verify(acDer, { name: "
|
|
849
|
+
* }, { name: "Example AA", publicKey: aaSpki, key: aaKey });
|
|
850
|
+
* var r = await pki.attrcert.verify(acDer, { name: "Example AA", publicKey: aaSpki },
|
|
851
851
|
* { time: new Date("2026-06-01T00:00:00Z") });
|
|
852
852
|
* r.verified; // true when every check whose slot is true passed
|
|
853
853
|
* r.attributes; // the privileges, re-derived from the signed bytes
|
|
@@ -919,7 +919,7 @@ function _verify(ac, issuer, opts) {
|
|
|
919
919
|
reason: undefined,
|
|
920
920
|
});
|
|
921
921
|
|
|
922
|
-
return _promiseResolve(_Promise, attrcertVerify.verifyAcSignature(parsed, trust.spki))
|
|
922
|
+
return intrinsic.chain(_promiseResolve(_Promise, attrcertVerify.verifyAcSignature(parsed, trust.spki)), function (sigOk) {
|
|
923
923
|
result.signatureValid = sigOk === true;
|
|
924
924
|
var fail = function (reason) { result.reason = reason; return result; };
|
|
925
925
|
if (!result.signatureValid) return fail("the signature does not verify under the issuer public key");
|
package/lib/cmc-verify.js
CHANGED
|
@@ -316,9 +316,9 @@ function verify(response, sent) {
|
|
|
316
316
|
_wipeOwnedSecret();
|
|
317
317
|
return _promiseReject(_Promise, e);
|
|
318
318
|
}
|
|
319
|
-
return guard.async.
|
|
320
|
-
.
|
|
321
|
-
|
|
319
|
+
return guard.async.cleanup(
|
|
320
|
+
guard.async.deferred(function () { return _verify(frozenResponse, frozenSent); }),
|
|
321
|
+
_wipeOwnedSecret);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function _assertOpts(sent) {
|
|
@@ -386,14 +386,14 @@ function _verify(response, sent) {
|
|
|
386
386
|
"pki.cmc.verify interprets a Full PKI Response (id-cct-PKIResponse); this message is a " + body.kind);
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
return _assertAuthentic(body, sent, response)
|
|
389
|
+
return intrinsic.chain(_assertAuthentic(body, sent, response), function (signatureVerified) {
|
|
390
390
|
return _shape(body, sent, signatureVerified, _assertBound(body, sent));
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
|
|
395
395
|
function _assertAuthentic(body, sent, responseBytes) {
|
|
396
|
-
return _promiseResolve(_Promise, undefined)
|
|
396
|
+
return intrinsic.chain(_promiseResolve(_Promise, undefined), function () {
|
|
397
397
|
var haveBytes = typeof responseBytes === "string" ||
|
|
398
398
|
Buffer.isBuffer(responseBytes) || responseBytes instanceof Uint8Array; // allow:byte-source-narrow -- responseBytes is _snapshotIfBytes's return: any BufferSource is already a Buffer here, so this is Buffer-or-string by construction
|
|
399
399
|
if (body.cms && body.cms.contentTypeName === "authData") {
|
|
@@ -424,12 +424,15 @@ function _assertAuthentic(body, sent, responseBytes) {
|
|
|
424
424
|
* that check, so the premise does not hold here and the opt-in states it. The opt-in is inert on
|
|
425
425
|
* this path, because an AuthenticatedData is dispatched to the MAC verifier before the refusal is
|
|
426
426
|
* reached; it is passed so the premise stays recorded where the content type is decided. */
|
|
427
|
-
|
|
427
|
+
var decrypted = intrinsic.chain(
|
|
428
|
+
cmsDecrypt.decrypt(responseBytes, recipient, { allowUnauthenticatedRsa15: true }),
|
|
429
|
+
function (r) { _wipeDerived(); return r; },
|
|
428
430
|
function (e) {
|
|
429
431
|
_wipeDerived();
|
|
430
432
|
throw E("cmc/unverified-response",
|
|
431
433
|
"the AuthenticatedData response did not authenticate under the supplied recipient key", e);
|
|
432
|
-
})
|
|
434
|
+
});
|
|
435
|
+
return intrinsic.chain(decrypted, function (res) {
|
|
433
436
|
if (!res || res.authenticated !== true) {
|
|
434
437
|
throw E("cmc/unverified-response", "the AuthenticatedData response did not authenticate under the supplied recipient key");
|
|
435
438
|
}
|
|
@@ -458,8 +461,9 @@ function _assertAuthentic(body, sent, responseBytes) {
|
|
|
458
461
|
"signature is over them -- pass the encoded response rather than a parsed one, or " +
|
|
459
462
|
"`allowUnverified: true` to interpret it unauthenticated");
|
|
460
463
|
}
|
|
461
|
-
return
|
|
462
|
-
.
|
|
464
|
+
return intrinsic.chain(
|
|
465
|
+
cmsVerify.verify(responseBytes, sent.certs && sent.certs.length ? { certs: sent.certs } : {}),
|
|
466
|
+
function (res) {
|
|
463
467
|
if (res.valid) return true;
|
|
464
468
|
var failed = (res.signers || []).filter(function (s) { return !s.ok; });
|
|
465
469
|
var code = failed.length ? failed[0].code : null;
|
package/lib/cmp-build.js
CHANGED
|
@@ -984,7 +984,7 @@ function _collectExtraCerts(opts, protCertDer) {
|
|
|
984
984
|
* var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) },
|
|
985
985
|
* { key: signerKeyPkcs8 });
|
|
986
986
|
* var der = await pki.cmp.build(
|
|
987
|
-
* { header: { sender: { directoryName: "
|
|
987
|
+
* { header: { sender: { directoryName: "client" }, recipient: { directoryName: "CA" } },
|
|
988
988
|
* body: { p10cr: csrDer } },
|
|
989
989
|
* { key: signerKeyPkcs8, cert: signerCertDer });
|
|
990
990
|
* pki.schema.cmp.parse(der).body.arm; // "p10cr"
|
|
@@ -1194,7 +1194,7 @@ function wellKnownUrl(base, opts) {
|
|
|
1194
1194
|
* - `proxy` -- reach the CA through a forward HTTP proxy (`{ url, auth?, tls? }`; see pki.transport).
|
|
1195
1195
|
* @example
|
|
1196
1196
|
* var reqDer = await pki.cmp.build(
|
|
1197
|
-
* { header: { sender: { directoryName: "
|
|
1197
|
+
* { header: { sender: { directoryName: "client" }, recipient: { directoryName: "CA" } },
|
|
1198
1198
|
* body: { p10cr: csrDer } },
|
|
1199
1199
|
* { key: signerKeyPkcs8, cert: signerCertDer });
|
|
1200
1200
|
* var url = pki.cmp.wellKnownUrl("https://ca.example", { operation: "p10cr" });
|
package/lib/cmp-verify.js
CHANGED
|
@@ -1275,7 +1275,7 @@ function openKeyPackage(container, opts) {
|
|
|
1275
1275
|
* var csrDer = await pki.csr.sign({ subject: "client", subjectPublicKey: await pki.key.export(pair.publicKey) },
|
|
1276
1276
|
* { key: signerKeyPkcs8 });
|
|
1277
1277
|
* var cmpDer = await pki.cmp.build(
|
|
1278
|
-
* { header: { sender: { directoryName: "
|
|
1278
|
+
* { header: { sender: { directoryName: "client" }, recipient: { directoryName: "CA" } },
|
|
1279
1279
|
* body: { p10cr: csrDer } }, { key: signerKeyPkcs8, cert: signerCertDer });
|
|
1280
1280
|
* var v = await pki.cmp.verify(cmpDer, { signerCert: signerCertDer, trustAnchors: [certDer] });
|
|
1281
1281
|
* if (v.valid && v.trusted) console.log("the response protection is authentic and the signer is trusted");
|