Security model
Every certificate, PEM text and DER blob pkinative reads is treated as attacker-controlled. This guide explains what that means in practice: strict decoding, bounded resources, typed failures, and a hard line around cryptography.
Refuse ambiguity #
A signature covers bytes. If two parsers can read the same bytes as two different values, an attacker can have one of them verify what the other one displays (CWE-436). DER exists to rule that out, and pkinative decodes it the way X.690 ยง10โ11 defines it: indefinite lengths, non-minimal lengths and tags, constructed strings, BOOLEAN values other than 0x00 and 0xFF, non-minimal INTEGERs, non-zero BIT STRING padding and trailing bytes are all refused with a stable code.
BER is accepted only when the caller asks for it with encodingRules: 'ber' โ as real CMS needs โ and every tolerated construct is reported once as a diagnostic. encodeAsn1Node refuses to re-encode a BER-only tree as if it were DER.
One exception is deliberate: a DEFAULT value encoded explicitly (cA FALSE, critical FALSE, version v1) reads the same in every parser, and Go, OpenSSL and BoringSSL accept it; pkinative reads it with the PKI_DIAG_DEFAULT_ENCODED diagnostic, and strict: true refuses it.
Bound every resource #
Every loop over input consults a named limit. The decoder is iterative, so nesting depth is a limit and never a call-stack overflow. Every declared length is checked against the remaining input before anything is allocated.
| Limit | Default | CWE | Guards |
|---|---|---|---|
maxInputBytes |
64 MiB | CWE-400 | The size of one DER input, or the length of one PEM text |
maxDepth |
64 | CWE-674 | The nesting depth of constructed values |
maxNodes |
200 000 | CWE-770 | The values decoded from one input |
maxIntegerBytes |
8 192 | CWE-407 | The content length of one INTEGER converted to a bigint |
maxOidBytes |
256 | CWE-400 | The content length of one OBJECT IDENTIFIER |
maxBerSegments |
10 000 | CWE-400 | The segments joined from one BER constructed string |
maxPemBlocks |
10 000 | CWE-400 | The blocks read from one PEM text |
maxExtensions |
256 | CWE-400 | The extensions of one certificate; of one certification request, its attributes, their values and the extensions it requests |
maxGeneralNames |
10 000 | CWE-400 | The GeneralName entries of one field |
maxNameAttributes |
1 024 | CWE-400 | The attributes of one distinguished name |
maxPolicies |
1 024 | CWE-400 | The policies or policy mappings of one extension |
maxChainLength |
10 | CWE-400 | The certificates in one path, the leaf and the anchor included |
maxPolicyNodes |
4 096 | CWE-770 | The live nodes of the RFC 5280 valid_policy_tree |
maxRevokedCertificates |
1 000 000 | CWE-400 | The entries walked in one CRL โ walked lazily, never decoded into nodes |
maxOcspSingleResponses |
256 | CWE-400 | The SingleResponse entries of one OCSP response |
maxPathsExplored |
1 000 | CWE-400 | The candidate paths explored while building one โ the denial-of-service bound of ยง6 |
maxSignerInfos |
64 | CWE-400 | The signers of one SignedData, each costing a signature verification |
maxAttributes |
256 | CWE-400 | The attributes in one attribute set โ a CMS signer's, a PKCS#8 key's, a PKCS#12 bag's |
maxCmsCertificatesAndCrls |
1 024 | CWE-400 | The certificates and revocation entries one SignedData carries |
maxKdfIterations |
10 000 000 | CWE-400 | The PBKDF2 iteration count honoured in one derivation โ declared by the file, run by the host |
maxPkcs12Bags |
4 096 | CWE-400 | The SafeBags read from one PKCS#12, across every SafeContents |
maxPkcs12KdfIterations |
10 000 000 | CWE-400 | The PBKDF2 iterations one PKCS#12 costs in total โ its MAC, every encrypted SafeContents and every shrouded key |
Exceeding a limit throws PkiLimitError with code PKI_LIMIT_EXCEEDED and the limit, configured and observed values. Override per call with options.limits; DEFAULT_PKI_LIMITS holds the defaults.
Fail in one way #
A structural failure throws a PkiError subclass โ PkiEncodingError for X.690, OID and PEM syntax, PkiCertificateError for the RFC 5280 structure, PkiLimitError for a limit, PkiCryptoError for a Web Crypto operation that could not run, PkiCmsError and PkiKeyError for CMS and key containers โ with a stable code. A conformance concern is a diagnostic. The two never mix: a diagnostic never hides a structural failure, and a thrown error never carries a merely pedantic concern. Any other exception escaping on malformed input โ a TypeError, a RangeError โ is a bug, and the fuzzing suites plus the conformance gate check that none does across every truncation, every single-octet mutation and 30 361 unique x509-limbo certificates.
Decoded names and OIDs are kept in arrays and Maps, never as plain object keys, so a crafted __proto__ cannot reach an object prototype (CWE-1321). formatDistinguishedName escapes control characters, so a crafted name cannot drive the terminal that prints it.
Cryptography pkinative does not own #
pkinative never implements secret-dependent cryptography in TypeScript: no signing arithmetic, no key generation, no RSA modular exponentiation, no elliptic-curve scalar multiplication. Every signature it verifies and every signature it creates โ on a certificate, a certification request or a CMS SignedData โ is one Web Crypto call with the caller's key, and so is every password-based derivation, MAC and decryption of a PKCS#8 or PKCS#12 container; the arithmetic runs in the host's native implementation, not in JavaScript BigInts. The hashes pkinative computes itself โ SHA-1, SHA-256, SHA-384 and SHA-512 for fingerprints, SHAKE256 for Ed448 CMS signers โ run over public data only; shake256(data, outputLength) is the one exported as a general-purpose digest. src/crypto/webcrypto.ts is the only module that names those operations, and it refuses generateKey, exportKey, deriveBits, encrypt and wrapKey for good. The SHA-1, SHA-256, SHA-384 and SHA-512 code exists for synchronous fingerprints of public data only and is not exported as general-purpose hashing.
The consequence is a split worth knowing: the structure around a signature is pkinative's to check โ the DER of an ECDSA signature, the algorithm named inside and outside the signed bytes, the key's curve โ and the arithmetic is the host's, RSA padding, Ed25519 canonicity and the ECDSA scalar range included. The CVE-class corpus below says which side stops each known attack.
This is a deliberate departure from the code pkinative grew out of: pdfnative's pure-JavaScript RSA and ECDSA use BigInt arithmetic that is not constant-time, and are not ported. The reasoning behind each permanent refusal โ a Web Crypto operation, a legacy PKCS#12 scheme, an algorithm Web Crypto lacks โ is recorded in the architecture decision records.
No reach outside the engine #
src/ has no filesystem, network, process or dynamic-import access, no eval and no Function, and no module-level side effects โ all enforced from the syntax tree by the architecture test. The package is tree-shakeable, and npm run verify:bundle proves that a certificate parser bundle carries no PEM code, no hash and no OID name registry.
CVE classes #
Every published vulnerability of a PKI library whose class pkinative could share is replayed against the engine on every gate run: tests/security/cve-classes.test.ts rebuilds each one from synthetic input โ raw DER, keys generated in the test, forgeries computed with public arithmetic โ and asserts the refusal, the code or the verdict. docs/data/cve-classes.json records, for each identifier, the library it hit, the class, the CWE, whether it applies, why, and the test that proves it; the cve-class-parity rule of npm run verify:docs fails the build when the registry, the tests and this table disagree.
Applies is no when pkinative has the code path and refuses the attack, n.a. when the class cannot exist here (no DSA, no padding parser, no memory-unsafe code), and yes when the audit found it. Stopped by is honest about delegation: where it says the host's Web Crypto, pkinative hands the bytes over unchanged and the test proves the host refuses them โ a host that stopped refusing would turn the suite red.
| Identifier | Library | Class | Applies | Stopped by |
|---|---|---|---|---|
| CVE-2020-0601 | Windows CryptoAPI (crypt32.dll) | CurveBall: an ECC certificate carrying a trusted root's public point under explicit curve parameters with an attacker-chosen generator is trusted as that root | no | pkinative |
| CVE-2024-42461, GHSA-49q7-c7j4-3p7m | elliptic (npm) 5.2.1โ6.5.6 | BER-encoded ECDSA signatures accepted (signature malleability) | no | pkinative |
| CVE-2020-14966 | jsrsasign (npm) โค 8.0.18 | ECDSA signature malleability through unchecked sequence lengths and appended zero octets | no | pkinative |
| CVE-2024-42460, GHSA-977x-g7h5-7qgw | elliptic (npm) 2.0.0โ6.5.6 | ECDSA r and s not checked for a redundant leading octet or a negative value | no | pkinative |
| CVE-2020-13822 | elliptic (npm) 6.5.2 | ECDSA malleability through integer overflow (r + n, s + n) | no | pkinative, then the host |
| CVE-2022-21449 | Oracle Java SE 17.0.2 and 18 | ECDSA signature with r = s = 0 accepted | no | the host's Web Crypto |
| CVE-2022-24771 | node-forge (npm) < 1.3.0 | RSA PKCS#1 v1.5 verification lenient about the DigestInfo algorithm structure | n.a. | the host's Web Crypto |
| CVE-2022-24773 | node-forge (npm) < 1.3.0 | RSA PKCS#1 v1.5 verification does not check the DigestInfo ASN.1 structure | n.a. | the host's Web Crypto |
| CVE-2026-33894, GHSA-ppp5-5v6c-4jwp | node-forge (npm) < 1.4.0 | RSA PKCS#1 v1.5 signature forgery through an extra ASN.1 field in the DigestInfo | n.a. | the host's Web Crypto |
| CVE-2022-24772 | node-forge (npm) < 1.3.0 | RSA PKCS#1 v1.5 verification ignores trailing garbage after the DigestInfo | n.a. | the host's Web Crypto |
| CVE-2021-30246 | jsrsasign (npm) โค 10.1.13 | some invalid RSA PKCS#1 v1.5 signatures (malformed encoded message) recognised as valid | n.a. | the host's Web Crypto |
| CVE-2006-4339 | OpenSSL before 0.9.7k, and 0.9.8 before 0.9.8c | Bleichenbacher forgery of PKCS#1 v1.5 signatures under an e = 3 key, computed without the private key | n.a. | the host's Web Crypto |
| CVE-2020-14968, GHSA-q3gh-5r98-j4h3 | jsrsasign (npm) 3.0.0โ8.0.15 | RSA signature with prepended zero octets accepted | no | the host's Web Crypto |
| CVE-2026-4598 | jsrsasign (npm) < 11.1.1 | infinite loop in modular inversion on a zero or negative input | no | pkinative |
| CVE-2026-4602 | jsrsasign (npm) < 11.1.1 | negative exponents mishandled, breaking signature verification | no | pkinative |
| CVE-2026-4603 | jsrsasign (npm) < 11.1.1 | division by zero in RSA public-key operations on a zero modulus | no | pkinative |
| CVE-2024-42459, GHSA-f7q4-pwc6-w24p | elliptic (npm) 4.0.0โ6.5.6 | EdDSA signature length not checked, so zero octets can be appended or removed | no | the host's Web Crypto |
| CVE-2026-33895, GHSA-q67f-28xg-22rw | node-forge (npm) < 1.4.0 | Ed25519 signature with S โฅ L (non-canonical) accepted | no | the host's Web Crypto |
| CVE-2024-48949, GHSA-434g-2637-qmqr | elliptic (npm) < 6.5.6 | EdDSA verify omits the S < n range check | no | the host's Web Crypto |
| CVE-2024-24783 | Go crypto/x509 | Certificate.Verify panics on a certificate with an unknown public-key algorithm in the chain | no | pkinative |
| CVE-2024-29857 | Bouncy Castle (Java) < 1.78 | explicit F2m curve parameters cause excessive CPU when a certificate is imported | no | pkinative |
| CVE-2023-0217 | OpenSSL 3.0 | NULL dereference checking a malformed DSA public key (EVP_PKEY_public_check) | n.a. | by construction |
| CVE-2026-4599 | jsrsasign (npm) 7.0.0 to before 11.1.1 | biased random scalar generation (DSA/ECDSA signing) leaks the private key | n.a. | by construction |
| CVE-2026-4600 | jsrsasign (npm) < 11.1.1 | DSA domain parameters not validated before verification | n.a. | by construction |
| CVE-2026-4601 | jsrsasign (npm) < 11.1.1 | DSA signing does not retry when r or s is zero, leaking the private key | n.a. | by construction |
| CVE-2025-66031, GHSA-554w-wpv2-vw27 | node-forge (npm) < 1.3.2 | unbounded recursion in ASN.1 decoding | no | pkinative |
| CVE-2025-66030, GHSA-65ch-62r8-g69g | node-forge (npm) < 1.3.2 | OID arc truncated to 32 bits, so a crafted OID reads as another (OID spoofing) | no | pkinative |
| CVE-2023-2650 | OpenSSL 1.0.2, 1.1.1, 3.0 and 3.1 | very slow translation of a crafted OID with huge arcs (OBJ_obj2txt) | no | pkinative |
| CVE-2025-12816, GHSA-5gfm-wpxj-wjgq | node-forge (npm) < 1.3.2 | ASN.1 schema validation desynchronised by optional fields (interpretation conflict) | no | pkinative |
| CVE-2023-33202 | Bouncy Castle (Java) < 1.73 | denial of service in the PEM parser on crafted input | no | pkinative |
| CVE-2024-0727 | OpenSSL 1.0.2, 1.1.1, 3.0, 3.1 and 3.2 | NULL dereference on a PKCS#12 whose ContentInfo content is absent | no | pkinative |
| CVE-2023-0216 | OpenSSL 3.0 | invalid pointer dereference loading malformed PKCS#7 data (d2i_PKCS7) | no | pkinative |
| CVE-2022-36083 | jose (npm) | attacker-chosen PBKDF2 iteration count (the JWE p2c header) costs the receiver unbounded CPU | yes | pkinative |
| CVE-2026-33896, GHSA-2328-f5f3-gj25 | node-forge (npm) โค 1.3.3 | an intermediate without basicConstraints or keyUsage accepted as a CA | no | pkinative |
| CVE-2023-0286 | OpenSSL 1.0.2, 1.1.1 and 3.0 | type confusion on x400Address in GeneralName comparison | n.a. | by construction |
| CVE-2022-3602 | OpenSSL 3.0 | buffer overrun decoding a punycode email address in name-constraint checking | n.a. | by construction |
| CVE-2022-3786 | OpenSSL 3.0 | buffer overrun (variable length) decoding a punycode email address in name-constraint checking | n.a. | by construction |
| CVE-2024-45341 | Go crypto/x509 | a URI with an IPv6 zone identifier wrongly satisfies a URI name constraint | no | pkinative |
| CVE-2025-61727 | Go crypto/x509 | an excluded subdomain constraint does not restrict a wildcard dNSName | no | pkinative |
| CVE-2023-0464 | OpenSSL 1.0.2, 1.1.1, 3.0 and 3.1 | exponential growth of the policy tree on crafted policy mappings (denial of service) | no | pkinative |
| CVE-2023-0465 | OpenSSL 1.0.2, 1.1.1, 3.0 and 3.1 | invalid certificate policies in a leaf silently ignored | no | pkinative |
| CVE-2009-2408 | Mozilla NSS < 3.12.3 | a NUL character in the commonName matches the name before it | no | pkinative |
| CVE-2021-42574 | Unicode Bidirectional Algorithm | bidirectional control characters reorder rendered text | no | pkinative |
CVE-2022-36083 is the one that applied: a PKCS#12 with no MAC could make openPkcs12 run one ten-million-iteration PBKDF2 per entry. maxPkcs12KdfIterations bounds the work of a whole file and refuses it at parse, before anything is derived.
Coverage-guided fuzzing complements the replays: ClusterFuzzLite runs eight targets in fuzz/ โ the X.690 decoder, X.509, PEM and OIDs, CMS SignedData, CRLs, OCSP responses, RFC 3161 timestamps, and PKCS#12 with PKCS#8 โ each seeded with a real structure of its grammar and each executed against src/ on every gate run by tests/fuzzing/targets.test.ts.
Reporting a vulnerability #
Report privately, never in an issue: through GitHub's private vulnerability reporting, or by email to security@pkinative.dev. See SECURITY.md for scope and response times.