@blamejs/pki 0.8.5 → 0.8.7
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 +32 -0
- package/README.md +13 -4
- package/index.js +6 -0
- package/lib/asn1-der.js +58 -6
- package/lib/cbor-det.js +11 -1
- package/lib/framework-error.js +2 -0
- package/lib/guard-encoding.js +31 -1
- package/lib/guard-json.js +1 -6
- package/lib/guard-name.js +1 -6
- package/lib/pkcs11-uri.js +646 -0
- package/lib/pkcs12-build.js +6 -1
- package/lib/sign-scheme.js +82 -3
- package/lib/x509-sign.js +8 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.8.7 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
A PKCS #11 URI names a token and an object on it, read and written to the RFC 7512 grammar.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.pkcs11.parseUri(uri)` reads an RFC 7512 PKCS #11 URI into `{ path, vendorPath, query, vendorQuery }`. The path carries what identifies the object (`token`, `manufacturer`, `serial`, `model`, the three `library-*` attributes, the three `slot-*` attributes, `object`, `type` and `id`) and the query what tells a consumer how to reach it (`pin-source`, `pin-value`, `module-name`, `module-path`). A vendor attribute in either component is kept under its own name. A vendor query attribute carries a list of its values, which sec. 2.4 permits to repeat and leaves to the consumer to interpret; a vendor path attribute carries one value, sec. 2.3 admitting no repeat. Each record is prototype-less, so a computed read answers from the URI and never from `Object.prototype`. `id` is a `Buffer`, because the attribute names bytes rather than text, and `slot-id` and `library-version` are read as numbers.
|
|
14
|
+
- `pki.pkcs11.formatUri(record)` writes the record back in canonical form: attributes in the order the RFC lists them, vendor attributes sorted, each value percent-encoded to what its own component admits unencoded, `id` encoded whole with uppercase hexadecimal, `slot-id` without leading zeros and `library-version` stating its minor. That is what lets two URIs naming one object be compared as strings, which sec. 2.6 asks of a consumer, and the form is stable: writing it again changes nothing. One difference it keeps is the order of a repeated vendor query attribute's values, since sec. 2.4 leaves what a repeat means to the consumer and the order may carry part of it. An attribute name and a `type` value are read whichever case they arrive in, RFC 5234 sec. 2.3 making an ABNF quoted literal case-insensitive, and carried in one spelling; so `PIN-SOURCE` is the `pin-source` attribute and is held to the rules `pin-source` is held to, rather than passing as vendor data. The fold is the ASCII one the grammar is written in, not the Unicode one, so a name carrying a character that Unicode folds onto an ASCII letter, such as the Kelvin sign onto `k`, is refused rather than read as the attribute it resembles.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- A builder that encodes text now requires text. `pki.asn1.build.utf8`, `.ia5`, `.printable` and `.bmpString` refuse a value that is not a string with `asn1/bad-string`, `pki.cbor.build.textString` refuses one with `cbor/bad-argument`, and a `pki.pkcs12.build` bag whose `friendlyName` is not a string is refused with `pkcs12/bad-input`. Each of those used to turn the value into text first, so an object was encoded as `[object Object]` and a list as its members joined with a comma, and the certificate, message or key store carried that instead of anything the caller named. Pass the string; where the value came from bytes, decode it yourself and pass the result.
|
|
19
|
+
- `pki.asn1.encode` takes bytes for its content: a `Buffer`, a typed array, a list of byte values, or nothing for an empty value. A string, an object, a number, and a list carrying an element outside 0 to 255 are refused with `asn1/not-buffer`. Each of those used to be converted, so a string became its UTF-8 encoding, an object went through its `valueOf`, an element past 255 became that element modulo 256, and a number ended the call with a Node argument fault rather than a `PkiError`.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- Parsing follows the sec. 2.3 and sec. 2.4 grammar rather than a general URI reader, and refuses with its own `pkcs11/*` code rather than guessing: a character the component admits only percent-encoded, a truncated or non-hexadecimal escape, a value that is not valid UTF-8, an empty attribute between two delimiters, an attribute that appears twice where the RFC admits it once, a percent-escape inside `type`, `slot-id` or `library-version`, whose grammar is literal alternatives and digit runs, a `type` outside the five the RFC enumerates, a `library-version` component above 255, each being one byte in the `CK_INFO` structure sec. 2.3 points at, a URI carrying both `pin-source` and `pin-value`, and a `module-path` that is not absolute on any platform this toolkit runs on. The last two are what sec. 2.4 asks a consumer to treat as invalid: one leaves which PIN applies undecided, and the other decides which shared object a process loads from wherever it happens to be running.
|
|
24
|
+
- Writing is held to the same rules, so the formatter never emits a URI the parser would refuse. A vendor attribute name is held to the vendor grammar and refused when a standard attribute owns it, which is what keeps a name carrying a delimiter from putting an attribute into the URI that the caller never named and that the standard attribute's own checks never saw. A vendor name given no value, or given an empty list, is refused rather than dropped, so the URI that comes back carries every attribute that was asked for or the call fails. Every component of the record is read once, before anything is written, and a text attribute must be given a string rather than a value turned into one, so nothing a caller passes can run code partway through a write and add an attribute to a component whose checks have already passed.
|
|
25
|
+
|
|
26
|
+
## v0.8.6 — 2026-09-25
|
|
27
|
+
|
|
28
|
+
A node:crypto KeyObject signs, without being asked to export its private half.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- A `node:crypto` `KeyObject` is accepted wherever a signing verb takes a private key, and signs through `node:crypto` rather than being exported to PKCS#8 first. RSA with PKCS#1 v1.5 and with PSS padding, ECDSA on P-256, P-384 and P-521, Ed25519, Ed448, ML-DSA and SLH-DSA all sign this way, and the bytes produced are the bytes WebCrypto would have produced: an ECDSA signature is taken in the fixed-width form of RFC 9053 sec. 2.1 and encoded by the toolkit, so the artifact is byte-identical to one signed by the equivalent `CryptoKey`.
|
|
33
|
+
- A `KeyObject` is held to the key the artifact names exactly as a `CryptoKey` is. Its algorithm must match the certificate's key algorithm and, for an EC key, its curve must match the certificate's curve; a public or a secret key is refused by type; and the signature is verified against the declared key before the artifact is returned, so a `KeyObject` for a different key of the same algorithm is refused rather than producing something no relying party can validate.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `pki.x509.sign` and the other signing verbs no longer refuse a `node:crypto` `KeyObject` with the message naming it as not a `CryptoKey`. A caller matching on that message to detect the refusal will stop seeing it, because the key now signs.
|
|
38
|
+
|
|
7
39
|
## v0.8.5 — 2026-09-25
|
|
8
40
|
|
|
9
41
|
A key this process cannot export can sign, and every verb that signs proves its signature first.
|
package/README.md
CHANGED
|
@@ -192,10 +192,18 @@ var ok = await subtle.verify({ name: "ML-DSA-65" }, kp.publicKey, sig, data); /
|
|
|
192
192
|
|
|
193
193
|
### Sign with a key this process cannot export
|
|
194
194
|
|
|
195
|
-
Every signing verb takes a
|
|
196
|
-
key
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
Every signing verb takes a `node:crypto` `KeyObject` wherever it takes a private
|
|
196
|
+
key, and signs through `node:crypto`, so a key an OpenSSL engine or provider
|
|
197
|
+
refuses to export still signs.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
var keyObject = crypto.createPrivateKey({ key: pem });
|
|
201
|
+
var cert = await pki.x509.sign(spec, { key: keyObject, cert: caCert });
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
For a key that is not in this process at all, held in a hardware security module,
|
|
205
|
+
a cloud key management service, a PKCS#11 token, a PIV slot or a Trusted Platform
|
|
206
|
+
Module, every signing verb takes a signer object in the same place.
|
|
199
207
|
|
|
200
208
|
```js
|
|
201
209
|
var signer = {
|
|
@@ -266,6 +274,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
266
274
|
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuer alternative name, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access, which is every extension RFC 5280 §5.2 defines) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The authority key identifier is emitted on every CRL whether or not it is named, as §5.2.1 places it on every conforming issuer, from the issuer certificate's SKI or the issuer key. The signature algorithm is resolved from the issuer 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. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign`, `verify`, `isRevoked` |
|
|
267
275
|
| `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` |
|
|
268
276
|
| `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` |
|
|
277
|
+
| `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` |
|
|
269
278
|
| `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 — including an RFC 3161 timestamp token, attachable via `sign`'s `unsignedAttributes` — under `signers[i].unsignedAttrs`, surfaced unauthenticated. `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` |
|
|
270
279
|
| `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` |
|
|
271
280
|
| `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` |
|
package/index.js
CHANGED
|
@@ -50,6 +50,7 @@ var cmp = require("./lib/cmp-session");
|
|
|
50
50
|
var crl = require("./lib/crl-sign");
|
|
51
51
|
var key = require("./lib/key");
|
|
52
52
|
var pkcs12 = require("./lib/pkcs12-build");
|
|
53
|
+
var pkcs11 = require("./lib/pkcs11-uri");
|
|
53
54
|
var merkle = require("./lib/merkle");
|
|
54
55
|
var shbs = require("./lib/shbs");
|
|
55
56
|
var hpke = require("./lib/hpke");
|
|
@@ -148,6 +149,11 @@ module.exports = {
|
|
|
148
149
|
// under PBES2, a classic HMAC or PBMAC1 MAC), and pki.pkcs12.verifyMac checks a store's MAC. Parsing lives
|
|
149
150
|
// at pki.schema.pkcs12.parse.
|
|
150
151
|
pkcs12: pkcs12,
|
|
152
|
+
// `pkcs11` is the RFC 7512 PKCS #11 URI syntax -- pki.pkcs11.parseUri reads a URI naming a token,
|
|
153
|
+
// a slot, a library and an object on a PKCS #11 device into a prototype-less record, and
|
|
154
|
+
// pki.pkcs11.formatUri writes one back. Syntax only: nothing here loads a module or talks to a
|
|
155
|
+
// token, and `id` is carried as bytes rather than text so two spellings of one identifier compare.
|
|
156
|
+
pkcs11: pkcs11,
|
|
151
157
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
152
158
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
153
159
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/asn1-der.js
CHANGED
|
@@ -750,7 +750,7 @@ function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
|
750
750
|
* pki.asn1.encode(0x00, false, pki.asn1.TAGS.NULL, Buffer.alloc(0));
|
|
751
751
|
*/
|
|
752
752
|
function encodeTLV(classBits, constructed, tagNumber, content) {
|
|
753
|
-
var body =
|
|
753
|
+
var body = _contentBytes(content);
|
|
754
754
|
var id = encodeIdentifier(classBits, constructed, tagNumber);
|
|
755
755
|
return Buffer.concat([id, encodeLength(body.length), body]);
|
|
756
756
|
}
|
|
@@ -759,6 +759,53 @@ function _universal(tagNumber, constructed, content) {
|
|
|
759
759
|
return encodeTLV(CLASS_UNIVERSAL, constructed, tagNumber, content);
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
/** @internal Content is bytes. A value that is not bytes reaches the structure as whatever the
|
|
763
|
+
* byte conversion makes of it: a string as its UTF-8 encoding, an object through its `valueOf`,
|
|
764
|
+
* an array element past 255 as that element modulo 256. A byte source and a list of byte values
|
|
765
|
+
* are the two spellings this takes; an omitted content is the empty one. */
|
|
766
|
+
function _contentBytes(content) {
|
|
767
|
+
if (content == null) return intrinsic.bufferAlloc(0);
|
|
768
|
+
if (guard.bytes.isByteSource(content)) {
|
|
769
|
+
return guard.bytes.source(content, Asn1Error, "asn1/not-buffer", "encodeTLV content");
|
|
770
|
+
}
|
|
771
|
+
if (!intrinsic.isArray(content)) {
|
|
772
|
+
throw new Asn1Error("asn1/not-buffer", "encodeTLV content: the content must be bytes, got " + _typeOf(content));
|
|
773
|
+
}
|
|
774
|
+
/** @internal Each element is read ONCE, into the byte the check and the content both use. Read
|
|
775
|
+
* again to copy, an element answering from a getter could pass the check as 2 and be written
|
|
776
|
+
* as 300, which the copy would narrow to 44. */
|
|
777
|
+
var n = content.length;
|
|
778
|
+
var out = intrinsic.bufferAlloc(n);
|
|
779
|
+
for (var i = 0; i < n; i++) {
|
|
780
|
+
var b = content[i];
|
|
781
|
+
if (typeof b !== "number" || !intrinsic.isSafeInteger(b) || b < 0 || b > 255) {
|
|
782
|
+
throw new Asn1Error("asn1/not-buffer", "encodeTLV content: element " + i + " is not a byte value 0 to 255");
|
|
783
|
+
}
|
|
784
|
+
out[i] = b;
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** @internal A string builder encodes the caller's text. Turning something else into text encodes
|
|
790
|
+
* a value the caller never named: an object arrives as its default string form, a list as its
|
|
791
|
+
* members joined with a comma, and the structure carries that. The value is required to be a
|
|
792
|
+
* string rather than made into one, which also settles what a value with a `toString` answers,
|
|
793
|
+
* since a string cannot answer differently between a check and the encoding. */
|
|
794
|
+
function _asString(s, who) {
|
|
795
|
+
if (typeof s !== "string") {
|
|
796
|
+
throw new Asn1Error("asn1/bad-string", who + ": the value must be a string, got " + _typeOf(s));
|
|
797
|
+
}
|
|
798
|
+
return s;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function _typeOf(v) {
|
|
802
|
+
if (v === null) return "null";
|
|
803
|
+
if (intrinsic.isBuffer(v)) return "a Buffer";
|
|
804
|
+
if (intrinsic.isArray(v)) return "an array";
|
|
805
|
+
var t = typeof v;
|
|
806
|
+
return (t === "object" || t === "undefined" ? "an " : "a ") + t;
|
|
807
|
+
}
|
|
808
|
+
|
|
762
809
|
function intToDer(v) {
|
|
763
810
|
if (typeof v === "number") {
|
|
764
811
|
if (!Number.isSafeInteger(v)) throw new Asn1Error("asn1/bad-integer", "unsafe integer; pass a BigInt");
|
|
@@ -911,21 +958,21 @@ var build = intrinsic.assign(intrinsic.create(null), {
|
|
|
911
958
|
for (i = 0; i < positions.length; i++) { p = positions[i]; buf[p >> 3] |= 0x80 >> (p & 7); }
|
|
912
959
|
return build.bitString(buf, 7 - (hi & 7));
|
|
913
960
|
},
|
|
914
|
-
utf8: function (s) { var v =
|
|
961
|
+
utf8: function (s) { var v = _asString(s, "build.utf8"); _assertWellFormedUtf16(v); return _universal(TAGS.UTF8_STRING, false, Buffer.from(v, "utf8")); },
|
|
915
962
|
ia5: function (s) {
|
|
916
|
-
s =
|
|
963
|
+
s = _asString(s, "build.ia5");
|
|
917
964
|
for (var i = 0; i < s.length; i++) {
|
|
918
965
|
if (_charCodeAt(s, i) > 0x7F) throw new Asn1Error("asn1/bad-ia5-string", "IA5String requires 7-bit ASCII");
|
|
919
966
|
}
|
|
920
967
|
return _universal(TAGS.IA5_STRING, false, Buffer.from(s, "latin1"));
|
|
921
968
|
},
|
|
922
969
|
printable: function (s) {
|
|
923
|
-
s =
|
|
970
|
+
s = _asString(s, "build.printable");
|
|
924
971
|
if (!isPrintableString(s)) throw new Asn1Error("asn1/bad-printable-string", "value has characters outside the PrintableString set");
|
|
925
972
|
return _universal(TAGS.PRINTABLE_STRING, false, Buffer.from(s, "latin1"));
|
|
926
973
|
},
|
|
927
974
|
bmpString: function (s) {
|
|
928
|
-
s =
|
|
975
|
+
s = _asString(s, "build.bmpString");
|
|
929
976
|
var out = Buffer.alloc(s.length * 2);
|
|
930
977
|
for (var i = 0; i < s.length; i++) {
|
|
931
978
|
var u = _charCodeAt(s, i);
|
|
@@ -977,9 +1024,14 @@ function _intContent(v, who) {
|
|
|
977
1024
|
return content;
|
|
978
1025
|
}
|
|
979
1026
|
|
|
1027
|
+
/** @internal Walked by index rather than through the array's own `map`, which a caller can define
|
|
1028
|
+
* to answer with children this never checked. Each element is read once, into the buffer the
|
|
1029
|
+
* check and the encoding both use. */
|
|
980
1030
|
function _asBufferArray(arr, who) {
|
|
981
1031
|
if (!_isArray(arr)) throw new Asn1Error("asn1/bad-children", who + ": expected an array of TLV buffers");
|
|
982
|
-
|
|
1032
|
+
var n = arr.length, out = [];
|
|
1033
|
+
for (var i = 0; i < n; i++) out[i] = _asBuffer(arr[i], who);
|
|
1034
|
+
return out;
|
|
983
1035
|
}
|
|
984
1036
|
|
|
985
1037
|
module.exports = {
|
package/lib/cbor-det.js
CHANGED
|
@@ -627,6 +627,16 @@ function _capString(out, who) {
|
|
|
627
627
|
if (out.length > constants.LIMITS.CBOR_MAX_BYTES) throw new CborError("cbor/too-large", who + ": encoded " + out.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BYTES);
|
|
628
628
|
return out;
|
|
629
629
|
}
|
|
630
|
+
/** @internal A text builder encodes the caller's text. Turning something else into text encodes a
|
|
631
|
+
* value the caller never named: an object arrives as its default string form and a list as its
|
|
632
|
+
* members joined with a comma. */
|
|
633
|
+
function _asText(s, who) {
|
|
634
|
+
if (typeof s !== "string") {
|
|
635
|
+
throw new CborError("cbor/bad-argument", who + ": the value must be a string, got " + (s === null ? "null" : "a " + typeof s));
|
|
636
|
+
}
|
|
637
|
+
return s;
|
|
638
|
+
}
|
|
639
|
+
|
|
630
640
|
function _assertWellFormedText(str, who) {
|
|
631
641
|
for (var i = 0; i < str.length; i++) {
|
|
632
642
|
var c = str.charCodeAt(i);
|
|
@@ -650,7 +660,7 @@ var build = Object.assign(Object.create(null), {
|
|
|
650
660
|
nint: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); if (a >= 0n) throw new CborError("cbor/bad-argument", "build.nint: value must be negative"); return _head(1, -1n - a, "build.nint"); },
|
|
651
661
|
int: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); return a >= 0n ? _head(0, a, "build.int") : _head(1, -1n - a, "build.int"); },
|
|
652
662
|
byteString: function (buf) { var b = _asBuffer(buf, "build.byteString"); return _capString(Buffer.concat([_head(2, b.length, "build.byteString"), b]), "build.byteString"); },
|
|
653
|
-
textString: function (s) { var str =
|
|
663
|
+
textString: function (s) { var str = _asText(s, "build.textString"); _assertWellFormedText(str, "build.textString"); var b = Buffer.from(str, "utf8"); return _capString(Buffer.concat([_head(3, b.length, "build.textString"), b]), "build.textString"); },
|
|
654
664
|
array: function (items) {
|
|
655
665
|
if (!Array.isArray(items)) throw new CborError("cbor/bad-argument", "build.array: expected an array of pre-encoded items");
|
|
656
666
|
var parts = items.map(function (x, i) { return _cbItem(x, "build.array[" + i + "]"); });
|
package/lib/framework-error.js
CHANGED
|
@@ -215,6 +215,7 @@ var InspectError = defineClass("InspectError", { withCause: true });
|
|
|
215
215
|
var WebauthnError = defineClass("WebauthnError", { withCause: true });
|
|
216
216
|
var LintError = defineClass("LintError", { withCause: true });
|
|
217
217
|
var KeyError = defineClass("KeyError", { withCause: true });
|
|
218
|
+
var Pkcs11Error = defineClass("Pkcs11Error", { withCause: true });
|
|
218
219
|
|
|
219
220
|
module.exports = {
|
|
220
221
|
PkiError: PkiError,
|
|
@@ -258,4 +259,5 @@ module.exports = {
|
|
|
258
259
|
AcmeError: AcmeError,
|
|
259
260
|
TrustError: TrustError,
|
|
260
261
|
KeyError: KeyError,
|
|
262
|
+
Pkcs11Error: Pkcs11Error,
|
|
261
263
|
};
|
package/lib/guard-encoding.js
CHANGED
|
@@ -76,4 +76,34 @@ function hex(text, maxBytes, E, code, label) {
|
|
|
76
76
|
return buf;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* @internal The value of one hexadecimal digit, or -1 when the byte is not one. Every
|
|
81
|
+
* character-scanning reader of an escape (a DN string, a JSON string, a URI) asks the same
|
|
82
|
+
* question, and asking it in one place keeps the three ranges from drifting apart.
|
|
83
|
+
*
|
|
84
|
+
* @enforced-by guard-shape-reinlined
|
|
85
|
+
* @guard-shape c >= 0x30 && c <= 0x39\) return c - 0x30
|
|
86
|
+
*/
|
|
87
|
+
function hexNibble(c) {
|
|
88
|
+
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
|
89
|
+
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10;
|
|
90
|
+
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10;
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @internal Whether a byte is an ASCII letter or digit. It is the base of every
|
|
96
|
+
* character-class a textual syntax defines on top of it, so the three ranges live here
|
|
97
|
+
* rather than being spelled again per reader.
|
|
98
|
+
*
|
|
99
|
+
* @enforced-by behavioral -- the ranges have no rename-proof shape distinct from a legitimate
|
|
100
|
+
* range test, and each consumer's own acceptance vector is the guard.
|
|
101
|
+
*/
|
|
102
|
+
function isAlphanumericByte(c) {
|
|
103
|
+
return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = _intrinsic.freeze({
|
|
107
|
+
base64url: base64url, base64: base64, hex: hex,
|
|
108
|
+
hexNibble: hexNibble, isAlphanumericByte: isAlphanumericByte,
|
|
109
|
+
});
|
package/lib/guard-json.js
CHANGED
|
@@ -15,12 +15,7 @@ var _stringify = require("./guard-intrinsic").stringify;
|
|
|
15
15
|
var _append = require("./guard-list").append;
|
|
16
16
|
var limits = require("./guard-limits");
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
|
20
|
-
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10;
|
|
21
|
-
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10;
|
|
22
|
-
return -1;
|
|
23
|
-
}
|
|
18
|
+
var _hexVal = require("./guard-encoding").hexNibble;
|
|
24
19
|
|
|
25
20
|
// @enforced-by json-parse-not-via-guard
|
|
26
21
|
function parse(input, ErrorClass, spec) {
|
package/lib/guard-name.js
CHANGED
|
@@ -164,12 +164,7 @@ function escapeControlBytes(str) {
|
|
|
164
164
|
|
|
165
165
|
var DN_SPECIAL = intrinsic.assign(intrinsic.create(null), { 0x2c: 1, 0x2b: 1, 0x22: 1, 0x5c: 1, 0x3c: 1, 0x3e: 1, 0x3b: 1 });
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
|
169
|
-
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10;
|
|
170
|
-
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10;
|
|
171
|
-
return -1;
|
|
172
|
-
}
|
|
167
|
+
var _hexNibble = require("./guard-encoding").hexNibble;
|
|
173
168
|
function _isAttrTypeStart(c) { return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a); }
|
|
174
169
|
function _isAttrTypeChar(c) { return _isAttrTypeStart(c) || (c >= 0x30 && c <= 0x39) || c === 0x2d; }
|
|
175
170
|
function _isDigit(c) { return c >= 0x30 && c <= 0x39; }
|
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.pkcs11
|
|
6
|
+
* @nav Keys
|
|
7
|
+
* @title PKCS#11 URI
|
|
8
|
+
* @fullname PKCS #11 URIs: naming a token and an object on it (RFC 7512)
|
|
9
|
+
* @order 235
|
|
10
|
+
* @slug pkcs11
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* RFC 7512 defines a URI that names a token, a slot, a library and an object
|
|
14
|
+
* on a PKCS #11 device, which is how an operator points a tool at a key in a
|
|
15
|
+
* hardware security module, a smart card or a PIV slot without naming a file.
|
|
16
|
+
* `parseUri` reads one into a record and `formatUri` writes the record back.
|
|
17
|
+
*
|
|
18
|
+
* The path component carries what identifies the object: `token`,
|
|
19
|
+
* `manufacturer`, `serial`, `model`, the three `library-*` attributes, the
|
|
20
|
+
* three `slot-*` attributes, `object`, `type` and `id`. The query component
|
|
21
|
+
* carries what tells a consumer how to reach it: `pin-source`, `pin-value`,
|
|
22
|
+
* `module-name` and `module-path`. A vendor attribute in either component is
|
|
23
|
+
* kept under its own name rather than dropped. A vendor query attribute is
|
|
24
|
+
* carried as a list of its values, which sec. 2.4 permits to repeat and leaves
|
|
25
|
+
* to the consumer to interpret; a vendor path attribute carries one value.
|
|
26
|
+
*
|
|
27
|
+
* Parsing is fail-closed and follows the ABNF of sec. 2.3 and sec. 2.4 rather
|
|
28
|
+
* than a general URI reader: a character a component does not admit unencoded
|
|
29
|
+
* is refused, a truncated or non-hex escape is refused, an empty attribute
|
|
30
|
+
* between two delimiters is refused, a repeated attribute is refused wherever
|
|
31
|
+
* the RFC does not permit one, and the attributes the RFC writes as digits or
|
|
32
|
+
* as literal alternatives are read that way rather than through an escape. `id` is decoded to a `Buffer`, because it
|
|
33
|
+
* names bytes and not text. A URI carrying both `pin-source` and `pin-value`
|
|
34
|
+
* is refused, and so is a `module-path` that is not absolute, both of which
|
|
35
|
+
* sec. 2.4 asks a consumer to treat as invalid.
|
|
36
|
+
*
|
|
37
|
+
* An attribute name and a `type` value are read whichever case they arrive in,
|
|
38
|
+
* RFC 5234 sec. 2.3 making an ABNF quoted literal case-insensitive, so
|
|
39
|
+
* `PIN-SOURCE` is the `pin-source` attribute and is held to the rules
|
|
40
|
+
* `pin-source` is held to rather than passing as vendor data. Writing produces
|
|
41
|
+
* the canonical form, which is what lets two URIs naming one object be compared
|
|
42
|
+
* as strings, and every rule parsing refuses writing refuses too, so the two
|
|
43
|
+
* directions cannot drift. This is a syntax, not a driver: nothing here loads a
|
|
44
|
+
* PKCS #11 module or talks to a token.
|
|
45
|
+
*
|
|
46
|
+
* @card
|
|
47
|
+
* `parseUri` / `formatUri` over the RFC 7512 ABNF, fail-closed, with `id` as
|
|
48
|
+
* bytes and no module loading.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
var frameworkError = require("./framework-error");
|
|
52
|
+
var guard = require("./guard-all");
|
|
53
|
+
var intrinsic = require("./guard-intrinsic");
|
|
54
|
+
|
|
55
|
+
var _create = intrinsic.create;
|
|
56
|
+
var _assign = intrinsic.assign;
|
|
57
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
58
|
+
var _keys = intrinsic.keys;
|
|
59
|
+
var _forEach = intrinsic.forEach;
|
|
60
|
+
var _push = intrinsic.push;
|
|
61
|
+
var _join = intrinsic.join;
|
|
62
|
+
var _String = intrinsic.String;
|
|
63
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
64
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
65
|
+
var _fromCharCode = String.fromCharCode;
|
|
66
|
+
|
|
67
|
+
var Pkcs11Error = frameworkError.Pkcs11Error;
|
|
68
|
+
|
|
69
|
+
function _err(code, message, cause) { return new Pkcs11Error(code, message, cause); }
|
|
70
|
+
|
|
71
|
+
var SCHEME = "pkcs11:";
|
|
72
|
+
|
|
73
|
+
/** @internal The RFC 7512 sec. 2.3 / sec. 2.4 attribute tables, keyed by the name on the wire.
|
|
74
|
+
* `field` is the name the parsed record carries, `kind` how the value is read. A vendor
|
|
75
|
+
* attribute matches neither table and is kept under its own wire name. */
|
|
76
|
+
var PATH_ATTRS = _assign(_create(null), {
|
|
77
|
+
"token": { field: "token", kind: "text" },
|
|
78
|
+
"manufacturer": { field: "manufacturer", kind: "text" },
|
|
79
|
+
"serial": { field: "serial", kind: "text" },
|
|
80
|
+
"model": { field: "model", kind: "text" },
|
|
81
|
+
"library-manufacturer": { field: "libraryManufacturer", kind: "text" },
|
|
82
|
+
"library-description": { field: "libraryDescription", kind: "text" },
|
|
83
|
+
"library-version": { field: "libraryVersion", kind: "version" },
|
|
84
|
+
"object": { field: "object", kind: "text" },
|
|
85
|
+
"type": { field: "type", kind: "type" },
|
|
86
|
+
"id": { field: "id", kind: "bytes" },
|
|
87
|
+
"slot-manufacturer": { field: "slotManufacturer", kind: "text" },
|
|
88
|
+
"slot-description": { field: "slotDescription", kind: "text" },
|
|
89
|
+
"slot-id": { field: "slotId", kind: "number" },
|
|
90
|
+
});
|
|
91
|
+
var QUERY_ATTRS = _assign(_create(null), {
|
|
92
|
+
"pin-source": { field: "pinSource", kind: "text" },
|
|
93
|
+
"pin-value": { field: "pinValue", kind: "text" },
|
|
94
|
+
"module-name": { field: "moduleName", kind: "text" },
|
|
95
|
+
"module-path": { field: "modulePath", kind: "path" },
|
|
96
|
+
});
|
|
97
|
+
var OBJECT_TYPES = _assign(_create(null), {
|
|
98
|
+
"public": 1, "private": 1, "cert": 1, "secret-key": 1, "data": 1,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** @internal The wire name each record field writes back to, derived from the tables above so the
|
|
102
|
+
* two directions cannot name different attributes. */
|
|
103
|
+
function _byField(table) {
|
|
104
|
+
var out = _create(null);
|
|
105
|
+
_forEach(_keys(table), function (wire) { out[table[wire].field] = _assign(_create(null), { wire: wire, kind: table[wire].kind }); });
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
var PATH_BY_FIELD = _byField(PATH_ATTRS);
|
|
109
|
+
var QUERY_BY_FIELD = _byField(QUERY_ATTRS);
|
|
110
|
+
|
|
111
|
+
/** @internal `unreserved` of RFC 3986 sec. 2.3: ALPHA / DIGIT / "-" / "." / "_" / "~". */
|
|
112
|
+
function _isUnreserved(c) {
|
|
113
|
+
return guard.encoding.isAlphanumericByte(c) || c === 0x2d || c === 0x2e || c === 0x5f || c === 0x7e;
|
|
114
|
+
}
|
|
115
|
+
/** @internal `pk11-res-avail` of sec. 2.3: the reserved characters both components admit unencoded. */
|
|
116
|
+
function _isResAvail(c) {
|
|
117
|
+
return c === 0x3a || c === 0x5b || c === 0x5d || c === 0x40 || c === 0x21 || c === 0x24 ||
|
|
118
|
+
c === 0x27 || c === 0x28 || c === 0x29 || c === 0x2a || c === 0x2b || c === 0x2c || c === 0x3d;
|
|
119
|
+
}
|
|
120
|
+
/** @internal `pk11-pchar` minus pct-encoded: the path admits "&" on top of the shared set. */
|
|
121
|
+
function _isPathChar(c) { return _isUnreserved(c) || _isResAvail(c) || c === 0x26; }
|
|
122
|
+
/** @internal `pk11-qchar` minus pct-encoded: the query admits "/", "?" and "|" instead. */
|
|
123
|
+
function _isQueryChar(c) { return _isUnreserved(c) || _isResAvail(c) || c === 0x2f || c === 0x3f || c === 0x7c; }
|
|
124
|
+
|
|
125
|
+
var _hexValue = guard.encoding.hexNibble;
|
|
126
|
+
|
|
127
|
+
/** @internal The ASCII fold, not the Unicode one. RFC 5234 sec. 2.3 makes a quoted literal
|
|
128
|
+
* case-insensitive over the US-ASCII the ABNF is written in, and `String.prototype.toLowerCase`
|
|
129
|
+
* folds further than that: U+212A KELVIN SIGN lowercases to "k", so folding before the grammar
|
|
130
|
+
* check would read a name carrying one as a standard attribute it is not. */
|
|
131
|
+
function _asciiLower(text) {
|
|
132
|
+
var out = "";
|
|
133
|
+
for (var i = 0; i < text.length; i++) {
|
|
134
|
+
var c = _charCodeAt(text, i);
|
|
135
|
+
out += _fromCharCode(c >= 0x41 && c <= 0x5a ? c + 32 : c);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var HEX = "0123456789ABCDEF";
|
|
141
|
+
function _pctEncodeByte(b) {
|
|
142
|
+
return "%" + _fromCharCode(_charCodeAt(HEX, b >> 4)) + _fromCharCode(_charCodeAt(HEX, b & 0x0f));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @internal The octets a component's value encodes, with every escape decoded and every bare
|
|
146
|
+
* character held to the component's own set. A character outside that set is not a URI this
|
|
147
|
+
* toolkit reads: the ABNF admits it only percent-encoded, so accepting it would accept a string
|
|
148
|
+
* no conforming producer emits and no other consumer reads the same way. */
|
|
149
|
+
function _decodeOctets(text, isChar, what) {
|
|
150
|
+
var out = intrinsic.bufferAlloc(text.length);
|
|
151
|
+
var n = 0;
|
|
152
|
+
for (var i = 0; i < text.length; i++) {
|
|
153
|
+
var c = _charCodeAt(text, i);
|
|
154
|
+
if (c === 0x25) {
|
|
155
|
+
if (i + 2 >= text.length) throw _err("pkcs11/bad-escape", what + " ends in a truncated percent-escape");
|
|
156
|
+
var hi = _hexValue(_charCodeAt(text, i + 1));
|
|
157
|
+
var lo = _hexValue(_charCodeAt(text, i + 2));
|
|
158
|
+
if (hi < 0 || lo < 0) throw _err("pkcs11/bad-escape", what + " carries a percent-escape that is not two hexadecimal digits");
|
|
159
|
+
out[n++] = (hi << 4) | lo;
|
|
160
|
+
i += 2;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!isChar(c)) {
|
|
164
|
+
throw _err("pkcs11/bad-uri", what + " carries a character this URI component admits only percent-encoded (RFC 7512 sec. 2.3, sec. 2.4)");
|
|
165
|
+
}
|
|
166
|
+
out[n++] = c;
|
|
167
|
+
}
|
|
168
|
+
return intrinsic.subarray(out, 0, n);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @internal A value read as text is UTF-8, per sec. 2.3. A byte run that is not valid UTF-8 is
|
|
172
|
+
* refused rather than replaced, because a replacement character would compare equal to a
|
|
173
|
+
* different token's name. */
|
|
174
|
+
function _decodeText(text, isChar, what) {
|
|
175
|
+
var octets = _decodeOctets(text, isChar, what);
|
|
176
|
+
return guard.text.decode(octets, octets.length, Pkcs11Error, {
|
|
177
|
+
charset: "utf-8", fatal: true, tooLarge: "pkcs11/bad-uri",
|
|
178
|
+
badInput: "pkcs11/bad-input", badDecode: "pkcs11/bad-encoding", label: what,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _decimal(text, what) {
|
|
183
|
+
if (text.length === 0) throw _err("pkcs11/bad-uri", what + " must be a decimal number (RFC 7512 sec. 2.3)");
|
|
184
|
+
var value = 0;
|
|
185
|
+
for (var i = 0; i < text.length; i++) {
|
|
186
|
+
var c = _charCodeAt(text, i);
|
|
187
|
+
if (c < 0x30 || c > 0x39) throw _err("pkcs11/bad-uri", what + " must be a decimal number (RFC 7512 sec. 2.3)");
|
|
188
|
+
value = value * 10 + (c - 0x30);
|
|
189
|
+
if (!intrinsic.isSafeInteger(value)) throw _err("pkcs11/bad-uri", what + " is larger than this toolkit reads as a number");
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @internal sec. 2.3: "Both numbers are one byte in size; see the 'libraryVersion' member of the
|
|
195
|
+
* CK_INFO structure". A larger number names a version no token can report. */
|
|
196
|
+
var VERSION_MAX = 255;
|
|
197
|
+
function _versionPart(text, what) {
|
|
198
|
+
var value = _decimal(text, "library-version");
|
|
199
|
+
if (value > VERSION_MAX) {
|
|
200
|
+
throw _err("pkcs11/bad-uri", "the library-version " + what + " is one byte, so it is 0 to 255 (RFC 7512 sec. 2.3), got " + text);
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @internal `pk11-lib-ver` of sec. 2.3: the major is required, the minor defaults to zero. */
|
|
206
|
+
function _libraryVersion(text) {
|
|
207
|
+
var dot = -1;
|
|
208
|
+
for (var i = 0; i < text.length; i++) { if (_charCodeAt(text, i) === 0x2e) { dot = i; break; } }
|
|
209
|
+
var out = _create(null);
|
|
210
|
+
if (dot === -1) {
|
|
211
|
+
out.major = _versionPart(text, "major");
|
|
212
|
+
out.minor = 0;
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
out.major = _versionPart(_strSlice(text, 0, dot), "major");
|
|
216
|
+
out.minor = _versionPart(_strSlice(text, dot + 1), "minor");
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @internal `pk11-type`, `pk11-slot-id` and `pk11-lib-ver` are written in the ABNF as literal
|
|
221
|
+
* alternatives and digit runs, with no `pct-encoded` among them. Decoding an escape there and
|
|
222
|
+
* then reading the result would make `%70ublic` and `public` two spellings of one value, and
|
|
223
|
+
* sec. 2.6 asks a consumer to be able to compare two URIs as strings. */
|
|
224
|
+
/** @internal sec. 2.4 asks a consumer to refuse a module-path that is relative, because a relative
|
|
225
|
+
* one names a different shared object depending on where the process happens to be running. What
|
|
226
|
+
* counts as absolute is the platform's answer rather than the RFC's, and this toolkit runs on
|
|
227
|
+
* every platform Node does: a leading separator, a drive letter followed by one, and a UNC root
|
|
228
|
+
* are the three forms. */
|
|
229
|
+
function _isAbsolutePath(text) {
|
|
230
|
+
var first = _charCodeAt(text, 0);
|
|
231
|
+
if (first === 0x2f || first === 0x5c) return true;
|
|
232
|
+
var isDriveLetter = (first >= 0x41 && first <= 0x5a) || (first >= 0x61 && first <= 0x7a);
|
|
233
|
+
if (!isDriveLetter || _charCodeAt(text, 1) !== 0x3a) return false;
|
|
234
|
+
var third = _charCodeAt(text, 2);
|
|
235
|
+
return third === 0x2f || third === 0x5c;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _assertNoEscape(raw, wire) {
|
|
239
|
+
for (var i = 0; i < raw.length; i++) {
|
|
240
|
+
if (_charCodeAt(raw, i) === 0x25) {
|
|
241
|
+
throw _err("pkcs11/bad-uri", "the " + wire + " attribute is written out rather than percent-encoded (RFC 7512 sec. 2.3)");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return raw;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _readValue(spec, raw, isChar, wire) {
|
|
248
|
+
if (spec.kind === "bytes") return _decodeOctets(raw, isChar, "the " + wire + " attribute");
|
|
249
|
+
if (spec.kind === "number") return _decimal(_assertNoEscape(raw, wire), wire);
|
|
250
|
+
if (spec.kind === "version") return _libraryVersion(_assertNoEscape(raw, wire));
|
|
251
|
+
if (spec.kind === "type") {
|
|
252
|
+
/** @internal RFC 5234 sec. 2.3 makes an ABNF quoted literal case-insensitive, and sec. 2.6
|
|
253
|
+
* asks for comparison after case normalization, so the spelling is read either way and
|
|
254
|
+
* carried in one form. */
|
|
255
|
+
var named = _asciiLower(_assertNoEscape(raw, wire));
|
|
256
|
+
if (OBJECT_TYPES[named] !== 1) {
|
|
257
|
+
throw _err("pkcs11/bad-uri", "the type attribute is one of public, private, cert, secret-key or data (RFC 7512 sec. 2.3), got " + intrinsic.stringify(raw));
|
|
258
|
+
}
|
|
259
|
+
return named;
|
|
260
|
+
}
|
|
261
|
+
var text = _decodeText(raw, isChar, "the " + wire + " attribute");
|
|
262
|
+
if (spec.kind === "path" && !_isAbsolutePath(text)) {
|
|
263
|
+
throw _err("pkcs11/relative-module-path", "a module-path with a relative path is refused (RFC 7512 sec. 2.4)");
|
|
264
|
+
}
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @internal `pk11-v-attr-nm-char` of sec. 2.3, asked once so reading and writing hold a vendor
|
|
269
|
+
* name to the same rule. A name the component's own table owns is refused as a vendor name, and
|
|
270
|
+
* a name carrying a delimiter is refused outright: written out, it would put a standard attribute
|
|
271
|
+
* into the URI that the caller never named and that neither component's checks ever saw. */
|
|
272
|
+
function _assertVendorName(wire, table, component, code) {
|
|
273
|
+
if (wire.length === 0) throw _err(code, "a " + component + " attribute name must not be empty (RFC 7512 sec. 2.3)");
|
|
274
|
+
for (var i = 0; i < wire.length; i++) {
|
|
275
|
+
var c = _charCodeAt(wire, i);
|
|
276
|
+
if (!(guard.encoding.isAlphanumericByte(c) || c === 0x2d || c === 0x5f)) {
|
|
277
|
+
throw _err(code, "a vendor attribute name is letters, digits, hyphen and underscore (RFC 7512 sec. 2.3), got " + intrinsic.stringify(wire));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (_hasOwn(table, _asciiLower(wire))) {
|
|
281
|
+
throw _err(code, "the " + component + " attribute " + wire + " is defined by RFC 7512 sec. 2.3, so it is named as itself rather than as a vendor attribute");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @internal One `name=value` run. The name is a vendor name when neither table knows it, and a
|
|
286
|
+
* vendor name is held to `pk11-v-attr-nm-char` so a stray delimiter cannot pass as one. */
|
|
287
|
+
function _attribute(run, table, isChar, component, known, vendor, vendorRepeats) {
|
|
288
|
+
if (run.length === 0) {
|
|
289
|
+
throw _err("pkcs11/bad-uri", "a " + component + " component carries no empty attribute between its delimiters (RFC 7512 sec. 2.3)");
|
|
290
|
+
}
|
|
291
|
+
var eq = -1;
|
|
292
|
+
for (var i = 0; i < run.length; i++) { if (_charCodeAt(run, i) === 0x3d) { eq = i; break; } }
|
|
293
|
+
if (eq === -1) throw _err("pkcs11/bad-uri", "every " + component + " attribute is name=value (RFC 7512 sec. 2.3), got " + intrinsic.stringify(run));
|
|
294
|
+
/** @internal RFC 7512 writes each attribute name as an ABNF quoted literal, which RFC 5234
|
|
295
|
+
* sec. 2.3 makes case-insensitive, so the name is matched in one case. Matching it as written
|
|
296
|
+
* would read `PIN-SOURCE` as a vendor attribute and let it past the rules pin-source is held
|
|
297
|
+
* to, which is the opposite of what those rules are for. */
|
|
298
|
+
var wire = _asciiLower(_strSlice(run, 0, eq));
|
|
299
|
+
var raw = _strSlice(run, eq + 1);
|
|
300
|
+
var spec = _hasOwn(table, wire) ? table[wire] : null;
|
|
301
|
+
if (spec) {
|
|
302
|
+
if (_hasOwn(known, spec.field)) {
|
|
303
|
+
throw _err("pkcs11/duplicate-attribute", "the " + wire + " attribute appears more than once (RFC 7512 sec. 2.3)");
|
|
304
|
+
}
|
|
305
|
+
known[spec.field] = _readValue(spec, raw, isChar, wire);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
_assertVendorName(wire, table, component, "pkcs11/bad-uri");
|
|
309
|
+
var text = _decodeText(raw, isChar, "the " + wire + " attribute");
|
|
310
|
+
/** @internal sec. 2.4: "Aside from the query attributes defined in this document, duplicate
|
|
311
|
+
* (vendor) attributes MAY be present in the URI query component". The path admits none at all
|
|
312
|
+
* (sec. 2.3), which is why the query carries its vendor values as a list and the path does not. */
|
|
313
|
+
if (!vendorRepeats) {
|
|
314
|
+
if (_hasOwn(vendor, wire)) {
|
|
315
|
+
throw _err("pkcs11/duplicate-attribute", "the " + wire + " attribute appears more than once (RFC 7512 sec. 2.3)");
|
|
316
|
+
}
|
|
317
|
+
vendor[wire] = text;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (!_hasOwn(vendor, wire)) vendor[wire] = [];
|
|
321
|
+
_push(vendor[wire], text);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _split(text, delimiter, into) {
|
|
325
|
+
var start = 0;
|
|
326
|
+
for (var i = 0; i < text.length; i++) {
|
|
327
|
+
if (_charCodeAt(text, i) === delimiter) { _push(into, _strSlice(text, start, i)); start = i + 1; }
|
|
328
|
+
}
|
|
329
|
+
_push(into, _strSlice(text, start));
|
|
330
|
+
return into;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function _assertPinExclusive(query) {
|
|
334
|
+
if (query.pinSource != null && query.pinValue != null) {
|
|
335
|
+
throw _err("pkcs11/conflicting-pin", "a URI carrying both pin-source and pin-value is refused (RFC 7512 sec. 2.4)");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @primitive pki.pkcs11.parseUri
|
|
341
|
+
* @signature pki.pkcs11.parseUri(uri) -> parsed
|
|
342
|
+
* @since 0.8.7
|
|
343
|
+
* @status stable
|
|
344
|
+
* @spec RFC 7512
|
|
345
|
+
* @related pki.pkcs11.formatUri
|
|
346
|
+
*
|
|
347
|
+
* Read an RFC 7512 PKCS #11 URI. Returns `{ path, vendorPath, query, vendorQuery }`,
|
|
348
|
+
* each a prototype-less record, so a computed read answers from the URI and never from
|
|
349
|
+
* `Object.prototype`. `path.id` is a `Buffer`, since the attribute names bytes;
|
|
350
|
+
* `path.slotId` and `path.libraryVersion` (`{ major, minor }`) are numbers. A
|
|
351
|
+
* `vendorQuery` attribute carries a list of its values, which sec. 2.4 permits to repeat;
|
|
352
|
+
* a `vendorPath` attribute carries one value.
|
|
353
|
+
*
|
|
354
|
+
* Fail-closed against the ABNF rather than a general URI reader: a character the
|
|
355
|
+
* component admits only percent-encoded, a truncated or non-hex escape, a value that is
|
|
356
|
+
* not valid UTF-8, an empty attribute between two delimiters, a repeated attribute the
|
|
357
|
+
* RFC does not permit to repeat, a percent-escape inside `type`, `slot-id` or
|
|
358
|
+
* `library-version`, a `type` outside the five
|
|
359
|
+
* the RFC enumerates, a `library-version` component above 255, a URI carrying both
|
|
360
|
+
* `pin-source` and `pin-value`, and a `module-path` that is not absolute are each refused
|
|
361
|
+
* with their own `pkcs11/*` code. An attribute name is matched case-insensitively, so a
|
|
362
|
+
* standard attribute cannot arrive spelled differently and pass as vendor data.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* var u = pki.pkcs11.parseUri("pkcs11:token=My%20Token;object=signing-key;type=private?module-name=softhsm2");
|
|
366
|
+
* u.path.token; // "My Token"
|
|
367
|
+
* u.path.type; // "private"
|
|
368
|
+
* u.query.moduleName; // "softhsm2"
|
|
369
|
+
*/
|
|
370
|
+
function parseUri(uri) {
|
|
371
|
+
if (typeof uri !== "string") {
|
|
372
|
+
throw _err("pkcs11/bad-input", "a PKCS #11 URI must be a string, got " + guard.text.showValue(uri));
|
|
373
|
+
}
|
|
374
|
+
var head = _strSlice(uri, 0, SCHEME.length);
|
|
375
|
+
if (_asciiLower(head) !== SCHEME) {
|
|
376
|
+
throw _err("pkcs11/bad-uri", "a PKCS #11 URI begins with the pkcs11: scheme (RFC 7512 sec. 2.1)");
|
|
377
|
+
}
|
|
378
|
+
var rest = _strSlice(uri, SCHEME.length);
|
|
379
|
+
var mark = -1;
|
|
380
|
+
for (var i = 0; i < rest.length; i++) { if (_charCodeAt(rest, i) === 0x3f) { mark = i; break; } }
|
|
381
|
+
var pathText = mark === -1 ? rest : _strSlice(rest, 0, mark);
|
|
382
|
+
var queryText = mark === -1 ? "" : _strSlice(rest, mark + 1);
|
|
383
|
+
|
|
384
|
+
var path = _create(null), vendorPath = _create(null);
|
|
385
|
+
var query = _create(null), vendorQuery = _create(null);
|
|
386
|
+
/** @internal `pk11-path` and `pk11-query` are each optional as a whole, and only as a whole:
|
|
387
|
+
* between two delimiters the ABNF requires an attribute, so an empty run there is a refusal
|
|
388
|
+
* rather than something to skip past. */
|
|
389
|
+
if (pathText.length) {
|
|
390
|
+
_forEach(_split(pathText, 0x3b, []), function (run) {
|
|
391
|
+
_attribute(run, PATH_ATTRS, _isPathChar, "path", path, vendorPath);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (queryText.length) {
|
|
395
|
+
_forEach(_split(queryText, 0x26, []), function (run) {
|
|
396
|
+
_attribute(run, QUERY_ATTRS, _isQueryChar, "query", query, vendorQuery, true);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
_assertPinExclusive(query);
|
|
400
|
+
|
|
401
|
+
var out = _create(null);
|
|
402
|
+
out.path = path;
|
|
403
|
+
out.vendorPath = vendorPath;
|
|
404
|
+
out.query = query;
|
|
405
|
+
out.vendorQuery = vendorQuery;
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** @internal A caller's string is encoded as the UTF-8 sec. 2.3 asks for. An unpaired surrogate
|
|
410
|
+
* has no UTF-8 form, and writing the replacement character for it would put a different name in
|
|
411
|
+
* the URI than the caller asked for, so it is refused instead. */
|
|
412
|
+
function _encodeText(text, isChar, what) {
|
|
413
|
+
var octets = intrinsic.bufferFrom(text, "utf8");
|
|
414
|
+
if (intrinsic.bufToString(octets, "utf8") !== text) {
|
|
415
|
+
throw _err("pkcs11/bad-input", what + " carries a character with no UTF-8 form (an unpaired surrogate)");
|
|
416
|
+
}
|
|
417
|
+
var parts = [];
|
|
418
|
+
for (var i = 0; i < octets.length; i++) {
|
|
419
|
+
var b = octets[i];
|
|
420
|
+
_push(parts, isChar(b) ? _fromCharCode(b) : _pctEncodeByte(b));
|
|
421
|
+
}
|
|
422
|
+
return _join(parts, "");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** @internal sec. 2.3 asks for `id` to be percent-encoded whole rather than read as text, since it
|
|
426
|
+
* carries bytes, and for the hexadecimal to be uppercase so two spellings of one identifier
|
|
427
|
+
* compare equal. */
|
|
428
|
+
function _encodeBytes(value) {
|
|
429
|
+
var octets = guard.bytes.snapshotSource(value, Pkcs11Error, "pkcs11/bad-input", "the id attribute");
|
|
430
|
+
var parts = [];
|
|
431
|
+
for (var i = 0; i < octets.length; i++) _push(parts, _pctEncodeByte(octets[i]));
|
|
432
|
+
return _join(parts, "");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function _writableDecimal(value) {
|
|
436
|
+
return intrinsic.isSafeInteger(value) && value >= 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @internal A text attribute carries what the URI carries, which is a string, and `parseUri`
|
|
440
|
+
* answers with one. Coercing anything else writes a value the caller never named: an object
|
|
441
|
+
* reaches the URI as its default string form and a list as its members joined with a comma,
|
|
442
|
+
* and both read back as one value. So the value is required to be a string rather than turned
|
|
443
|
+
* into one, which also settles the second question a coercion raises, since a string cannot
|
|
444
|
+
* answer differently between the check and the encoding. */
|
|
445
|
+
function _writableText(value, what) {
|
|
446
|
+
if (typeof value !== "string") {
|
|
447
|
+
throw _err("pkcs11/bad-input", what + " must be a string, got " + guard.text.showValue(value));
|
|
448
|
+
}
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function _writeValue(kind, value, isChar, wire) {
|
|
453
|
+
if (kind === "bytes") return _encodeBytes(value);
|
|
454
|
+
/** @internal The bound is the one the reader applies, so nothing is written that reading it back
|
|
455
|
+
* would refuse: past the safe-integer range a number also stringifies in exponent notation,
|
|
456
|
+
* which `1*DIGIT` does not admit. */
|
|
457
|
+
if (kind === "number") {
|
|
458
|
+
if (!_writableDecimal(value)) {
|
|
459
|
+
throw _err("pkcs11/bad-input", "the " + wire + " attribute must be a non-negative integer this toolkit reads back (RFC 7512 sec. 2.3)");
|
|
460
|
+
}
|
|
461
|
+
return _String(value);
|
|
462
|
+
}
|
|
463
|
+
if (kind === "version") {
|
|
464
|
+
/** @internal Each member read ONCE, into the number the check and the URI both use. Read
|
|
465
|
+
* again per use, a getter could answer inside the range for the check and outside it for
|
|
466
|
+
* the string, which is the same "checked one value, wrote another" the component snapshot
|
|
467
|
+
* above closes one level up. */
|
|
468
|
+
var pair = value != null && typeof value === "object" ? value : {};
|
|
469
|
+
var major = pair.major;
|
|
470
|
+
var minor = pair.minor;
|
|
471
|
+
if (!_writableDecimal(major) || !_writableDecimal(minor) || major > VERSION_MAX || minor > VERSION_MAX) {
|
|
472
|
+
throw _err("pkcs11/bad-input", "library-version is { major, minor }, each one byte and so 0 to 255 (RFC 7512 sec. 2.3)");
|
|
473
|
+
}
|
|
474
|
+
return _String(major) + "." + _String(minor);
|
|
475
|
+
}
|
|
476
|
+
var text = _writableText(value, "the " + wire + " attribute");
|
|
477
|
+
if (kind === "type") {
|
|
478
|
+
var named = _asciiLower(text);
|
|
479
|
+
if (OBJECT_TYPES[named] !== 1) {
|
|
480
|
+
throw _err("pkcs11/bad-input", "the type attribute is one of public, private, cert, secret-key or data (RFC 7512 sec. 2.3), got " + intrinsic.stringify(text));
|
|
481
|
+
}
|
|
482
|
+
return named;
|
|
483
|
+
}
|
|
484
|
+
if (kind === "path" && !_isAbsolutePath(text)) {
|
|
485
|
+
throw _err("pkcs11/relative-module-path", "a module-path with a relative path is refused (RFC 7512 sec. 2.4)");
|
|
486
|
+
}
|
|
487
|
+
return _encodeText(text, isChar, "the " + wire + " attribute");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** @internal One read per known field, into a record of this module's own. */
|
|
491
|
+
function _snapshotComponent(record, byField, component) {
|
|
492
|
+
if (record == null) return _create(null);
|
|
493
|
+
if (typeof record !== "object") {
|
|
494
|
+
throw _err("pkcs11/bad-input", "the " + component + " component must be an object of attributes");
|
|
495
|
+
}
|
|
496
|
+
guard.identifier.assertKnownKeys(record, byField, _err, "pkcs11/bad-input",
|
|
497
|
+
"the " + component + " component has an unknown attribute ");
|
|
498
|
+
return guard.identifier.snapshotOptions(record, byField);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** @internal One read per own name, since a vendor component names its own attributes. */
|
|
502
|
+
function _snapshotVendor(record, component) {
|
|
503
|
+
var out = _create(null);
|
|
504
|
+
if (record == null) return out;
|
|
505
|
+
if (typeof record !== "object") {
|
|
506
|
+
throw _err("pkcs11/bad-input", "the " + component + " vendor attributes must be an object");
|
|
507
|
+
}
|
|
508
|
+
/** @internal A list is copied too, so a value appended to it while another component is being
|
|
509
|
+
* written cannot reach the URI. */
|
|
510
|
+
_forEach(_keys(record), function (name) {
|
|
511
|
+
var value = record[name];
|
|
512
|
+
if (!intrinsic.isArray(value)) { out[name] = value; return; }
|
|
513
|
+
var copy = [];
|
|
514
|
+
for (var i = 0; i < value.length; i++) _push(copy, value[i]);
|
|
515
|
+
out[name] = copy;
|
|
516
|
+
});
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function _writeComponent(record, byField, isChar, component, parts) {
|
|
521
|
+
_forEach(_keys(byField), function (field) {
|
|
522
|
+
if (!_hasOwn(record, field) || record[field] == null) return;
|
|
523
|
+
var spec = byField[field];
|
|
524
|
+
_push(parts, spec.wire + "=" + _writeValue(spec.kind, record[field], isChar, spec.wire));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function _writeVendor(record, table, isChar, component, parts, repeats) {
|
|
529
|
+
/** @internal Lowercased and ordered by name, because the canonical form is what lets two URIs
|
|
530
|
+
* naming one object be compared as strings: the reader matches a name in one case, and the
|
|
531
|
+
* order a caller happened to build the record in is not part of what the URI says. Two names
|
|
532
|
+
* that differ only in case are the one attribute twice, which is what the reader would call
|
|
533
|
+
* them. A name's own values keep the order they were given, since sec. 2.4 leaves what a
|
|
534
|
+
* repeated vendor attribute means to the consumer and the order may be part of it. The
|
|
535
|
+
* standard attributes are already in the order the RFC lists them.
|
|
536
|
+
*
|
|
537
|
+
* A name in this record IS an attribute the caller asked for, the standard components being
|
|
538
|
+
* where a field is named in advance and left unset. So a name carrying nothing is refused
|
|
539
|
+
* rather than skipped: writing the URI without it answers a different URI than the one that
|
|
540
|
+
* was asked for, and says so nowhere. */
|
|
541
|
+
var written = _create(null), names = [];
|
|
542
|
+
_forEach(_keys(record), function (given) {
|
|
543
|
+
_assertVendorName(given, table, component, "pkcs11/bad-input");
|
|
544
|
+
var wire = _asciiLower(given);
|
|
545
|
+
if (_hasOwn(written, wire)) {
|
|
546
|
+
throw _err("pkcs11/duplicate-attribute", "the " + wire + " attribute appears more than once (RFC 7512 sec. 2.3)");
|
|
547
|
+
}
|
|
548
|
+
var value = record[given];
|
|
549
|
+
if (value == null) {
|
|
550
|
+
throw _err("pkcs11/bad-input", "the " + wire + " attribute is named with no value; remove the name or give it one");
|
|
551
|
+
}
|
|
552
|
+
if (repeats) {
|
|
553
|
+
if (!intrinsic.isArray(value)) {
|
|
554
|
+
throw _err("pkcs11/bad-input", "a vendor query attribute carries a list of values, since RFC 7512 sec. 2.4 permits it to repeat; got " + guard.text.showValue(value));
|
|
555
|
+
}
|
|
556
|
+
if (value.length === 0) {
|
|
557
|
+
throw _err("pkcs11/bad-input", "the " + wire + " attribute is named with an empty list; remove the name or give it a value");
|
|
558
|
+
}
|
|
559
|
+
written[wire] = _mapValues(value, wire, isChar);
|
|
560
|
+
} else {
|
|
561
|
+
written[wire] = [_encodeText(_writableText(value, "the " + wire + " attribute"), isChar, "the " + wire + " attribute")];
|
|
562
|
+
}
|
|
563
|
+
_push(names, wire);
|
|
564
|
+
});
|
|
565
|
+
_forEach(intrinsic.sort(names), function (wire) {
|
|
566
|
+
_forEach(written[wire], function (encoded) { _push(parts, wire + "=" + encoded); });
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function _mapValues(list, wire, isChar) {
|
|
571
|
+
var out = [];
|
|
572
|
+
for (var i = 0; i < list.length; i++) {
|
|
573
|
+
if (list[i] == null) {
|
|
574
|
+
throw _err("pkcs11/bad-input", "a vendor query attribute's list carries no missing value; the " + wire + " attribute has one");
|
|
575
|
+
}
|
|
576
|
+
_push(out, _encodeText(_writableText(list[i], "the " + wire + " attribute"), isChar, "the " + wire + " attribute"));
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
var KNOWN_RECORD_KEYS = _assign(_create(null), { path: 1, vendorPath: 1, query: 1, vendorQuery: 1 });
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @primitive pki.pkcs11.formatUri
|
|
585
|
+
* @signature pki.pkcs11.formatUri(record) -> string
|
|
586
|
+
* @since 0.8.7
|
|
587
|
+
* @status stable
|
|
588
|
+
* @spec RFC 7512
|
|
589
|
+
* @related pki.pkcs11.parseUri
|
|
590
|
+
*
|
|
591
|
+
* Write an RFC 7512 PKCS #11 URI from `{ path, vendorPath, query, vendorQuery }`, the
|
|
592
|
+
* shape `parseUri` returns. What comes out is the canonical form rather than the input
|
|
593
|
+
* byte for byte: attributes are emitted in the order the RFC lists them, each value is
|
|
594
|
+
* percent-encoded to what its own component admits unencoded, `id` is encoded whole with
|
|
595
|
+
* uppercase hexadecimal, `slot-id` drops leading zeros and `library-version` states its
|
|
596
|
+
* minor. That is what makes two URIs naming one object compare equal as strings, which
|
|
597
|
+
* sec. 2.6 asks of a consumer, and the form is stable: writing it again changes nothing.
|
|
598
|
+
*
|
|
599
|
+
* The rules `parseUri` refuses are refused here too rather than written out: a `type`
|
|
600
|
+
* outside the five the RFC enumerates, both `pin-source` and `pin-value`, a relative
|
|
601
|
+
* `module-path`, and a number this toolkit would not read back. An attribute the RFC does
|
|
602
|
+
* not define is named in the refusal, so a misspelled field is a refusal rather than a
|
|
603
|
+
* silently dropped one; put a vendor attribute under `vendorPath` or `vendorQuery`, where
|
|
604
|
+
* its name is held to the vendor grammar and refused if a standard attribute owns it, so
|
|
605
|
+
* a name carrying a delimiter cannot put an attribute into the URI the caller never named.
|
|
606
|
+
* A `vendorQuery` attribute is given as a list and its values are written in the order the
|
|
607
|
+
* list gives them, which is the shape `parseUri` returns; a `vendorPath` attribute is
|
|
608
|
+
* given one value. A vendor name given no value, or given an empty list, is refused as
|
|
609
|
+
* well, since the name is the attribute the caller asked for.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* pki.pkcs11.formatUri({ path: { token: "My Token", type: "private" }, query: { moduleName: "softhsm2" } });
|
|
613
|
+
* // -> "pkcs11:token=My%20Token;type=private?module-name=softhsm2"
|
|
614
|
+
*/
|
|
615
|
+
function formatUri(record) {
|
|
616
|
+
if (record == null || typeof record !== "object" || guard.bytes.isByteSource(record)) {
|
|
617
|
+
throw _err("pkcs11/bad-input", "a PKCS #11 URI is written from { path, vendorPath, query, vendorQuery }");
|
|
618
|
+
}
|
|
619
|
+
guard.identifier.assertKnownKeys(record, KNOWN_RECORD_KEYS, _err, "pkcs11/bad-input",
|
|
620
|
+
"a PKCS #11 URI record has an unknown field ");
|
|
621
|
+
|
|
622
|
+
/** @internal Every component is snapshotted BEFORE anything is written. A getter on one
|
|
623
|
+
* component that ran while another was being written could otherwise add an attribute to a
|
|
624
|
+
* component written after it, past a check that had already passed. What is checked is what
|
|
625
|
+
* is written. */
|
|
626
|
+
var path = _snapshotComponent(record.path, PATH_BY_FIELD, "path");
|
|
627
|
+
var query = _snapshotComponent(record.query, QUERY_BY_FIELD, "query");
|
|
628
|
+
var vendorPath = _snapshotVendor(record.vendorPath, "path");
|
|
629
|
+
var vendorQuery = _snapshotVendor(record.vendorQuery, "query");
|
|
630
|
+
_assertPinExclusive(query);
|
|
631
|
+
|
|
632
|
+
var pathParts = [], queryParts = [];
|
|
633
|
+
_writeComponent(path, PATH_BY_FIELD, _isPathChar, "path", pathParts);
|
|
634
|
+
_writeVendor(vendorPath, PATH_ATTRS, _isPathChar, "path", pathParts);
|
|
635
|
+
_writeComponent(query, QUERY_BY_FIELD, _isQueryChar, "query", queryParts);
|
|
636
|
+
_writeVendor(vendorQuery, QUERY_ATTRS, _isQueryChar, "query", queryParts, true);
|
|
637
|
+
|
|
638
|
+
var out = SCHEME + _join(pathParts, ";");
|
|
639
|
+
if (queryParts.length) out += "?" + _join(queryParts, "&");
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
module.exports = intrinsic.freeze({
|
|
644
|
+
parseUri: parseUri,
|
|
645
|
+
formatUri: formatUri,
|
|
646
|
+
});
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -188,7 +188,12 @@ function _assertMacIter(n, cap) {
|
|
|
188
188
|
|
|
189
189
|
function _bagAttributes(bag) {
|
|
190
190
|
var attrs = [];
|
|
191
|
-
if (bag.friendlyName != null)
|
|
191
|
+
if (bag.friendlyName != null) {
|
|
192
|
+
if (typeof bag.friendlyName !== "string") {
|
|
193
|
+
throw _err("pkcs12/bad-input", "a bag's friendlyName must be a string, since it is the text a key store lists the bag under");
|
|
194
|
+
}
|
|
195
|
+
attrs.push(b.sequence([b.oid(O("friendlyName")), b.set([b.bmpString(bag.friendlyName)])]));
|
|
196
|
+
}
|
|
192
197
|
if (bag.localKeyId != null) {
|
|
193
198
|
if (bag.localKeyId === "ski") throw _err("pkcs12/bad-input", "localKeyId 'ski' auto-derivation is not yet supported -- supply an explicit Buffer");
|
|
194
199
|
attrs.push(b.sequence([b.oid(O("localKeyId")), b.set([b.octetString(_bytes(bag.localKeyId, "localKeyId"))])]));
|
package/lib/sign-scheme.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
var asn1 = require("./asn1-der");
|
|
7
7
|
var oid = require("./oid");
|
|
8
|
+
var nodeCrypto = require("crypto");
|
|
8
9
|
var pkcs8 = require("./schema-pkcs8");
|
|
9
10
|
var pkix = require("./schema-pkix");
|
|
10
11
|
var frameworkError = require("./framework-error");
|
|
@@ -22,6 +23,7 @@ var _String = intrinsic.String;
|
|
|
22
23
|
var _filter = intrinsic.filter;
|
|
23
24
|
var _stringify = intrinsic.stringify;
|
|
24
25
|
var _toUpperCase = intrinsic.toUpperCase;
|
|
26
|
+
var _toLowerCase = intrinsic.toLowerCase;
|
|
25
27
|
var _isBuffer = intrinsic.isBuffer;
|
|
26
28
|
var _bufferFrom = intrinsic.bufferFrom;
|
|
27
29
|
var _isU8 = intrinsic.types.isUint8Array;
|
|
@@ -218,9 +220,10 @@ function _importKey(key, imp, E) {
|
|
|
218
220
|
try { der = pkcs8.pemDecode(key); }
|
|
219
221
|
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
|
220
222
|
owned = true;
|
|
221
|
-
} else
|
|
222
|
-
throw E("bad-input", "
|
|
223
|
-
|
|
223
|
+
} else {
|
|
224
|
+
throw E("bad-input", "a signer key must be a WebCrypto CryptoKey, a node:crypto KeyObject, a PKCS#8 DER Buffer, " +
|
|
225
|
+
"a PKCS#8 PEM string, or a signer { algorithm, publicKey, sign }");
|
|
226
|
+
}
|
|
224
227
|
var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
|
|
225
228
|
if (!owned) return imported;
|
|
226
229
|
return imported.then(function (k) {
|
|
@@ -324,7 +327,83 @@ function _signExternally(signer, scheme, signedBytes, E) {
|
|
|
324
327
|
});
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
/** @internal What `node:crypto` calls the key an algorithm the toolkit signs with uses, keyed by
|
|
331
|
+
* the WebCrypto name the scheme resolved to. The two vocabularies differ, and the map is the one
|
|
332
|
+
* place that knows it, so a new algorithm adds a row rather than a branch. */
|
|
333
|
+
var NODE_KEY_TYPE = intrinsic.assign(intrinsic.create(null), {
|
|
334
|
+
"RSASSA-PKCS1-v1_5": "rsa", "RSA-PSS": "rsa-pss", ECDSA: "ec", Ed25519: "ed25519", Ed448: "ed448",
|
|
335
|
+
});
|
|
336
|
+
/** @internal The curve a KeyObject reports, keyed by the WebCrypto curve a scheme names. */
|
|
337
|
+
var NODE_CURVE = intrinsic.assign(intrinsic.create(null), {
|
|
338
|
+
"P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/** @internal An RSA key carries no padding in its type, so an RSA-PSS scheme signs with a plain
|
|
342
|
+
* `rsa` key too; the padding is named on the signature, not on the key. */
|
|
343
|
+
function _keyTypeAccepts(reported, wanted) {
|
|
344
|
+
if (reported === wanted) return true;
|
|
345
|
+
return wanted === "rsa-pss" && reported === "rsa";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function _assertKeyObjectMatchesScheme(key, scheme, E) {
|
|
349
|
+
var reported = key.asymmetricKeyType;
|
|
350
|
+
var wanted = NODE_KEY_TYPE[scheme.imp.name] || _toLowerCase(_String(scheme.imp.name));
|
|
351
|
+
if (!_keyTypeAccepts(reported, wanted)) {
|
|
352
|
+
throw E("bad-input", "the signer KeyObject algorithm (" + reported +
|
|
353
|
+
") does not match the certificate's key algorithm (" + scheme.imp.name + ")");
|
|
354
|
+
}
|
|
355
|
+
if (scheme.imp.namedCurve) {
|
|
356
|
+
var details = key.asymmetricKeyDetails;
|
|
357
|
+
var curve = details && details.namedCurve;
|
|
358
|
+
if (curve !== NODE_CURVE[scheme.imp.namedCurve]) {
|
|
359
|
+
throw E("bad-input", "the signer KeyObject curve (" + curve +
|
|
360
|
+
") does not match the certificate curve (" + scheme.imp.namedCurve + ")");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** @internal The options `node:crypto` signs a scheme under, so the bytes it returns are the bytes
|
|
366
|
+
* WebCrypto would have returned: an ECDSA signature in the fixed-width form of RFC 9053 sec. 2.1,
|
|
367
|
+
* which the caller then encodes, and a PSS signature at the salt length the scheme fixed. */
|
|
368
|
+
function _nodeSignOptions(key, scheme) {
|
|
369
|
+
var name = scheme.sign.name;
|
|
370
|
+
if (name === "ECDSA") return { key: key, dsaEncoding: "ieee-p1363" };
|
|
371
|
+
if (name === "RSA-PSS") {
|
|
372
|
+
return { key: key, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: scheme.sign.saltLength };
|
|
373
|
+
}
|
|
374
|
+
return key;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** @internal A digest `node:crypto` applies before signing. An algorithm that hashes the message
|
|
378
|
+
* itself (the Edwards curves, ML-DSA, SLH-DSA) takes none, and naming one there is an error. */
|
|
379
|
+
function _nodeDigest(scheme) {
|
|
380
|
+
var name = scheme.sign.name;
|
|
381
|
+
if (name === "ECDSA" || name === "RSA-PSS" || name === "RSASSA-PKCS1-v1_5") return NODE_DIGEST[scheme.digest] || scheme.digest;
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function _signWithKeyObject(key, scheme, signedBytes, E) {
|
|
386
|
+
if (scheme.composite) {
|
|
387
|
+
throw E("bad-input", "a composite " + scheme.composite.name + " signature is produced from both component private keys, " +
|
|
388
|
+
"which one KeyObject cannot hold; pass { mldsa, trad } PKCS#8 keys");
|
|
389
|
+
}
|
|
390
|
+
if (key.type !== "private") {
|
|
391
|
+
throw E("bad-input", "the signing key is a node:crypto KeyObject of type " + _stringify(key.type) +
|
|
392
|
+
"; a signing key must be a private key (type \"private\")");
|
|
393
|
+
}
|
|
394
|
+
_assertKeyObjectMatchesScheme(key, scheme, E);
|
|
395
|
+
try {
|
|
396
|
+
return _bufferFrom(nodeCrypto.sign(_nodeDigest(scheme), signedBytes, _nodeSignOptions(key, scheme)));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
throw E("bad-input", "the signer KeyObject could not produce a " + scheme.imp.name + " signature", e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
327
402
|
function signOverTbs(scheme, key, signedBytes, E) {
|
|
403
|
+
if (intrinsic.types.isKeyObject(key)) {
|
|
404
|
+
var koSig = _signWithKeyObject(key, scheme, signedBytes, E);
|
|
405
|
+
return _resolve(Promise, scheme.ecdsaDer ? validator.sig.rawToEcdsaDer(koSig, scheme.coordLen) : koSig);
|
|
406
|
+
}
|
|
328
407
|
var signer = _snapshotSigner(key);
|
|
329
408
|
if (signer) {
|
|
330
409
|
return _signExternally(signer, scheme, signedBytes, E).then(function (sig) {
|
package/lib/x509-sign.js
CHANGED
|
@@ -366,13 +366,14 @@ function _hasCriticalSan(extSpec) {
|
|
|
366
366
|
* `extensions` object. `issuer` is the signing side: `{ key }` alone issues a self-signed certificate
|
|
367
367
|
* (issuer = subject, signed with the subject's own key); `{ name, publicKey, key }` or `{ cert, key }`
|
|
368
368
|
* issues a CA-signed one. The signing key `key` is a WebCrypto `CryptoKey` (a `pki.key.generate`
|
|
369
|
-
* private key passed directly, without exporting), a PKCS#8 private
|
|
370
|
-
* (`string`), or a signer `{ algorithm, publicKey, sign }` for a key
|
|
371
|
-
* such as one in a hardware security module, a cloud key management
|
|
372
|
-
* PIV slot. `
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
369
|
+
* private key passed directly, without exporting), a `node:crypto` `KeyObject`, a PKCS#8 private
|
|
370
|
+
* key as DER (`Buffer`) or PEM (`string`), or a signer `{ algorithm, publicKey, sign }` for a key
|
|
371
|
+
* this process cannot export, such as one in a hardware security module, a cloud key management
|
|
372
|
+
* service, a PKCS#11 token or a PIV slot. A `KeyObject` signs through `node:crypto`, so a key that
|
|
373
|
+
* refuses to export its private half still signs. `sign(bytes)` returns the bytes WebCrypto would
|
|
374
|
+
* return for that algorithm, which for ECDSA is the fixed-width `r || s` of RFC 9053 sec. 2.1, and
|
|
375
|
+
* holds its client in its own closure, since this verb snapshots its options. A public or secret
|
|
376
|
+
* key is refused whichever form carries it. A `subjectAltName`
|
|
376
377
|
* (or any `GeneralName`) entry may be a form object (`{ dNSName: "..." }`) or a bare string classified
|
|
377
378
|
* fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
|
|
378
379
|
* The signature algorithm is resolved from the signing key: RSA (PKCS#1 v1.5
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f6667295-f56b-4c1b-a9a8-6f8889816a2b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-23T18:05:34.573Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.7",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.7",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.7",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.8.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.7",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|