Home โ€บ Guides โ€บ Security model

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.