@blamejs/pki 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +4 -2
- package/index.js +2 -0
- package/lib/cms-verify.js +404 -0
- package/lib/ip-utils.js +59 -0
- package/lib/lint.js +6 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ 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.2.14 — 2026-07-13
|
|
8
|
+
|
|
9
|
+
CMS SignedData signature verification arrives as pki.cms.verify -- verifying a signed message (S/MIME, timestamps, code signing) over the exact RFC 5652 preimage, for attached and detached content, one or many signers, across RSA, RSASSA-PSS, ECDSA, and EdDSA.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cms.verify(input, opts) verifies a CMS SignedData signature (RFC 5652 section 5) for attached and detached content, single and multiple signers, across RSA, RSASSA-PSS, ECDSA, and EdDSA. It accepts a PEM string, a DER Buffer, or a parsed pki.schema.cms object; opts.content supplies the external content for a detached signature and opts.certs supplies additional signer certificates. Each signer is located by its issuerAndSerialNumber or subjectKeyIdentifier and its signature checked over the exact RFC 5652 section 5.4 preimage -- the message-digest attribute bound to the content digest and the signature verified over the SignedAttributes re-encoding when signed attributes are present, otherwise over the content directly. Returns { valid, signers } with a per-signer verdict and the matched certificate; a false verdict or a structural fault is a fail-closed cms/* outcome.
|
|
14
|
+
|
|
15
|
+
## v0.2.13 — 2026-07-13
|
|
16
|
+
|
|
17
|
+
The certificate linter validates IP-literal common names with a strict in-house checker instead of node:net, so the toolkit pulls in no networking module.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- pki.lint validates an IP-literal common name with a strict in-house IPv4/IPv6 checker (lib/ip-utils) instead of node:net, so the toolkit needs no networking module; the validator now also recognizes IPv4-mapped and dual-stack IPv6 literals.
|
|
22
|
+
|
|
7
23
|
## v0.2.12 — 2026-07-13
|
|
8
24
|
|
|
9
25
|
pki.ct.verifySct verifies a Signed Certificate Timestamp's signature against a Certificate Transparency log's public key.
|
package/README.md
CHANGED
|
@@ -221,7 +221,8 @@ is callable today; nothing below is a stub.
|
|
|
221
221
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
222
222
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
223
223
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
|
-
| `pki.
|
|
224
|
+
| `pki.cms` | RFC 5652 §5 CMS SignedData signature verification — `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: when signed attributes are 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), otherwise directly over the content. Attached and detached content, single and multiple signers, RSA / RSASSA-PSS / ECDSA / EdDSA. Returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. Fail-closed with typed `cms/*` errors — `verify` |
|
|
225
|
+
| `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
|
|
225
226
|
| `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
|
|
226
227
|
| `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
|
|
227
228
|
| `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
|
|
@@ -336,7 +337,8 @@ modules reached by explicit call rather than DER routing: `pki.path` (RFC 5280
|
|
|
336
337
|
path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparency
|
|
337
338
|
SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
|
|
338
339
|
`pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
|
|
339
|
-
verification), `pki.webauthn` (WebAuthn / passkey attestation verification),
|
|
340
|
+
verification), `pki.webauthn` (WebAuthn / passkey attestation verification),
|
|
341
|
+
`pki.cms` (RFC 5652 SignedData signature verification), and the
|
|
340
342
|
`jose` / `acme` / `est` enrollment surfaces. Each composes
|
|
341
343
|
the shared structure, foundation, and crypto layers directly. Alongside the schema
|
|
342
344
|
engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
|
package/index.js
CHANGED
|
@@ -36,6 +36,7 @@ var webcrypto = require("./lib/webcrypto");
|
|
|
36
36
|
var schema = require("./lib/schema-all");
|
|
37
37
|
var path = require("./lib/path-validate");
|
|
38
38
|
var ct = require("./lib/ct");
|
|
39
|
+
var cms = require("./lib/cms-verify");
|
|
39
40
|
var merkle = require("./lib/merkle");
|
|
40
41
|
var shbs = require("./lib/shbs");
|
|
41
42
|
var hpke = require("./lib/hpke");
|
|
@@ -73,6 +74,7 @@ module.exports = {
|
|
|
73
74
|
// SCT-list extension a certificate / OCSP response carries; the signature is
|
|
74
75
|
// surfaced raw for external verification (pki.ct.reconstructSignedData).
|
|
75
76
|
ct: ct,
|
|
77
|
+
cms: cms,
|
|
76
78
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
77
79
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
78
80
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.cms
|
|
6
|
+
* @nav Verification
|
|
7
|
+
* @title CMS
|
|
8
|
+
* @intro Verify a CMS SignedData signature (RFC 5652 sec. 5) -- the structure S/MIME signed
|
|
9
|
+
* mail, RFC 3161 timestamp tokens, and code-signing all rest on. `verify(cms, opts)` parses
|
|
10
|
+
* the SignedData (over the strict `pki.schema.cms` codec), locates each SignerInfo's signer
|
|
11
|
+
* certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature
|
|
12
|
+
* over the exact bytes RFC 5652 sec. 5.4 defines: when signed attributes are present it
|
|
13
|
+
* confirms the message-digest attribute equals the digest of the content and verifies the
|
|
14
|
+
* signature over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced
|
|
15
|
+
* by a universal SET OF); otherwise it verifies directly over the content. Attached and
|
|
16
|
+
* detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA are
|
|
17
|
+
* covered. It reports a per-signer verdict; it does NOT chain the signer certificate to a
|
|
18
|
+
* trust anchor -- that is the caller's step through `pki.path.validate`.
|
|
19
|
+
* @spec RFC 5652
|
|
20
|
+
* @card Verify a CMS SignedData signature (S/MIME, timestamps, code signing).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
var asn1 = require("./asn1-der");
|
|
24
|
+
var oid = require("./oid");
|
|
25
|
+
var x509 = require("./schema-x509");
|
|
26
|
+
var cms = require("./schema-cms");
|
|
27
|
+
var webcrypto = require("./webcrypto");
|
|
28
|
+
var subtle = webcrypto.webcrypto.subtle;
|
|
29
|
+
var edwardsPoint = require("./edwards-point");
|
|
30
|
+
var validator = require("./validator-all");
|
|
31
|
+
var guard = require("./guard-all");
|
|
32
|
+
var frameworkError = require("./framework-error");
|
|
33
|
+
|
|
34
|
+
var CmsError = frameworkError.CmsError;
|
|
35
|
+
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
36
|
+
|
|
37
|
+
var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
|
|
38
|
+
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
39
|
+
|
|
40
|
+
// A digest-algorithm name -> the WebCrypto hash.
|
|
41
|
+
var DIGEST_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
42
|
+
// A signatureAlgorithm name -> its verify scheme. A combined OID (sha256WithRSAEncryption,
|
|
43
|
+
// ecdsaWithSHA256) carries its own hash; a bare key OID (rsaEncryption, ecPublicKey) takes the
|
|
44
|
+
// hash from the SignerInfo digestAlgorithm.
|
|
45
|
+
// `params` pins the AlgorithmIdentifier parameters shape RFC 5754 requires: "null" for the
|
|
46
|
+
// RSASSA-PKCS1-v1_5 family (RFC 4055), "absent" for ECDSA (RFC 5758) and EdDSA (RFC 8410).
|
|
47
|
+
// RSASSA-PSS omits it -- its parameters ARE the RSASSA-PSS-params, resolved by _resolvePss.
|
|
48
|
+
var SIG_SCHEME = {
|
|
49
|
+
rsaEncryption: { kind: "rsa", params: "null" },
|
|
50
|
+
rsassaPss: { kind: "rsapss" },
|
|
51
|
+
ecPublicKey: { kind: "ec", params: "absent" },
|
|
52
|
+
Ed25519: { kind: "eddsa", name: "Ed25519", params: "absent" },
|
|
53
|
+
Ed448: { kind: "eddsa", name: "Ed448", params: "absent" },
|
|
54
|
+
sha1WithRSAEncryption: { kind: "rsa", hash: "SHA-1", params: "null" },
|
|
55
|
+
sha256WithRSAEncryption: { kind: "rsa", hash: "SHA-256", params: "null" },
|
|
56
|
+
sha384WithRSAEncryption: { kind: "rsa", hash: "SHA-384", params: "null" },
|
|
57
|
+
sha512WithRSAEncryption: { kind: "rsa", hash: "SHA-512", params: "null" },
|
|
58
|
+
ecdsaWithSHA1: { kind: "ec", hash: "SHA-1", params: "absent" },
|
|
59
|
+
ecdsaWithSHA256: { kind: "ec", hash: "SHA-256", params: "absent" },
|
|
60
|
+
ecdsaWithSHA384: { kind: "ec", hash: "SHA-384", params: "absent" },
|
|
61
|
+
ecdsaWithSHA512: { kind: "ec", hash: "SHA-512", params: "absent" },
|
|
62
|
+
};
|
|
63
|
+
// The signatureAlgorithm parameters match the algorithm's fixed shape (RFC 5754). A DER NULL
|
|
64
|
+
// is exactly `05 00`; "absent" is the parameters field omitted entirely.
|
|
65
|
+
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
66
|
+
function _algParamsOk(shape, p) { return shape === "null" ? _isDerNull(p) : (p === null || p === undefined); }
|
|
67
|
+
// An EC named-curve OID (in the signer cert SPKI) -> the WebCrypto curve + r/s coordinate width.
|
|
68
|
+
var EC_CURVE = {};
|
|
69
|
+
EC_CURVE[oid.byName("prime256v1")] = { curve: "P-256", coordLen: 32 };
|
|
70
|
+
EC_CURVE[oid.byName("secp384r1")] = { curve: "P-384", coordLen: 48 };
|
|
71
|
+
EC_CURVE[oid.byName("secp521r1")] = { curve: "P-521", coordLen: 66 };
|
|
72
|
+
|
|
73
|
+
function _toBuf(v, what) {
|
|
74
|
+
if (Buffer.isBuffer(v)) return v;
|
|
75
|
+
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
76
|
+
throw _err("cms/bad-input", what + " must be a Buffer");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Every certificate a SignerInfo's signer identifier matches, among the SignedData's embedded
|
|
80
|
+
// certificates plus any the caller supplies. issuerAndSerialNumber matches on the canonical
|
|
81
|
+
// DN + serial; subjectKeyIdentifier matches the cert's SKI extension. ALL matches are returned
|
|
82
|
+
// (in candidate order) so a colliding certificate placed before the real signer cannot hide it.
|
|
83
|
+
function _findSignerCerts(sid, parsedCerts) {
|
|
84
|
+
var out = [];
|
|
85
|
+
for (var i = 0; i < parsedCerts.length; i++) {
|
|
86
|
+
var c = parsedCerts[i];
|
|
87
|
+
if (sid.subjectKeyIdentifier != null) {
|
|
88
|
+
if (c.ski && c.ski.equals(_toBuf(sid.subjectKeyIdentifier, "sid.subjectKeyIdentifier"))) out.push(c);
|
|
89
|
+
} else if (sid.issuer && sid.serialNumberHex != null) {
|
|
90
|
+
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer, sid.issuer, CmsError, "cms/bad-name")) out.push(c);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Verify `sigBytes` over `signedBytes` against EACH matching candidate certificate in turn; the
|
|
97
|
+
// signer is valid iff one candidate verifies. A candidate whose key is structurally unusable (a
|
|
98
|
+
// low-order EdDSA point, an unsupported curve) or whose signature simply does not verify falls
|
|
99
|
+
// through to the next -- so a colliding certificate cannot make a valid signature read invalid.
|
|
100
|
+
// When no candidate verifies, a plain false is a code-less verdict; the last structural fault's
|
|
101
|
+
// code is surfaced if one occurred (so the diagnostic is not lost).
|
|
102
|
+
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt) {
|
|
103
|
+
var lastErr = null;
|
|
104
|
+
function attempt(idx) {
|
|
105
|
+
if (idx >= candidates.length) {
|
|
106
|
+
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
107
|
+
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
108
|
+
}
|
|
109
|
+
var c = candidates[idx];
|
|
110
|
+
return Promise.resolve()
|
|
111
|
+
.then(function () { return _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt); })
|
|
112
|
+
.then(function (ok) { return ok === true ? { ok: true, sid: sid, cert: c.der } : attempt(idx + 1); },
|
|
113
|
+
function (e) { lastErr = (e instanceof CmsError) ? e : _err("cms/verify-error", "the SignerInfo signature could not be evaluated", e); return attempt(idx + 1); });
|
|
114
|
+
}
|
|
115
|
+
return attempt(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The cert's subjectKeyIdentifier extension value (the raw key id), or null.
|
|
119
|
+
function _certSki(cert) {
|
|
120
|
+
var ext = (cert.extensions || []).filter(function (e) { return e.oid === oid.byName("subjectKeyIdentifier"); })[0];
|
|
121
|
+
if (!ext) return null;
|
|
122
|
+
try {
|
|
123
|
+
return asn1.read.octetString(asn1.decode(ext.value));
|
|
124
|
+
} catch (_e) { return null; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// RSASSA-PSS-params resolution (RFC 4055). A SHA-2 hashAlgorithm name -> its WebCrypto hash
|
|
128
|
+
// and the salt length WebCrypto verifies (the hash-length salt of the supported profile).
|
|
129
|
+
var OID_MGF1 = oid.byName("mgf1");
|
|
130
|
+
var PSS_HASH = {};
|
|
131
|
+
PSS_HASH[oid.byName("sha256")] = "SHA-256";
|
|
132
|
+
PSS_HASH[oid.byName("sha384")] = "SHA-384";
|
|
133
|
+
PSS_HASH[oid.byName("sha512")] = "SHA-512";
|
|
134
|
+
|
|
135
|
+
// A hash AlgorithmIdentifier { OID, parameters? } whose parameters, when present, MUST be a
|
|
136
|
+
// DER NULL with empty content (RFC 4055 sec. 2.1 / RFC 5754). Returns the hash OID, or null
|
|
137
|
+
// on any malformed shape.
|
|
138
|
+
function _hashAlgOid(seq) {
|
|
139
|
+
if (!seq || seq.tagClass !== "universal" || seq.tagNumber !== asn1.TAGS.SEQUENCE || !seq.children || seq.children.length < 1 || seq.children.length > 2) return null;
|
|
140
|
+
var o;
|
|
141
|
+
try {
|
|
142
|
+
o = asn1.read.oid(seq.children[0]);
|
|
143
|
+
} catch (_e) { return null; }
|
|
144
|
+
if (seq.children.length === 2) {
|
|
145
|
+
var p = seq.children[1];
|
|
146
|
+
if (p.tagClass !== "universal" || p.tagNumber !== asn1.TAGS.NULL) return null;
|
|
147
|
+
try {
|
|
148
|
+
asn1.read.nullValue(p);
|
|
149
|
+
} catch (_e2) { return null; }
|
|
150
|
+
}
|
|
151
|
+
return o;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Resolve RSASSA-PSS-params (RFC 4055) to { hash, saltLength }, or null if the parameters
|
|
155
|
+
// deviate from the supported profile: an explicit SHA-2 hashAlgorithm, MGF1 keyed to the SAME
|
|
156
|
+
// hash, saltLength equal to the hash length, and trailerField 1. The SHA-1 DEFAULTs are
|
|
157
|
+
// rejected (params must be explicit), matching the certification-path validator, so a
|
|
158
|
+
// non-conformant PSS AlgorithmIdentifier is a fail-closed verdict -- never verified under
|
|
159
|
+
// WebCrypto's own defaults (a signatureAlgorithm bypass otherwise).
|
|
160
|
+
function _resolvePss(paramsBytes) {
|
|
161
|
+
if (!paramsBytes) return null;
|
|
162
|
+
var n;
|
|
163
|
+
try {
|
|
164
|
+
n = asn1.decode(paramsBytes);
|
|
165
|
+
} catch (_e) { return null; }
|
|
166
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children) return null;
|
|
167
|
+
var hash = null, saltLen = null, mgfNode = null, trailer = 1n, last = -1;
|
|
168
|
+
for (var i = 0; i < n.children.length; i++) {
|
|
169
|
+
var f = n.children[i];
|
|
170
|
+
// Each field is an EXPLICIT context [0..3] wrapper carrying exactly one value, at most
|
|
171
|
+
// once and in ascending order; anything else is malformed.
|
|
172
|
+
if (f.tagClass !== "context" || f.tagNumber > 3 || f.tagNumber <= last || !f.children || f.children.length !== 1) return null;
|
|
173
|
+
last = f.tagNumber;
|
|
174
|
+
try {
|
|
175
|
+
if (f.tagNumber === 0) { hash = PSS_HASH[_hashAlgOid(f.children[0])]; if (!hash) return null; }
|
|
176
|
+
else if (f.tagNumber === 1) { mgfNode = f.children[0]; }
|
|
177
|
+
else if (f.tagNumber === 2) { saltLen = asn1.read.integer(f.children[0]); }
|
|
178
|
+
else { trailer = asn1.read.integer(f.children[0]); }
|
|
179
|
+
} catch (_e3) { return null; }
|
|
180
|
+
}
|
|
181
|
+
if (hash === null || mgfNode === null) return null; // both MUST be explicit (SHA-1 default rejected)
|
|
182
|
+
if (mgfNode.tagClass !== "universal" || mgfNode.tagNumber !== asn1.TAGS.SEQUENCE || !mgfNode.children || mgfNode.children.length !== 2) return null;
|
|
183
|
+
var mgfOid;
|
|
184
|
+
try {
|
|
185
|
+
mgfOid = asn1.read.oid(mgfNode.children[0]);
|
|
186
|
+
} catch (_e4) { return null; }
|
|
187
|
+
if (mgfOid !== OID_MGF1 || PSS_HASH[_hashAlgOid(mgfNode.children[1])] !== hash) return null;
|
|
188
|
+
if (trailer !== 1n) return null;
|
|
189
|
+
// saltLength: an absent field is the RFC 4055 DEFAULT of 20; a declared value is honored and
|
|
190
|
+
// passed through to WebCrypto (RSASSA-PSS is not pinned to hash-length salt). It is bounded to
|
|
191
|
+
// a non-negative int -- a negative value maps to OpenSSL's RSA_PSS_SALTLEN_DIGEST/AUTO/MAX
|
|
192
|
+
// magic, and AUTO accepts any salt length, defeating the salt-length binding.
|
|
193
|
+
var saltLength = saltLen === null ? 20 : guard.range.uint31(saltLen, _err, "cms/unsupported-algorithm", "RSASSA-PSS saltLength");
|
|
194
|
+
return { hash: hash, saltLength: saltLength };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Verify the signature over `signedBytes` with the signer certificate's public key under the
|
|
198
|
+
// resolved scheme + hash. Resolves true/false (a false verdict is a verdict); a structural
|
|
199
|
+
// fault throws a typed CmsError.
|
|
200
|
+
function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOid, pssSalt) {
|
|
201
|
+
if (scheme.kind === "rsa") {
|
|
202
|
+
return subtle.importKey("spki", spki, { name: "RSASSA-PKCS1-v1_5", hash: hashName }, false, ["verify"])
|
|
203
|
+
.then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, sigBytes, signedBytes); });
|
|
204
|
+
}
|
|
205
|
+
if (scheme.kind === "rsapss") {
|
|
206
|
+
return subtle.importKey("spki", spki, { name: "RSA-PSS", hash: hashName }, false, ["verify"])
|
|
207
|
+
.then(function (k) { return subtle.verify({ name: "RSA-PSS", saltLength: pssSalt }, k, sigBytes, signedBytes); });
|
|
208
|
+
}
|
|
209
|
+
if (scheme.kind === "ec") {
|
|
210
|
+
var ec = EC_CURVE[curveOid];
|
|
211
|
+
if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
|
|
212
|
+
var raw = validator.sig.ecdsaSigToRaw(sigBytes, ec.coordLen, CmsError, "cms/bad-signature");
|
|
213
|
+
return subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"])
|
|
214
|
+
.then(function (k) { return subtle.verify({ name: "ECDSA", hash: hashName }, k, raw, signedBytes); });
|
|
215
|
+
}
|
|
216
|
+
// EdDSA -- the WebCrypto name follows the signing key's SPKI OID (Ed25519 / Ed448). node/
|
|
217
|
+
// OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order (e.g.
|
|
218
|
+
// all-zeroes) key verifies a forged signature -- so reject a non-full-order point first.
|
|
219
|
+
_requireValidEdPoint(spki, scheme.name);
|
|
220
|
+
return subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"])
|
|
221
|
+
.then(function (k) { return subtle.verify({ name: scheme.name }, k, sigBytes, signedBytes); });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The raw Edwards point an OKP SPKI carries (its BIT STRING body, past the unused-bits octet)
|
|
225
|
+
// MUST be a valid, canonical, full-order point -- reject an off-curve or low-order key before
|
|
226
|
+
// it verifies a signature (WebCrypto import does not check it). Curve from the WebCrypto name.
|
|
227
|
+
function _requireValidEdPoint(spkiBytes, name) {
|
|
228
|
+
var content;
|
|
229
|
+
try {
|
|
230
|
+
content = asn1.decode(spkiBytes).children[1].content;
|
|
231
|
+
} catch (e) { throw _err("cms/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
|
|
232
|
+
var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
|
|
233
|
+
if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
|
|
234
|
+
throw _err("cms/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The EC named-curve OID carried in a signer cert's SubjectPublicKeyInfo, or null (non-EC).
|
|
239
|
+
function _certCurveOid(cert) {
|
|
240
|
+
var p = cert.subjectPublicKeyInfo.algorithm.parameters;
|
|
241
|
+
if (cert.subjectPublicKeyInfo.algorithm.oid !== oid.byName("ecPublicKey") || !Buffer.isBuffer(p)) return null;
|
|
242
|
+
try {
|
|
243
|
+
return asn1.read.oid(asn1.decode(p));
|
|
244
|
+
} catch (_e) { return null; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Decode the SignedAttributes SET OF Attribute from the exact bytes the signature covers
|
|
248
|
+
// (the re-tagged signedAttrsBytes), returning [{ type, values: [valueNode] }]. Binding the
|
|
249
|
+
// content-type / message-digest checks to THESE decoded attributes -- not the caller-mutable
|
|
250
|
+
// parsed si.signedAttrs -- means a parsed-object input cannot desync the checked attributes
|
|
251
|
+
// from the verified preimage (RFC 5652 sec. 5.4).
|
|
252
|
+
function _decodeSignedAttrs(setOfBytes) {
|
|
253
|
+
var set = asn1.decode(setOfBytes);
|
|
254
|
+
if (set.tagClass !== "universal" || set.tagNumber !== asn1.TAGS.SET || !set.children) throw _err("cms/bad-signed-attrs", "signedAttrs is not a SET OF Attribute");
|
|
255
|
+
return set.children.map(function (attr) {
|
|
256
|
+
if (attr.tagClass !== "universal" || attr.tagNumber !== asn1.TAGS.SEQUENCE || !attr.children || attr.children.length !== 2) throw _err("cms/bad-signed-attrs", "a signed Attribute is not a SEQUENCE { type, values }");
|
|
257
|
+
var valuesSet = attr.children[1];
|
|
258
|
+
if (valuesSet.tagClass !== "universal" || valuesSet.tagNumber !== asn1.TAGS.SET || !valuesSet.children) throw _err("cms/bad-signed-attrs", "a signed Attribute values field is not a SET OF");
|
|
259
|
+
return { type: asn1.read.oid(attr.children[0]), values: valuesSet.children };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Verify one SignerInfo. Returns { ok, sid, cert, ... } -- a resolved false is a verdict; a
|
|
264
|
+
// structural failure carries a code.
|
|
265
|
+
function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
266
|
+
var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
|
|
267
|
+
if (!scheme) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
|
|
268
|
+
var digestHash = DIGEST_HASH[si.digestAlgorithm.name];
|
|
269
|
+
// RSASSA-PSS takes its signature hash and salt length from the RSASSA-PSS-params, not the
|
|
270
|
+
// digestAlgorithm; a non-conformant parameter set is a fail-closed unsupported verdict.
|
|
271
|
+
var pss = scheme.kind === "rsapss" ? _resolvePss(si.signatureAlgorithm.parameters) : null;
|
|
272
|
+
if (scheme.kind === "rsapss" && !pss) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported or non-conformant RSASSA-PSS parameters (RFC 4055)" });
|
|
273
|
+
// signatureAlgorithm is outside the signed preimage, so a forbidden/missing parameters field
|
|
274
|
+
// (a present parameter on ECDSA/EdDSA, a non-NULL or absent one on RSA) must fail closed --
|
|
275
|
+
// otherwise it is a parser differential a caller trusting the verdict inherits (RFC 5754).
|
|
276
|
+
if (scheme.params && !_algParamsOk(scheme.params, si.signatureAlgorithm.parameters)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.signatureAlgorithm.name + " signature algorithm parameters must be " + (scheme.params === "null" ? "DER NULL (RFC 4055)" : "absent (RFC 5758/8410)") });
|
|
277
|
+
// The digestAlgorithm parameters, when present, MUST be a DER NULL (RFC 5754 sec. 2 accepts
|
|
278
|
+
// absent OR NULL; a present non-NULL is malformed and, being outside the signed preimage,
|
|
279
|
+
// must fail closed like the signatureAlgorithm parameters above).
|
|
280
|
+
var dp = si.digestAlgorithm.parameters;
|
|
281
|
+
if (dp !== null && dp !== undefined && !_isDerNull(dp)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
|
|
282
|
+
var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
|
|
283
|
+
// The signature hash is required for every non-EdDSA scheme (EdDSA hashes internally);
|
|
284
|
+
// the content digest is required whenever signed attributes are present, for every scheme
|
|
285
|
+
// (the message-digest attribute is computed under digestAlgorithm). Either gap is a
|
|
286
|
+
// fail-closed unsupported-algorithm verdict, never a foreign-domain throw from the digest.
|
|
287
|
+
if ((scheme.kind !== "eddsa" && !sigHash) || (si.signedAttrsBytes && !digestHash)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
|
|
288
|
+
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
289
|
+
if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
290
|
+
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
291
|
+
|
|
292
|
+
return Promise.resolve().then(function () {
|
|
293
|
+
if (!si.signedAttrsBytes) return content; // no signed attributes: sign over the content directly
|
|
294
|
+
// With signed attributes: decode them from the EXACT bytes the signature covers -- the
|
|
295
|
+
// SignedAttributes SET OF, the on-wire [0] IMPLICIT tag replaced by a universal SET OF
|
|
296
|
+
// (RFC 5652 sec. 5.4) -- so the content-type / message-digest checks bind the same bytes
|
|
297
|
+
// that are verified. Reading the caller-mutable parsed si.signedAttrs instead would let a
|
|
298
|
+
// parsed-object input desync the checked attributes from the verified preimage.
|
|
299
|
+
var reTagged = Buffer.from(si.signedAttrsBytes);
|
|
300
|
+
reTagged[0] = 0x31; // [0] IMPLICIT -> universal SET OF
|
|
301
|
+
var attrs;
|
|
302
|
+
try {
|
|
303
|
+
attrs = _decodeSignedAttrs(reTagged);
|
|
304
|
+
} catch (e) { if (e instanceof CmsError) throw e; throw _err("cms/bad-signed-attrs", "signedAttrs is not a valid SET OF Attribute", e); }
|
|
305
|
+
// The content-type attribute MUST be present, single-valued, and equal the eContentType
|
|
306
|
+
// (RFC 5652 sec. 5.3).
|
|
307
|
+
var ctAttr = attrs.filter(function (a) { return a.type === OID_CONTENT_TYPE; });
|
|
308
|
+
if (ctAttr.length !== 1 || ctAttr[0].values.length !== 1) throw _err("cms/bad-signed-attrs", "signedAttrs must carry exactly one content-type attribute (RFC 5652 sec. 5.3)");
|
|
309
|
+
var ctOid;
|
|
310
|
+
try {
|
|
311
|
+
ctOid = asn1.read.oid(ctAttr[0].values[0]);
|
|
312
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the content-type attribute value is not an OBJECT IDENTIFIER", e); }
|
|
313
|
+
if (ctOid !== eContentType) return { mismatch: { code: "cms/content-type-mismatch", message: "the content-type signed attribute does not match the SignedData eContentType (RFC 5652 sec. 5.3)" } };
|
|
314
|
+
// The message-digest attribute MUST be present, single-valued, and equal the digest of the
|
|
315
|
+
// content (RFC 5652 sec. 5.4).
|
|
316
|
+
var mdAttr = attrs.filter(function (a) { return a.type === OID_MESSAGE_DIGEST; })[0];
|
|
317
|
+
if (!mdAttr || mdAttr.values.length !== 1) throw _err("cms/bad-signed-attrs", "signedAttrs must carry exactly one message-digest attribute (RFC 5652 sec. 5.4)");
|
|
318
|
+
var declared;
|
|
319
|
+
try {
|
|
320
|
+
declared = asn1.read.octetString(mdAttr.values[0]);
|
|
321
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the message-digest attribute value is not an OCTET STRING", e); }
|
|
322
|
+
return subtle.digest(digestHash, content).then(function (d) {
|
|
323
|
+
if (!Buffer.from(d).equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
|
|
324
|
+
return reTagged;
|
|
325
|
+
});
|
|
326
|
+
}).then(function (signedBytes) {
|
|
327
|
+
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
328
|
+
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @primitive pki.cms.verify
|
|
334
|
+
* @signature pki.cms.verify(input, opts?) -> Promise<{ valid, signers }>
|
|
335
|
+
* @since 0.2.14
|
|
336
|
+
* @status experimental
|
|
337
|
+
* @spec RFC 5652
|
|
338
|
+
* @defends cms-signature-forgery (CWE-347)
|
|
339
|
+
* @related pki.schema.cms.parse, pki.path.validate
|
|
340
|
+
*
|
|
341
|
+
* Verify a CMS SignedData signature (RFC 5652 sec. 5). `input` is a PEM string, a DER
|
|
342
|
+
* `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, signers }` where each
|
|
343
|
+
* `signers[i]` is `{ ok, sid, cert }` (`cert` the matched signer certificate DER) or carries
|
|
344
|
+
* a `code` on a structural failure; `valid` is true when there is at least one signer and
|
|
345
|
+
* every signer verified.
|
|
346
|
+
*
|
|
347
|
+
* @opts content The detached content (a `Buffer`) when the SignedData carries no
|
|
348
|
+
* encapsulated eContent. Required for a detached signature.
|
|
349
|
+
* @opts certs Extra signer certificates (an array of DER `Buffer`s) to match against, in
|
|
350
|
+
* addition to the certificates embedded in the SignedData.
|
|
351
|
+
* @example
|
|
352
|
+
* var res = await pki.cms.verify(p7sDer, { content: detachedBytes });
|
|
353
|
+
* res.valid; // boolean
|
|
354
|
+
* res.signers[0].ok; // per-signer verdict
|
|
355
|
+
*/
|
|
356
|
+
function verify(input, opts) {
|
|
357
|
+
opts = opts || {};
|
|
358
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.verify options must be an object");
|
|
359
|
+
var parsed = (input && typeof input === "object" && !Buffer.isBuffer(input) && Array.isArray(input.signerInfos)) ? input : cms.parse(input);
|
|
360
|
+
if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "input is not a CMS SignedData");
|
|
361
|
+
var content = parsed.encapContentInfo.eContent;
|
|
362
|
+
if (content == null) {
|
|
363
|
+
if (opts.content == null) throw _err("cms/detached-content-required", "this SignedData is detached; opts.content (the external content) is required");
|
|
364
|
+
content = _toBuf(opts.content, "opts.content");
|
|
365
|
+
}
|
|
366
|
+
// Pre-parse every candidate certificate (embedded + caller-supplied) once.
|
|
367
|
+
var parsedCerts = [];
|
|
368
|
+
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
369
|
+
(opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
|
|
370
|
+
var eContentType = parsed.encapContentInfo.eContentType;
|
|
371
|
+
return Promise.all(parsed.signerInfos.map(function (si) { return _verifyOne(si, content, eContentType, parsedCerts); }))
|
|
372
|
+
.then(function (signers) { return { valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }), signers: signers }; });
|
|
373
|
+
}
|
|
374
|
+
// Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
|
|
375
|
+
// simply cannot be a signer match, and a malformed embedded cert must not fail the verify).
|
|
376
|
+
function _addCert(out, der) {
|
|
377
|
+
var buf;
|
|
378
|
+
try {
|
|
379
|
+
buf = _toBuf(der, "a certificate");
|
|
380
|
+
} catch (_e) { return; }
|
|
381
|
+
var cert;
|
|
382
|
+
try {
|
|
383
|
+
cert = x509.parse(buf);
|
|
384
|
+
} catch (_e2) { return; }
|
|
385
|
+
out.push({ cert: cert, der: buf, ski: _certSki(cert) });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Coverage residual -- these defensive branches are unreachable through the shipped path
|
|
389
|
+
// because an upstream contract already narrows the shape:
|
|
390
|
+
// * `cert.extensions || []` -- x509.parse always surfaces `extensions` as an array (empty
|
|
391
|
+
// when absent), so the `|| []` fallback never fires.
|
|
392
|
+
// * `_requireValidEdPoint`'s SPKI-decode catch and its empty-content `Buffer.alloc(0)`
|
|
393
|
+
// fallback -- a signer certificate accepted by x509.parse always carries a well-formed,
|
|
394
|
+
// non-empty SubjectPublicKeyInfo BIT STRING, so the decode never throws and the content is
|
|
395
|
+
// never empty here.
|
|
396
|
+
// * `_decodeSignedAttrs`'s `tagNumber !== SET` / `!children` guard -- the caller forces the
|
|
397
|
+
// leading byte to a universal SET OF before decoding, so `asn1.decode` yields a SET whose
|
|
398
|
+
// `children` is always an array; the guard is belt-and-suspenders against a future caller.
|
|
399
|
+
// * `if (!Array.isArray(parsed.signerInfos))` -- the object-input branch is only taken when
|
|
400
|
+
// `signerInfos` is already an array, and cms.parse yields a SignedData whose `signerInfos`
|
|
401
|
+
// is always an array (or it throws), so this belt-and-suspenders throw never fires.
|
|
402
|
+
// * `c && c.bytes ? c.bytes : c` -- schema-cms surfaces every embedded certificate as an
|
|
403
|
+
// object carrying a `bytes` Buffer, so the raw-value fallback never fires.
|
|
404
|
+
module.exports = { verify: verify };
|
package/lib/ip-utils.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- no operator-facing namespace. Textual IP-literal validation, shared so a
|
|
6
|
+
// consumer (pki.lint's commonName-in-SAN check today; a future SAN iPAddress / name-
|
|
7
|
+
// constraint surface) does not hand-roll a partial regex that misses a valid IPv6 form.
|
|
8
|
+
//
|
|
9
|
+
// The IPv4 grammar is strict per-octet 0-255 (RFC 791); the IPv6 parser follows RFC 4291
|
|
10
|
+
// sec. 2.2 (8 groups of 1-4 hex, one "::" run compressing a contiguous zero run) plus the
|
|
11
|
+
// RFC 4291 sec. 2.5.5.2 / RFC 5952 sec. 5 IPv4-mapped + dual-stack "::ffff:1.2.3.4" tail.
|
|
12
|
+
// This mirrors the vetted validation in the sibling blamejs framework (lib/ip-utils.js);
|
|
13
|
+
// this toolkit keeps its own copy rather than take a runtime dependency (Hard rule #1).
|
|
14
|
+
|
|
15
|
+
// Strict RFC 791 dotted-quad: four 0-255 octets. Anchored + per-octet repeat-capped (no
|
|
16
|
+
// ReDoS on unbounded input).
|
|
17
|
+
var IPV4_RE = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
18
|
+
function isIPv4(s) { return typeof s === "string" && IPV4_RE.test(s); }
|
|
19
|
+
|
|
20
|
+
// Expand an IPv6 textual address to its 32-hex-character form, or null on any parse failure
|
|
21
|
+
// (bad hex group, group count != 8, more than one "::", an over-length group). Accepts the
|
|
22
|
+
// IPv4-mapped / dual-stack ".d.d.d.d" tail.
|
|
23
|
+
var IPV6_TEXT_MAX_LEN = 45; // 8 groups x4 hex + 7 colons + a dotted-quad tail headroom
|
|
24
|
+
function expandIpv6Hex(ip) {
|
|
25
|
+
if (typeof ip !== "string" || ip.length > IPV6_TEXT_MAX_LEN || ip.indexOf(":") === -1) return null;
|
|
26
|
+
// RFC 4291 sec. 2.5.5.2 IPv4-mapped / dual-stack: fold a trailing dotted-quad into two
|
|
27
|
+
// 16-bit hex groups before the pure-hex parse.
|
|
28
|
+
var dual = ip.match(/^(.*):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
29
|
+
if (dual) {
|
|
30
|
+
if (!isIPv4(dual[2])) return null;
|
|
31
|
+
var v4 = dual[2].split(".").map(Number);
|
|
32
|
+
ip = dual[1] + ":" + (((v4[0] << 8) | v4[1]).toString(16)) + ":" + (((v4[2] << 8) | v4[3]).toString(16));
|
|
33
|
+
}
|
|
34
|
+
var dbl = ip.split("::");
|
|
35
|
+
if (dbl.length > 2) return null;
|
|
36
|
+
var left = dbl[0] === "" ? [] : dbl[0].split(":");
|
|
37
|
+
var right = dbl.length === 2 ? (dbl[1] === "" ? [] : dbl[1].split(":")) : [];
|
|
38
|
+
if (dbl.length === 1 && left.length !== 8) return null;
|
|
39
|
+
var fill = 8 - left.length - right.length;
|
|
40
|
+
// A "::" run MUST compress at least one zero group (RFC 4291 sec. 2.2 / RFC 5952 sec.
|
|
41
|
+
// 4.2.2): a "::" with fill 0 is a full 8-group address that must not use compression.
|
|
42
|
+
if (dbl.length === 2 ? fill < 1 : fill !== 0) return null;
|
|
43
|
+
var groups = left, i;
|
|
44
|
+
for (i = 0; i < fill; i += 1) groups = groups.concat(["0"]);
|
|
45
|
+
groups = groups.concat(right);
|
|
46
|
+
if (groups.length !== 8) return null;
|
|
47
|
+
var hex = "";
|
|
48
|
+
for (i = 0; i < 8; i += 1) {
|
|
49
|
+
var g = groups[i];
|
|
50
|
+
if (g.length < 1 || g.length > 4 || !/^[0-9a-f]+$/i.test(g)) return null;
|
|
51
|
+
hex += g.toLowerCase().padStart(4, "0");
|
|
52
|
+
}
|
|
53
|
+
return hex;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Is `s` a syntactically valid IPv4 or IPv6 textual literal?
|
|
57
|
+
function isIpLiteral(s) { return isIPv4(s) || expandIpv6Hex(s) !== null; }
|
|
58
|
+
|
|
59
|
+
module.exports = { isIPv4: isIPv4, expandIpv6Hex: expandIpv6Hex, isIpLiteral: isIpLiteral, IPV4_RE: IPV4_RE };
|
package/lib/lint.js
CHANGED
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
* @card Lint a certificate against RFC 5280 + CABF TLS BR, in pure JS.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
var net = require("net");
|
|
27
26
|
var frameworkError = require("./framework-error");
|
|
28
27
|
var asn1 = require("./asn1-der");
|
|
29
28
|
var oid = require("./oid");
|
|
30
29
|
var x509 = require("./schema-x509");
|
|
31
30
|
var pkix = require("./schema-pkix");
|
|
32
31
|
var C = require("./constants");
|
|
32
|
+
var ipUtils = require("./ip-utils");
|
|
33
33
|
|
|
34
34
|
var MS_PER_DAY = C.TIME.days(1);
|
|
35
35
|
|
|
@@ -215,10 +215,11 @@ function _dnsNameProblem(s) {
|
|
|
215
215
|
return null;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
// A genuine IPv4 or IPv6 literal
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
218
|
+
// A genuine IPv4 or IPv6 literal -- a CN validated against an iPAddress SAN rather than a
|
|
219
|
+
// dNSName. Routed through the shared strict validator (no node:net, so the toolkit needs no
|
|
220
|
+
// networking module): a per-octet-bounded dotted quad, or a full RFC 4291 IPv6 form
|
|
221
|
+
// (including the IPv4-mapped / dual-stack tail). Rejects 999.999.999.999 and "api:443".
|
|
222
|
+
function _looksLikeIp(s) { return ipUtils.isIpLiteral(s); }
|
|
222
223
|
|
|
223
224
|
function _sanDnsNames(ctx) {
|
|
224
225
|
var d = ctx.decode("subjectAltName");
|
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:7f3139fc-e726-4373-afdf-0e304562ee70",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-13T21:48:02.712Z",
|
|
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.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.14",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.14",
|
|
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.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.14",
|
|
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.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.14",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|