Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Changed
- npm and JSR packages include
CHANGELOG.mdin the published tarball (package.jsonfiles,jsr.jsonpublish.include).
Fixed
- Two
site/guide/keys.mdLiveCode examples used TypeScript parameter types (key: CryptoKey,bytes: Uint8Array). LiveCode injects examples as browser JS modules, so Run failed withmissing ) after argument listandUnexpected token ':'. The annotations are removed.
0.14.0 - 2026-07-29
Everything that already worked but had no export: a micro509/crypto entrypoint for detached signatures, extension decoders, RFC 5280 §7.1 distinguished-name comparison, PKCS#7 signer resolution, and PBES2 inspection. Plus three wrongful-acceptance fixes under Security and a stricter typed-contract pass.
Added
ParsedCertificate.issuerAltNamesdecodes the issuerAltName extension (RFC 5280 §4.2.1.7, OID 2.5.29.18) with the subjectAltName GeneralNames decoder.checkCertificateRevocationAgainstCrlreportscoveredReasonson agoodvalue: the RFC 5280 §6.3.3 (d) interim_reasons_mask computed from the matched distribution point'sreasonsand the CRL'sonlySomeReasons.verifyPkcs7SignedDatasuccess values carrysigners, pairing each SignerInfo with the certificate that verified its signature (VerifiedPkcs7Signer), so callers can check trust, EKU, or identity of the actual signer without reimplementing the issuerAndSerial DN match. (https://github.com/kjanat/micro509/issues/67)micro509/crypto, a new entrypoint for detached signatures:verifySignaturechecks raw bytes against a signer's SubjectPublicKeyInfo across RSA PKCS#1 v1.5, RSA-PSS, ECDSA P-256/P-384/P-521, and Ed25519, retrying the alternate DER/raw ECDSA encoding;signDatasigns with a WebCrypto key and returns the signature beside itsAlgorithmIdentifiermaterial;ecdsaSignatureDerToRawandecdsaSignatureRawToDerconvert between the DERECDSA-Sig-ValueX.509 and CMS embed and the rawr || sform WebCrypto and JOSE use. (https://github.com/kjanat/micro509/issues/65)micro509/x509exports its extension-value decoders as thedecode*inverses of the existing encoders (decodeKeyUsage,decodeBasicConstraints,decodeSubjectAltNames, …,decodeAuthorityKeyIdentifier), the RFC 5280 §7.1 semantic DN comparison (compareDistinguishedNames,canonicalDnKey,isWithinDirectoryNameSubtree),parseDistinguishedNameDerfor a bareName, theparseCertificateFromSource/parseCertificatesFromSourceinput normalizers,subjectKeyIdentifier(RFC 5280 §4.2.1.2 method (1)), and the IP helpers name-constraint inputs demand (parseIpAddressToBytes,decodeIpAddress,allOnesMaskForIpAddress,normalizeIpAddress). (https://github.com/kjanat/micro509/issues/64)isSelfIssuedCertificatefrommicro509/verify: the RFC 5280 §7.1 subject-equals-issuer predicate path validation already used internally. (https://github.com/kjanat/micro509/issues/65)inspectEncryptedPkcs8Derreads the PBES2 parameters of an encrypted PKCS#8 key without the password (iterations, salt, PRF, AES-CBC variant, IV), andmicro509/keysexportsparsePbes2AlgorithmIdentifierwith thePbes2Parameterstypes behind it. (https://github.com/kjanat/micro509/issues/66)revocationReasonFromCodemaps a raw CRLReason integer to theRevocationReasonname the CRL path already returns, giving OCSP'srevocationReasonCodethe same vocabulary. (https://github.com/kjanat/micro509/issues/68)micro509/resultexportsrethrowIfInvariant, the boundary guard the library's own catch blocks use to keep programmer errors from being flattened into malformed-input failures. (https://github.com/kjanat/micro509/issues/69)
Changed
Tighten four exported TypeScript contracts to make invalid states unrepresentable. These changes can require source updates:
CrlEncoderErrorCodenow contains only'distribution_point_full_name_empty'.'distribution_point_name_conflict'and'distribution_point_name_empty'are removed becauseDistributionPointNameis now a discriminated union that cannot express either invalid shape.ParsedOcspSingleResponseis now discriminated bycertStatus.revokedAtandrevocationReasonCodeare available only after narrowing tocertStatus === 'revoked';revokedAtis then required.CreateOcspCertStatusInputrejectsrevokedAtandrevocationReasonCodeunlesscertStatusis'revoked', including when a previously declared object is passed instead of an object literal.CreateSelfSignedCertificateInputno longer accepts bothkeyPairandalgorithm. SupplykeyPairto reuse existing keys, oralgorithmto generate a new pair.
tsif (singleResponse.certStatus === 'revoked') { singleResponse.revokedAt; // Date singleResponse.revocationReasonCode; // number | undefined } await createSelfSignedCertificate({ subject, keyPair }); await createSelfSignedCertificate({ subject, algorithm: { kind: 'ecdsa', curve: 'P-256' }, });ParsedPkcs7SignedData.certificates: readonly ParsedCertificate[]becomescertificateChoices: readonly ParsedCertificateChoice[], modelling RFC 5652 §10.2.2 CertificateChoices as a discriminated union rather than discarding four of its five alternatives.certificatecarries the decoded X.509;extendedCertificate([0], obsolete),attributeCertificateV1([1], obsolete),attributeCertificateV2([2]), andother([3], with itsotherCertFormatOID decoded) keep their DER including the context tag, so a CertificateSet round-trips and a caller can tell an X.509-only bag from a mixed one. A certificate set entry whose tag is none of these is rejected asmalformed; previously any non-SEQUENCE element was silently dropped.parsePkcs7CertBagDerandparsePkcs7CertBagPemstill returnreadonly ParsedCertificate[], now the X.509 projection of the set.Builder input-validation now throws a
ResultErrorcarrying a stable machine-readablecoderather than a bareError.createCertificate, theencode*extension helpers, distinguished-name encoding, and CRL/IDP encoding reject invalid construction input (an emptykeyUsage, a duplicate policy OID, aDisplayTextout of range, an invalid country code) with a coded throw thatisResultErrordetects anderror.codediscriminates. Codes are per-operation unions (ExtensionEncoderErrorCode,NameEncoderErrorCode,CrlEncoderErrorCode,CreateCertificateErrorCode). DER decode guards and exhaustiveness invariants keep throwing a plainError. The thrown message gains acode:prefix.AuthorityInformationAccess.uri: stringbecomeslocation: GeneralName, the full accessLocation RFC 5280 §4.2.2.1 defines. The parser threwUnsupported authorityInfoAccess location tagfor any location that was not a URI, so a certificate carrying a directoryName or dNSName accessLocation (both conformant) failed to parse entirely. An OCSP entry requires a URI location (its discovery reads only URIs);directoryNameis defined forcaIssuers, and other GeneralName forms are syntactically representable. GeneralName encoding and parsing now reject any tag, class, or constructedness that is not one of the nine RFC 5280 §4.2.1.6 alternatives (x400Address [3],ediPartyName [5], andregisteredID [8]are preserved as unknown). The IA5String alternatives (dNSName,rfc822Name,uniformResourceIdentifier) reject non-ASCII input on encode and decode. (https://github.com/kjanat/micro509/pull/78)
Fixed
- Signature verification accepts an absent parameters field on the sha256WithRSAEncryption, sha384WithRSAEncryption, and sha512WithRSAEncryption AlgorithmIdentifiers. RFC 4055 §5 requires the parameters to be NULL and requires implementations to accept them absent as well as present; every surface that resolves a signature algorithm reported
unsupported_signature_algorithm_parametersfor the absent encoding, so a conformant certificate, CRL, OCSP response, CSR, or PKCS#7 signature that omits the NULL could not be verified. Parameters that are neither absent nor a DER NULL are still rejected, and signatures this library produces still carry the NULL. verifyCertificateChainandvalidateCandidatePathno longer reject a self-issued leaf that another key signed. RFC 5280 §3.2 calls a self-issued certificate self-signed only when the public key it binds verifies its signature, but the guard behindallowSelfSignedLeaffired on matching issuer and subject DNs alone. The RFC 8410 §10.2 example certificate is exactly that case, a self-issued X25519 certificate signed by a separate Ed25519 key, and failed withself_signed_leaf_not_allowedwhen anchored on the §10.1 key. The guard now verifies the leaf against its own public key first and only reportsself_signed_leaf_not_allowedwhen that verification succeeds.- Legacy OpenSSL-style encrypted PEM (
Proc-Type: 4,ENCRYPTED) parsing accepts an encapsulated header with no space after the colon (DEK-Info:AES-256-CBC,…). The parser keyed on': ', so a conformant no-space header ended the header scan early and folded into the base64 body. (https://github.com/kjanat/micro509/pull/89) - Legacy encrypted PEM parsing unfolds folded encapsulated headers. RFC 1421 §4.6 defines encapsulated header folding by reference to RFC 822, and its Figure 2 folds a
Key-Info:field across two lines. Every line was treated as a complete header, so a folded field was misparsed and its continuation fell into the base64 body. A line opening with an RFC 822 §3.3LWSP-char(SPACE or HTAB) now continues the preceding field; per §3.1.1 unfolding drops the CRLF and keeps the whitespace, so the field-body and theProc-TypeandDEK-Infofield comparisons strip the SPACE and HTAB unfolding leaves behind. Only those two characters are stripped:String.prototype.trimalso removes VT, FF, NBSP, and every UnicodeZs, none of which RFC 822 admits, so a header such asProc-Type: 4,<NBSP>ENCRYPTEDis rejected rather than read as4,ENCRYPTED. (https://github.com/kjanat/micro509/issues/92) - Certificate and CSR builders reject RFC 5280 MUST-NOT constructions with coded throws.
pathLenConstraintrequires the keyUsage extension to assertkeyCertSign; absent, empty, orkeyCertSign-less keyUsage is rejected (§4.2.1.9,path_length_requires_key_cert_sign). An empty subject DN requires a critical subjectAltName carrying at least one non-empty GeneralName; an empty typed value ({ type: 'dns', value: '' }), an emptysubjectAltNamesarray, and a criticalcustomExtensionsSAN whose value holds no usable GeneralName are all rejected (§4.2.1.6,empty_subject_requires_subject_alt_name), sosubject: {}can no longer sign a certificate with no identity. Encoding a GeneralName with an emptydNSName,rfc822Name, URI, or SRV value is rejected (§4.2.1.6,empty_general_name_value). AcRLIssuer, when present, may only containdirectoryNameentries, rejecting a non-DN entry or a directoryName smuggled through anunknowngeneral name; anameRelativeToCRLIssuerdistribution point additionally permits only one (§4.2.1.13,distribution_point_crl_issuer_not_directory_name,distribution_point_relative_name_multiple_crl_issuers). Known extensions supplied throughcustomExtensionsparticipate in these cross-field checks. AcustomExtensionsentry carrying a known OID must decode as that extension, rather than reaching the wire as opaque bytes the parser then rejects (malformed_known_extension_value). Extension OIDs resolve by their encoded value, so a non-canonical spelling such as2.5.029.17is the same extension as2.5.29.17for registry lookup, certificate-versus-CSR context restrictions, and duplicate detection; the diagnostic still quotes the OID as submitted. A customcRLDistributionPointspayload runs the same §4.2.1.13 cRLIssuer checks as the typed field, since decoding proves structure but not the profile the builder promises.validateOidalso rejects an OID that parses as decimals but breaks the X.660 arc bounds (3.1,1.40) withinvalid_oidrather than an uncodedError. (https://github.com/kjanat/micro509/pull/88) - CRL applicability follows the RFC 5280 §6.3.3 relying-party algorithm in three places it diverged. A certificate without a CRLDP extension accepts a CRL whose issuing distribution point names the certificate issuer or one of its issuerAltName entries, per the §6.3.3 assumed-distribution-point rule; such a CRL previously reported
non_applicable. A distribution point that omitsdistributionPointmatches the CRL IDP name against itscRLIssuernames (§6.3.3 (b)(2)(i)); an in-scope indirect CRL was previously rejected. Reason coverage uses the §6.3.3 (d) interim_reasons_mask, unioned across every matching distribution point, instead of the CRL'sonlySomeReasonsalone, so a distribution point scoped to a subset of reasons no longer grants full coverage. Every consumer of a CRLgood—checkChainRevocation,checkCertificateRevocation, delegated OCSP-responder validation, and recursive CRL-signer validation — now treats a reason-scopedgoodas definitive only once the applicable CRLs together cover all eight reasons; a revoked verdict from any CRL still wins immediately. GeneralName applicability comparisons apply the RFC 5280 name comparison rules: dNSName is case-insensitive (§7.2), the rfc822Name host-part is case-insensitive (§7.5), anotherNameSRV-ID is case-insensitive in both halves (RFC 4985 §2), and a uniformResourceIdentifier is prepared per §7.4 — IDN labels to ASCII Compatible Encoding, lowercased scheme and host, percent-encoding and path segment normalization, and scheme-based normalization forftp,http,https, andldap. Certificate and CRL parsing now share one canonical GeneralName decoder, so an SRV-ID matches across issuerAltName and the IDP.verifyCertificateChainrecognises a critical issuerAltName rather than rejecting it. A delta-CRLremoveFromCRLentry for an expired certificate now measures expiry against the delta'sthisUpdate(§5.2.4), not the evaluation time. (https://github.com/kjanat/micro509/pull/87) importEncryptedPkcs1PemandimportEncryptedSec1Pemreport a wrong password asinvalid_passwordrather than occasionally asmalformed. Traditional PEM encrypts with unauthenticated AES-CBC, so a wrong key clears the PKCS#7 padding check roughly once in every 256 attempts and yields random plaintext; the decrypted bytes are now required to parse as anRSAPrivateKeyorECPrivateKey, which is the check the PBES2 path already applied.importPkcs8Deraccepts aOneAsymmetricKey(RFC 5958 §2 / RFC 8410 §7) that carries bothattributes [0]andpublicKey [1]. The parser capped at four elements, so a five-element v2 key that OpenSSL and Node WebCrypto both accept returnedmalformed. The tail is now validated structurally rather than by ASN.1 class alone:attributes [0]must be a constructedSET OF, an optionalpublicKey [1]must be a primitive BIT STRING after it, the version is coupled to the public key's presence (v2iff present), and well-formed unknown extension additions are tolerated per the type's X.680 extensibility marker.- SEC1
ECPrivateKeyparsing rejects a version other than 1, comparing content octets (RFC 5915 §3, "version SHALL be ... one"). Only the tag was checked, so version 0 or 2 was deferred to the WebCrypto backend as a misleading error. - PBES2 decryption no longer rejects a PBKDF2 salt shorter than eight bytes. RFC 8018 §4.1 makes the eight-octet minimum a "should" for salt selection and says the salt need not be checked on receipt, so
openssl pkcs8 -saltlen 4could not be decrypted. The encrypt path keeps the minimum. (https://github.com/kjanat/micro509/pull/85) - Distinguished-name encoding enforces the RFC 5280 Appendix A.1 attribute constraints: no attribute value may be empty (
SIZE (1..ub-…)), andcommonName/organization/organizationalUnit/title/serialNumbercap at 64 characters,locality/stateat 128,emailAddressat 255, andsurname/givenNameat 32768 (ub-name). Only the country exact-length-2 rule was enforced before.streetstays unbounded (no A.1 bound applies). Bounds count code points. createCertificaterejects an empty issuer distinguished name, per RFC 5280 §4.1.2.4 ("The issuer field MUST contain a non-empty distinguished name"). An empty subject with a critical subjectAltName stays valid (§4.1.2.6). (https://github.com/kjanat/micro509/pull/84)- PKCS#12
MacDataomitsiterationswhen it equals itsDEFAULT 1, and the parser accepts a two-elementMacData, defaultingiterationsto 1 (RFC 7292 §4, X.690 §11.5). A conformant PFX with iteration count 1 previously failed to parse. - PBES2
PBKDF2-paramsomits theprfwhen it is theDEFAULTalgid-hmacWithSHA1(RFC 8018 A.2, X.690 §11.5);keyLength, being OPTIONAL rather than DEFAULT, is still emitted.exportEncryptedPkcs8Der(key, { prf: 'HMAC-SHA-1' })produced a non-DER structure. (https://github.com/kjanat/micro509/pull/83) - PKCS#7/CMS
SignedDataemits SHA-2 digestAlgorithmIdentifiers with absent parameters, per RFC 5754 §2 (a MUST). BothdigestAlgorithmsand eachSignerInfo.digestAlgorithmcarried an explicit05 00NULL. createPkcs7CertBagorders thecertificatesCertificateSetcanonically (DER SET OF, X.690 §11.6), matchingcreatePkcs7SignedData. It concatenated certificates in caller order, so the output was not valid DER and depended on input order. (https://github.com/kjanat/micro509/pull/82)- PEM decoding handles every RFC 7468 §3 newline convention (
CRLF,CR,LF).pemDecodeandsplitPemBlocksstripped\routright, which joins every line of a CR-only file into one, so such a file failed to decode. splitPemBlocksaccepts RFC 7468 labels with an internal-separator and no longer discards unrelated blocks in the same file when it meets a label it does not recognise.pemEncodeemits the RFC 7468 strict trailing end-of-line, so concatenating two blocks no longer produces-----END … ----------BEGIN …-----, whichopenssl storeutlrejects. (https://github.com/kjanat/micro509/pull/81)subjectAltNameparsing rejects an empty or non-SEQUENCE extension value, per RFC 5280 §4.2.1.6 (GeneralNames ::= SEQUENCE SIZE (1..MAX)). An empty SAN previously decoded to[], indistinguishable from an absent extension, so common-name fallback suppression did not engage.directoryName [4]now requires exactly one explicit X.501 Name with valid RDN and attribute structure instead of repairing malformed implicit encodings, including in CRL GeneralNames.extendedKeyUsageparsing rejects an empty SEQUENCE and any child that is not an OBJECT IDENTIFIER, per RFC 5280 §4.2.1.12.decodeObjectIdentifierran on every child regardless of tag, so30 03 02 01 01fabricated the OID0.1from an INTEGER. (https://github.com/kjanat/micro509/pull/80)- Extension encoders reject input RFC 5280 forbids rather than emitting non-conformant DER: an empty
keyUsage(§4.2.1.3),extendedKeyUsage(§4.2.1.12),authorityInfoAccess/cRLDistributionPoints(§4.2.2.1, §4.2.1.13) ornameConstraints(§4.2.1.10) SEQUENCE, a duplicate certificate policy OID compared by encoded identity so leading-zero aliases collide (§4.2.1.4), a policy qualifier reusing the built-incpsoruserNoticeOID in the opaqueoidvariant (§4.2.1.4), aDisplayTextoutside SIZE (1..200) (§4.2.1.4), and an IP name constraint whose address and mask do not total 8 or 32 octets (§4.2.1.10). Each previously encoded a structure the library's own parser, or OpenSSL, rejects. (https://github.com/kjanat/micro509/pull/79) - A
directoryNameSubjectAltName or name constraint now encodes the complete Name TLV inside[4], per RFC 5280 §4.2.1.6 (Name is an untagged CHOICE, so[4]is EXPLICIT). The encoder stripped the Name's SEQUENCE header, emittinga4 12 31 10 ...where OpenSSL emitsa4 14 30 12 31 10 .... - An
otherNameSubjectAltName now decodes with the type-id and value as the direct children of[0], per RFC 5280 §4.2.1.6 (otherName [0]is IMPLICIT, so[0]replaces the SEQUENCE tag). The parser required an inner SEQUENCE, so any realotherName(an SRV-ID, a Microsoft UPN) failed the whole certificate parse; the SRV-ID encoder emitted the same non-conformant nesting. A structurally validotherNamewith an unsupported type is preserved as{ type: 'unknown' }, but a malformedotherNameenvelope or a malformed value of a recognisedid-on-dnsSRVis rejected rather than erased tounknown. Path validation rejects a criticalsubjectAltNamecarrying a GeneralName the verifier cannot interpret (RFC 5280 §4.2), while a non-critical one keeps the unknown entry. (https://github.com/kjanat/micro509/pull/77) - OCSP responses now encode
ResponderIDbyKeyas[2]EXPLICIT wrapping an OCTET STRING and every time field as GeneralizedTime, per RFC 6960 Appendix B.1. ThebyKeyresponder was written as[2]IMPLICIT over the raw hash and the times as UTCTime, so OpenSSL and Go'scrypto/ocspcould not parse a response this library produced. The parser reads the EXPLICIT form and requires thebyKeyhash to be a 20-byte SHA-1 digest. Embedded certificates are wrapped in thecerts [0] EXPLICIT SEQUENCE OF Certificatethe field's syntax requires rather than concatenated, so a response withincludedCertificatesis parseable. An end-to-end differential test confirms OpenSSL accepts a micro509-produced response. (https://github.com/kjanat/micro509/pull/76) - The certificate builder enforces the RFC 8410 §5 keyUsage rules for the four 1.3.101 curves. A keyUsage extension on a certificate whose subject key names id-X25519 or id-X448 must set
keyAgreement(montgomery_key_usage_requires_key_agreement); one whose subject key names id-Ed25519 or id-Ed448 must setnonRepudiationordigitalSignature, widened in a certification authority certificate to also acceptkeyCertSignorcRLSign(edwards_key_usage_requires_signing_bit). The X25519 and X448 clause admits "one of the following MAY also be present: encipherOnly; or decipherOnly", so a keyUsage setting both is rejected (montgomery_key_usage_forbids_both_cipher_bits); the two Ed clauses say "one or both" and "one or more", and keep combining. All three codes joinExtensionEncoderErrorCode. The rules bind only a keyUsage that reaches the wire, and read the effective value across the typed field and acustomExtensionsentry carrying the keyUsage OID.OIDSgainsx25519,x448, anded448alongside the existinged25519. - PKCS#8 import requires the
privateKeyfield of an id-X25519, id-X448, id-Ed25519, or id-Ed448 key to hold exactly one DERCurvePrivateKeyOCTET STRING, per RFC 8410 §7. The field's content was passed to WebCrypto unexamined, so a BER long-form length (04 81 20 …) around an otherwise valid Ed25519 key imported. publicKeyAlgorithmNamereportsX25519,X448, andEd448alongside the existingEd25519, andsignatureAlgorithmNamereportsEd448, the human-readable names RFC 8410 §8 establishes. Every one of those OIDs was reported asUnknown (1.3.101.…), including the subject key of the X25519 certificate the RFC prints in §10.2. The names reach certificate, CSR, CRL, OCSP, and PKCS#7 parse output.- CSR parsing rejects a
CertificationRequestInfothat omitsattributes [0], per RFC 2986 §4.1, which lists it as a component withoutOPTIONAL. A three-field request parsed and came back with an emptyrequestedExtensions, so a truncated structure was indistinguishable from one requesting no extensions. RFC 7468 §7 requires the octets under theCERTIFICATE REQUESTlabel to be aCertificationRequestas described in RFC 2986. - PKCS#7/CMS PEM parsing accepts the RFC 7468 §9
CMSlabel alongsidePKCS7.parsePkcs7SignedDataPem,parsePkcs7CertBagPem, andverifyPkcs7SignedDataread onlyPKCS7blocks, so the RFC 5652 ContentInfo that §9 armors was unreadable, including the RFC's own Figure 11. createPkcs7SignedDataarmors a version 3 SignedData under theCMSlabel. Version 3 (anencapContentInfoeContentTypeother thanid-data, RFC 5652 §5.1) is outside RFC 2315, whose SignedData version "shall be 1" (§9.1), and RFC 7468 §8 requires the octets underPKCS7to be an RFC 2315 ContentInfo. A version 1 SignedData and the degenerate certificate bag keep thePKCS7label.- The certificate builder rejects a keyUsage that gives one 1.3.101 subject key both applications RFC 8410 §12 separates: "the same public key cannot be used for both ECDH and EdDSA". A certificate whose subject key names id-X25519 or id-X448 must not set
digitalSignature,nonRepudiation,keyCertSign, orcRLSign(montgomery_key_usage_forbids_signature_bit); one whose subject key names id-Ed25519 or id-Ed448 must not setkeyAgreement,encipherOnly, ordecipherOnly(edwards_key_usage_forbids_agreement_bit). RFC 5280 §4.2.1.3 defines the first four bits over a key used to verify signatures andkeyAgreementover a key used for key agreement, and leavesencipherOnlyanddecipherOnlyundefined without it. Both codes joinExtensionEncoderErrorCode. - The certificate builder rejects a
serialNumberRFC 5280 §4.1.2.2 forbids a CA to issue: a zero value ("the serial number MUST be a positive integer",serial_number_not_positive) and one whose DER INTEGER runs past 20 octets ("Conforming CAs MUST NOT use serialNumber values longer than 20 octets",serial_number_too_long, counting the leading zero octet a high bit forces). The bytes were encoded unexamined, sonew Uint8Array(21)or an empty array produced a certificate no conforming CA may issue. Both codes joinCreateCertificateErrorCode. - The CRL builder rejects an empty issuer distinguished name (
issuer_distinguished_name_empty, joiningCrlEncoderErrorCode). RFC 5280 §5.1.2.3 requires the issuer field to contain a non-empty X.500 distinguished name, and RFC 7468 §6 requires the octets under theX509 CRLlabel to be aCertificateListas described in RFC 5280 §5.createCertificateRevocationListencoded an emptySEQUENCEforissuer: {}, naming an entity no certificate can identify. The certificate builder already enforced the same rule from RFC 5280 §4.1.2.4. - Parsing checks the ASN.1 tag of an
AlgorithmIdentifier, of aNameand itsRelativeDistinguishedNameandAttributeTypeAndValuecomponents, and of a PKCS#10 attribute and itstypeandvaluesfields. RFC 7468 §7 requires the octets under theCERTIFICATE REQUESTlabel to be aCertificationRequestas described in RFC 2986, whose §4.1 and §4.2 give each of those fields a type. A CSR could carry its subject as aSET, its signature algorithm as aSET, or an attribute whosetypewas an OCTET STRING holding the extensionRequest OID's content octets, and parse; the last one had its extensions decoded as if the type had been an OBJECT IDENTIFIER. An attributevaluesfield is now also required to be a non-emptySET, per theSET SIZE(1..MAX)in RFC 2986 §4.1. TheAlgorithmIdentifierandNamechecks apply to certificate, CRL, and OCSP parsing as well. - PKCS#7/CMS parsing rejects a SignedData whose
EXPLICIT [0]content tag holds more than one value, and one whose signedcontentInfois not the two-fieldSEQUENCEof RFC 2315 §7 with an OBJECT IDENTIFIERcontentType. RFC 7468 §8 requires the octets underPKCS7to be an RFC 2315 ContentInfo, whosecontentis[0] EXPLICIT ANY DEFINED BY contentType OPTIONAL. A value appended inside theeContenttag was ignored andverifyPkcs7SignedDatastill returnedok, because RFC 2315 §9.3 digests only the contents octets of the first value, so two encodings verified under one signature; acontentTypecarrying another tag was decoded as if it were an OBJECT IDENTIFIER, yielding a fabricated OID. createPkcs7SignedDataandcreateOcspResponsereject a signer certificate whose subject public key cannot verify the algorithm the signer private key produces. RFC 8410 §12: "the same public key cannot be used for both ECDH and EdDSA", and both builders accepted an id-X25519 certificate beside an Ed25519 key, emitting a SignerInfo or a BasicOCSPResponse that named id-Ed25519 over a certificate that can never verify it.CreatePkcs7SignedDataErrorCodegainssigner_certificate_key_mismatch;createOcspResponsethrows aResultErrorcarrying the same code, from the newOcspEncoderErrorCode.- Base64 decoding rejects a final quantum that is not the RFC 4648 §4 encoding of its own octets. RFC 7468 §2 takes the encapsulated data as base64 "according to Section 4 of [RFC4648]", which completes a short final quantum with pad characters and "bits with value zero", and §14 names data encoding ambiguity as an opportunity for side channels.
atobignores the pad bits and the padding alike, so four texts decoded to a one-pad structure and sixteen to a two-pad one: Figure 6 of RFC 7468 parsed to the same certificate from a body endingIpo=,Ipp=,Ipq=, orIpr=, and an unpaddedAQdecoded asAQ==does.pemDecode,splitPemBlocks, every PEM parser above them,importSpkiBase64,importPkcs8Base64, and legacy encrypted PEM now reject both.
Security
verifyCertificateChainreportedok: truefor a chain containing a certificate whoseid-ecPublicKeypublic key carries no namedCurve OID, which RFC 5480 §2.1.1 requires clients to reject. The caller received a "verified" certificate binding a keycertificatePublicKeycannot import. Chain validation now fails such a path withec_domain_parameters_missing(joiningVERIFY_ERROR_CODES); absent parameters, animplicitCurveNULL, and aspecifiedCurveSEQUENCE all fail it.- Parsing rejects a zero-length
dNSName,rfc822Name, oruniformResourceIdentifierGeneralName, which RFC 5280 §4.2.1.6 forbids. An external certificate could previously carry an empty subjectAltName value and parse, leaving chain verification to accept a certificate with no usable identity when no identity match was requested. Certificate and CRL parsing share the decoder, so this covers subjectAltName, issuerAltName, authorityInfoAccess locations, CRL distribution points,cRLIssuer, the issuing distribution point, andcertificateIssuer. Name constraints keep their own decoder, where an empty base is meaningful. (https://github.com/kjanat/micro509/pull/88) - CRL parsing rejects a
CertificateListwhosesignatureAlgorithmdiffers from thesignaturefield of the signedtbsCertList, per RFC 5280 §5.1.1.2. The outer field is outside the signature, and it was the one reported as the CRL's signature algorithm, so a CRL could name one algorithm to the caller and another to the signer. Certificate parsing already enforced the same rule from RFC 5280 §4.1.1.2.
0.13.0 - 2026-07-23
A public micro509/der entrypoint, and RFC-conformance fixes across path validation: name constraints, issuer chaining, CRL issuer paths, distinguished-name comparison, and policy node sets.
Added
micro509/derexposes the DER reader, writer, and value decoders that back every built-in parser.defineExtensionDecoderhands a consumervalueDerand expects raw DER back fromencode, and nothing in the public API could read or write those bytes, so a custom extension needed a second ASN.1 library. Readers and decoders take untrusted bytes and come in pairs:decodeDerIntegerreturns aResult,decodeDerIntegerOrThrowthrows. Writers take typed input and throw, matchingencodeNameandpemEncode. (https://github.com/kjanat/micro509/issues/63)bmpStringanduniversalStringencode the two string types that already decoded, so a PKCS#12friendlyNamecan now be written as well as read. Both reject lone surrogates, andbmpStringrejects code points above U+FFFF.decodeBitStringreturns a BIT STRING's payload with its unused-bit count.extractBitStringValuerejects a non-zero count, which excluded KeyUsage, the most common BIT STRING in a certificate. Unused trailing bits are returned as encoded, matching how the extension decoders already treat non-conformant certificates.
Changed
decodeIntegerNumberaccepts a non-negative, minimally encoded INTEGER up toNumber.MAX_SAFE_INTEGERand rejects above it. Negative and non-minimal encodings are still rejected, as before. It stopped at 6 bytes whileintegerFromNumberencoded any non-negative safe integer, so values from 2^47 up encoded and would not decode. The old limit cited 48 bits as the safe-integer boundary; that boundary is 53. A 7- or 8-byte INTEGER inside a certificate that previously threw now parses.
Security
- Name constraints reject a URI SAN whose authority has no FQDN host (an IP literal, a single-label host such as
localhost, or no authority at all) when a uniformResourceIdentifier constraint applies, per RFC 5280 §4.2.1.10. Such a URI previously slipped past the constraint. Email constraint matching now compares the local part case-sensitively and only the host case-insensitively (RFC 5280 §7.5, as replaced by RFC 9549 §7.5.1), soadmin@example.comno longer matchesADMIN@example.comand widens the permitted subtrees. (https://github.com/kjanat/micro509/pull/71) validateCandidatePathcompares each certificate's issuer DN against the candidate issuer's subject DN, per RFC 5280 §6.1.3(a)(4). It verified only the signature, so a leaf whose issuer DN was unrelated to the signing CA validated as ok on the pre-built-path API.buildChainInternalalready compared them, soverifyCertificateChainwas unaffected. (https://github.com/kjanat/micro509/pull/72)- CRL evidence validates the CRL issuer's own certification path to the trust anchor before trusting its verdict (RFC 5280 §6.3.3(f)). A forged indirect-CRL signer whose subject DN collided with a chain certificate was accepted on a name match, its signature never checked against a trusted key, so a forged empty CRL reported a revoked certificate as
good. Each candidate CRL issuer runs the full pipeline as one step (signature, then §6.3.3(f) path, then signer revocation), so an unusable candidate no longer shadows a later authorized one, and the signer's path is validated against a pool that includes the validated chain intermediates, so a delegated signer issued by a chain CA authorizes. Without these, a genuine revoked CRL becamecrl_signer_not_authorizedand soft-fail allowed the revoked certificate. (https://github.com/kjanat/micro509/pull/73) - Distinguished name comparison implements the RFC 4518 string-preparation profile that RFC 5280 §7.1 requires, against the frozen Unicode 3.2 repertoire RFC 4518 §2.1 fixes: the Map, Normalize, Prohibit, and Insignificant Space steps with the complete RFC 3454 Appendix B.2 case fold and Table A.1 unassigned set. The old NFKC-plus-lowercase shortcut left ignorable code points in place (so an excluded subtree failed to exclude a name carrying a SOFT HYPHEN), folded with
toLowerCasealone (soStraßedid not equalSTRASSE), and delegated the repertoire to the running Unicode version. That last part let a code point unassigned in 3.2 slip through: U+1D2C normalized toaand matchedCN=a, U+2F868 used the Unicode 4.0 NFKC correction, and U+10A0 took a post-3.2 fold. The A.1 unassigned set, the B.2 fold, and the five CJK NFKC corrections are generated from the vendored RFC 3454 and guarded by a test that re-derives them from the RFC text. BMPString and UniversalString values are prepared alongside UTF8String and PrintableString.domainComponentcompares ascaseIgnoreIA5Match(RFC 4519), requiring the IA5String tag and ASCII, so a UTF8String or non-ASCII value no longer matches an IA5String one;DC=Examplechains toDC=example. Bare-anchor selection confirms the anchor's subject equals the certificate's issuer rather than trusting the canonical-key bucket. RFC 4518 and RFC 3454 are vendored underdocs/rfc/. (https://github.com/kjanat/micro509/pull/74) - Certificate policy validation computes the RFC 9618 §5.5(g)
valid_policy_node_setcorrectly: the nodes at any depth whose valid_policy is not anyPolicy and whose single parent is an anyPolicy node, plus a depth-n anyPolicy node. It previously took depth-n nodes tracing back to the depth-0 anyPolicy root, which diverges under policy mapping. A CA that mapped1.2.3.4to1.2.3.5reportedauthorityConstrainedPoliciesof1.2.3.5where the spec requires1.2.3.4(NIST PKITS 4.10.1), so a chain validated against the mapped policy instead of the authority's.userConstrainedPoliciesnow follows §5.5(g)(5)-(6) from the corrected set. Policy validation also processes the terminal certificate when a bare trust anchor is used: a path built to an out-of-band anchor ends at a real CA, and skipping it let a leaf policy satisfyinitialPolicySeteven when that CA omitted or contradicted the policy.authorityConstrainedPoliciesnow aggregates each policy's qualifiers from its node, ancestors, and descendants per §5.5(g)(4)(ii) rather than reporting one arbitrary node's set. (https://github.com/kjanat/micro509/pull/75)
0.12.0 - 2026-07-21
Text rendering for subject alternative names and distinguished names, and runnable docs examples that survive client-side navigation.
Added
subjectAltNameToString(name, options?)renders oneSubjectAltNameas text. Printing a SAN is the most ordinary thing to do with one after parsing, and it took a hand-written narrowing in every consumer:dns/ip/email/uri/srvcarry astringvalue,directoryNamecarries onlyderHexand has novalueat all, andunknowncarries aUint8Arraythat stringifies itself as192,0,2,1inside ajoin(). AdirectoryNamenow renders as an RFC 4514 distinguished name (CN=Example CA,O=Acme\, Inc.,C=US), falling back to its hex when the DER does not decode, and anunknownrenders as hex. Pass{ prefix: true }for theopenssl x509 -textlabels (DNS:,IP Address:,DirName:), or callsubjectAltNameLabel(name)for the label alone. CompaniondistinguishedNameToString(name)andrelativeDistinguishedNameToString(rdn)render a parsed subject or issuer, which until now had no public renderer either. Exported from the root andmicro509/x509, with theSubjectAltNameTextOptionstype. (https://github.com/kjanat/micro509/issues/50)The docs site serves a permalink for the newest release.
/v0.11.0/…used to 404 while 0.11.0 sat at the root, then silently come into existence as an archive one release later, so a link to the root changed meaning on every release. The build now emits a_redirectsfile sending/v<latest>/*to the root with a 302; once the next release takes the root, the rule disappears and the same URL serves the archived copy. Temporary on purpose: browsers cache a 301 past the release that makes it wrong.
Changed
- The X.509 reference now identifies every asynchronous operation in one place and explains the split: PEM/DER parsing and transformation are synchronous, while hashing, signing, and key operations backed by WebCrypto return promises. The API summaries for
parseCertificatePemandcertificateFingerprintmake their return timing explicit, avoiding a no-opawaiton the parser or reading properties from an unawaited fingerprint promise. (https://github.com/kjanat/micro509/issues/51) - The docs site no longer co-hosts the library: each version's runnable examples import it from a CDN, bound to the version that page documents. A release imports what it published, from jsDelivr, whose
+esmbuilds arrive bundled — one request where esm.sh's module graph took thirty-nine, and a third of the time to load./next/imports a pkg.pr.new build of the deployed commit, which only esm.sh can serve, bundled with?standalone. The co-hosted copy put the library's own file layout in the site's URL space, wherex509/fingerprint.jsmatched an EasyPrivacy rule blocking that path on every domain (/fingerprint.js^$domain=~github.com). Content blockers refused it, and because it is a static import of the root entry, every example on the site died for readers running one. - CI gates the docs site on its examples actually working:
run site:import-mapschecks every version's map names its own library and that every URL in it resolves, andrun site:live-examplesclicks Run in a real browser with a content blocker simulated — a headless browser has none, and would have passed while the bug above was live. - Materializing an archived version's pages now repairs a runnable example the tag shipped with a syntax error, so its Run button executes valid code. Each
<LiveCode>block is parsed, and one missing the brace that closes a block gets it restored at the position the TypeScript parser expects. Nineguide/getting-startedexamples across v0.3.0 through v0.9.0 were affected. The archived page then differs from the code that release published. (https://github.com/kjanat/micro509/issues/53)
Fixed
- Runnable examples now execute the version of the page they run on after client-side navigation. Each page shipped its own
<script type="importmap">, which the browser reads once per document, so navigating from the landing page to an archived version and pressing Run resolvedmicro509against the entry page's map and imported the wrong release; only a hard refresh picked up the right one. Every page now ships one identical map: top-level imports for the root version and a scope per version prefix. Scopes match the URL of the importing module, and the injected example module inherits the document URL, so resolution follows the page at run time with no map swapping.run site:import-mapsnow verifies the map is identical on every page and each scope binds its own version, andrun site:live-examplesreplays the failing flow: enter at the root, navigate client-side to an archive, run its example, and require every import to carry that archive's version.
0.11.0 - 2026-07-13
Certificate fingerprinting and private-key ownership checks for certificate inspection, intake, and issuance workflows.
Added
certificateFingerprint(certificate, algorithm?)computes the standard certificate fingerprint — a hash over the DER encoding, the identifieropenssl x509 -fingerprintand TLS UIs display. It accepts the samestring | Uint8Array | ParsedCertificatesource union as the verification APIs and returns{ bytes, hex, colonHex }: the raw digest, lowercase hex with no separators, and uppercase colon-separated hex (openssl style). It defaults to SHA-256; SHA-1/384/512 are also supported for legacy interop (older certificate pinning, PGP-adjacent tooling). Exported from the root andmicro509/x509ascertificateFingerprint, withCertificateFingerprint,CertificateFingerprintAlgorithm, andCertificateFingerprintSourcetypes. Interop withopenssl x509 -fingerprintverified across all four algorithms. (https://github.com/kjanat/micro509/issues/45)certificateMatchesPrivateKey(certificate, privateKey)checks whether an uploaded private key belongs to a certificate — the first thing any key-intake or issuance endpoint must do. It derives the public half of the private key, exports it as SubjectPublicKeyInfo DER, and byte-compares it against the certificate's own SPKI (the canonical, algorithm-agnostic ownership test), returning a plainboolean.certificateaccepts a PEM string, DER bytes, or an already-parsedParsedCertificate; a private key of a different type simply produces different SPKI and returnsfalse.matchCertificatePrivateKeyis theResult-returning companion:ok: trueon a match, or a typed failure carryingkey_mismatch,key_type_mismatch,malformed_certificate, orunsupported_private_key— so trust boundaries get the reason (and no thrown errors on untrusted input). The error-code union is exported asMatchCertificatePrivateKeyErrorCode. (https://github.com/kjanat/micro509/issues/46)
0.10.0 - 2026-07-13
Detached PKCS#7 / CMS signatures — the form git x509 commit signing and S/MIME rely on — and algorithm inference across every private-key import family.
Added
createPkcs7SignedDataacceptsdetached: trueto omiteContentfromencapContentInfo(RFC 5652 §5.2 detached form), andverifyPkcs7SignedDataaccepts an options bag withcontentto supply the externally-held bytes when verifying a detached signature — the shape git x509 commit signing (gpg.format=x509) and S/MIME detached signatures use. The error-code union is now exported asVerifyPkcs7SignedDataErrorCode, the options asVerifyPkcs7SignedDataOptions. Interop withopenssl cmsverified in both directions. (https://github.com/kjanat/micro509/issues/40)- The private-key import families infer the algorithm from the container when the
algorithmparameter is omitted, mirroring the existing SPKI behavior; passing it still asserts and fails typed on mismatch:importPkcs8Der/Pem/Base64(+OrThrow)andimportEncryptedPkcs8Der/Pem(+OrThrow)read the PKCS#8privateKeyAlgorithm(RSA defaults topkcs1-v1_5/SHA-256, as with SPKI inference).importSec1Der/Pem(+OrThrow)andimportEncryptedSec1Pem(+OrThrow)read the RFC 5915parameters [0]named curve; a SEC 1 key without one still requires the explicit curve.importPublicJwk/importPrivateJwk(+OrThrow) readkty,crv, andalg(RS*/PS*/RSA-OAEP-256/384/512select the RSA scheme and hash). (https://github.com/kjanat/micro509/issues/41)
Changed
- BREAKING —
verifyPkcs7SignedDatareports a SignedData that carries noeContentas'detached_content_required'; the'content_missing'code is gone. Such a message is not malformed, it is a detached signature awaiting its external content, and it is now only a failure when nocontentoption is supplied. Rename any match on'content_missing'; matches against the exportedVerifyPkcs7SignedDataErrorCodeunion fail to typecheck until you do. exportSec1Der/exportSec1Pem/exportEncryptedSec1Pemalways embed the RFC 5915parameters [0]named curve (WebCrypto's inner ECPrivateKey omits it; OpenSSL writes it), so exported SEC 1 keys are self-describing and re-import without an explicit curve.- Preserve public API behavior while decomposing DER, parsing, verification, revocation, PKCS#7, and API-documentation flows into focused helpers that satisfy the stricter cognitive-complexity limit. (https://github.com/kjanat/micro509/pull/39)
Fixed
createPkcs7SignedDatareturns the typed'invalid_signer_certificate'/'invalid_certificate'(new code) failures for malformed signer and additional-certificate inputs instead of rejecting the promise. EachadditionalCertificatesvalue is structurally validated as a real X.509 certificate, so malformed DER also returns'invalid_certificate'.
0.9.0 - 2026-07-06
RSA-OAEP encryption support across the whole key lifecycle: generate, import, derive, encrypt, decrypt.
Added
RsaSchemeaccepts'oaep':generateKeyPair, the SPKI / PKCS#8 / JWK import functions, andderivePublicKeyproduceRSA-OAEPkeys withencrypt/decryptusages when the scheme is'oaep'(signature schemes keepsign/verify). (https://github.com/kjanat/micro509/pull/36)encryptRsaOaep/decryptRsaOaep(and their…OrThrowsiblings) encrypt and decrypt small messages with an RSA-OAEP key pair, with an optional OAEPlabelbound to the ciphertext. Failures are typed:'invalid_key' | 'message_too_long'on encrypt,'invalid_key' | 'decryption_failed'on decrypt — ciphertext-level decryption failures are deliberately opaque (no padding-oracle detail).RsaSignatureSchemenarrowsRsaSchemeto the signature schemes ('pkcs1-v1_5' | 'pss'), so certificate signature verification can never silently accept an OAEP key.
0.8.0 - 2026-07-05
Key/certificate import ergonomics and validation hardening — every API finding from the OpenSSL differential fuzzer, shipped in one release.
Added
- The
keysandx509domain barrels (and the root barrel) re-export the 20OrThrowsiblings that were implemented and documented but unreachable from the published package — 16 key-import variants (importSpkiDerOrThrow,importPkcs8PemOrThrow, …) and 4 certificate/CSR parsers (parseCertificateDerOrThrow, …). A conventions test now fails whenever a barrel exposes a function while omitting itsOrThrowsibling. (https://github.com/kjanat/micro509/issues/26) derivePublicKey(privateKey)derives the matching publicCryptoKeyfrom an imported (or generated) private key, so a private key loaded viaimportPkcs8*/importPkcs1*/importSec1*/importPrivateJwkcan go straight toexportSpkiDer/exportSpkiPemwithout hand-rolling JWK surgery. Supports RSA, ECDSA, and Ed25519. (https://github.com/kjanat/micro509/issues/19)importSpkiDer,importSpkiPem,importSpkiBase64(and their…OrThrowvariants) now accept an optional algorithm argument. A SubjectPublicKeyInfo already encodes its algorithm OID — and the EC curve OID — so when no hint is given the algorithm is inferred straight from the DER, letting callers import keys whose type isn't known ahead of time. Passing an explicit algorithm is unchanged and still asserts the key matches it. (https://github.com/kjanat/micro509/issues/20)getSubjectPublicKey(parsed)/getSubjectPublicKeyOrThrow(parsed)import the subject public key of a parsed certificate or CSR as a WebCryptoCryptoKey, inferring the algorithm (and EC curve) from the embedded SubjectPublicKeyInfo — callers no longer hand-roll thepublicKeyAlgorithmOid→ import-algorithm mapping. (https://github.com/kjanat/micro509/issues/21)
Fixed
importPrivateJwk/importPrivateJwkOrThrownow validate the JWK against the requested algorithm before handing it to WebCrypto, matching whatimportPublicJwkalready did:kty/crvmust match the requestedkind/curve, the private material the kind implies must be present (deverywhere, plusn/e/p/q/dp/dq/qifor RSA), and symmetric (k) or multi-prime (oth) material is rejected. Wrong-algorithm and public-only JWKs previously surfaced as opaque WebCrypto errors instead of the library's'malformed'failures. (https://github.com/kjanat/micro509/issues/23)importSec1Der/importSec1Pem/importEncryptedSec1Pem(and their…OrThrowvariants) trusted the caller'scurvewithout reading the SEC 1 ECPrivateKey itself. The RFC 5915parameters [0]field (OpenSSL always writes it) is now parsed and cross-checked: a curve mismatch fails withSEC 1 private key curve does not match requested import algorithm, and bytes that are not an ECPrivateKey fail withMalformed SEC 1 private key— instead of both surfacing as WebCrypto's opaque "Malformed PKCS#8" error. Keys without the optional parameters field import as before. (https://github.com/kjanat/micro509/issues/22)
0.7.2 - 2026-07-04
Error-classification fix for encrypted-key imports with a wrong password.
Fixed
importEncryptedPkcs8Der/importEncryptedPkcs8Pemreported'malformed'instead of'invalid_password'roughly once per 256 wrong-password attempts: AES-CBC padding is unauthenticated, so a wrong key occasionally "decrypts" to random bytes that pass the padding check, and the resulting PKCS#8 parse failure was misclassified. Decrypted plaintext that is not a PrivateKeyInfo now reports'invalid_password'.parsePfxhad the same wrong-password tail (reporting'malformed'), plus the reverse: structurally malformed EncryptedData could report'invalid_password'. Classification now keys on the decryption failure itself instead of error-message prefixes.
0.7.1 - 2026-07-04
ECDSA signature-encoding bug fix — re-issue any ECDSA-signed artifacts that fail external verification.
Fixed
- ECDSA signing embedded an invalid signature roughly once per 256 signatures: WebCrypto's raw
r || soutput was detected by sniffing the first byte for the DER SEQUENCE tag, so a raw signature whoserbegan with0x30was emitted unconverted. OpenSSL rejects such certificates and CRLs with a signature failure (micro509's own verifier masked the bug by making the mirror-image guess). Detection is now by exact raw length. Affects all ECDSA-signed artifacts from previous releases — re-issue anything that fails external verification.
0.7.0 - 2026-07-04
Pre-1.0 API freeze cleanup: one coherent breaking pass over vocabulary, error-handling doctrine, type shapes, and export surface, so 1.0 can freeze a surface with no known regrets. Every rename is in the migration table below.
Added
- Runtime code arrays
REVOCATION_INDETERMINATE_REASON_CODESandREVOCATION_INDETERMINATE_REASONS(theVERIFY_ERROR_CODESpattern); their unions now derive from the arrays. - The root entry point exports every type reachable from public signatures that were previously subpath-only (MacData, policy-validation outcome, identity failure details, revocation error codes/failure payloads, and the new
Parse*Result/Pem*Resulttypes). Pkcs7CertificateSourceandPfxCertificateSourceaccept an already-parsedParsedCertificate(parity with the revocation source unions).- Documented error-code stability policy: unions may gain members in minor releases — treat them as non-exhaustive.
- CI now smoke-tests the two previously untested runtime claims: Cloudflare Workers (real workerd via wrangler's test harness) and browsers (headless Chromium via Playwright, loading the built
dist/output). All five supported runtimes are now exercised in CI.
Changed
Every entry in this section is BREAKING; the migration table at the end maps each old name to its replacement.
Revocation defaults to hard-fail once enabled. Revocation checking remains opt-in (no evidence supplied ⇒ no check), but once
revocationis passed,policy.modenow defaults to'hard-fail': indeterminate status denies. Opt back into the old behavior explicitly withpolicy: { mode: 'soft-fail' }. Mental model: no revocation input → no check; revocation input → revocation matters.Vocabulary unified. The CRL/OCSP discriminant is
kindeverywhere (was akind/source/typemix); the verifier-level can't-tell status is'indeterminate'everywhere (was'unknown'in standalone checks vs'indeterminate'in chain checks). Protocol-level reason codes that quote RFC 6960'sunknowncertificate status keep the word (ocsp_status_unknown,certificate_status_unknown,responder_revocation_unknown), as doesOcspCertStatus.Result doctrine completed. The last public parsers consuming untrusted input that still threw now return a typed
Result(code: 'malformed'): CRL, OCSP request/response, PKCS#12 MacData, and the PEM primitives. Each has an*OrThrowtwin (same convention as the x509 parsers since 0.3.0).Illegal states made unrepresentable.
ParsedPkcs7SignerInfodiscriminates onhasSignedAttrs(whentrue,signedAttrsDeris guaranteed);CertificateRevocationStatusis a discriminated union (good/revoked always carrysource, revoked always carriesrevocationInfo, indeterminate always carriesindeterminateReasons);BasicConstraintsrejects{ ca: false, pathLength }at compile time;ParsedPkcs12MacData.valid(optional boolean tri-state) becameverification: 'valid' | 'invalid' | 'unchecked';ParsedName.valuesis a readonly map;ValidateCandidatePathResultlost its duplicate top-levelpolicyValidation.pkcs7 creators match the rest of the library.
createPkcs7CertBagandcreatePkcs7SignedDatareturn aResultwhose value is{ der, pem, base64 }material like every other creator (check.ok, then read.value); theDer/Pemvariants are gone.Signature-only verifiers say so.
verifyOcspResponseSignature/verifyCertificateRevocationListSignaturecheck the signature only — the bareverify*names read as full validation, which isvalidateOcspResponse/validateCertificateRevocationList. (verifyCertificateSigningRequestandverifyPkcs7SignedDatakeep their names: each is the complete operation for its object.)Export surface curated.
micro509/revocationandmicro509/verifylist every export explicitly (noexport type *); dead aliases removed (MatchableServiceIdentityInput,VerifyServiceIdentityInput,MatchServiceIdentityEvaluation— useServiceIdentityInput);rethrowIfInvariant(internal control flow) leftmicro509/result;Pbes2EncryptionOptions/Pbes2EncryptionScheme/Pbes2Prfleft the public barrels (EncryptedPkcs8OptionsandPfxEncryptionOptionsare the canonical names); the duplicateCertificateSourcealias on the revocation subpath (which collided with the root-exported verify type under a different shape) is gone — chain inputs useRevocationCertificateSource.IssuingDistributionPoint*types moved from the x509 surface tomicro509/revocation(they are CRL types; the root still exports them).
Migration table
| 0.6.0 | 0.7.0 |
|---|---|
RevocationStatus 'unknown' | 'indeterminate' |
RevocationCheckUnknownValue | RevocationCheckIndeterminateValue |
revocation_status_unknown | revocation_status_indeterminate |
RevocationSource.type | RevocationSource.kind |
RevocationCheck{Good,Revoked}Value.source | .kind |
RevocationIndeterminateEvidence.source | .kind |
revocationInfo.date | revocationInfo.revocationDate |
policy.mode default 'soft-fail' | 'hard-fail' |
verifyOcspResponse | verifyOcspResponseSignature |
verifyCertificateRevocationList | verifyCertificateRevocationListSignature |
parseCertificateRevocationList{Der,Pem} (throwing) | Result-returning; *OrThrow for the old behavior |
parseOcsp{Request,Response}{Der,Pem} (throwing) | Result-returning; *OrThrow for the old behavior |
parsePkcs12MacData (throwing) | Result-returning; parsePkcs12MacDataOrThrow |
pemDecode / splitPemBlocks / categorizePemBlocks | Result-returning; *OrThrow for the old behavior |
ParsedPkcs12MacData.valid?: boolean | verification: 'valid' | 'invalid' | 'unchecked' |
createPkcs7CertBag{Der,Pem} | createPkcs7CertBag (Result of { der, pem, base64 }) |
createPkcs7SignedData{Der,Pem} | createPkcs7SignedData (Result of { der, pem, base64 }) |
Pkcs7CertBag | Pkcs7CertBagMaterial |
Import{Rsa,Ec,Ed25519}PublicKeyInput | Import{Rsa,Ec,Ed25519}KeyInput |
PBES2 option encryption: 'aes256-cbc' | cipher: 'AES-256-CBC' |
PBES2 option prf: 'hmac-sha256' | prf: 'HMAC-SHA-256' |
trustedResponderCertificates (validateOcspResponse) | trustedOcspResponders |
delta_crl_unsupported / indirect_crl_unsupported | unsupported_delta_crl / unsupported_indirect_crl |
service_identity_type_unsupported | unsupported_service_identity_type |
service_identity_service_mismatch | service_identity_mismatch |
VerifyServiceIdentityInput / MatchableServiceIdentityInput | ServiceIdentityInput |
VerifyOcspResponse{Result,Failure} | VerifyOcspResponseSignature{Result,Failure} |
VerifyCertificateRevocationList{Result,Failure} | VerifyCertificateRevocationListSignature{Result,Failure} |
Fixed
- Manual
npm publishoutside the release workflow now fails via aprepublishOnlyguard: only the workflow rewrites the dev-onlybun → ./src/*.tsexport conditions, so a raw publish would ship an exports map pointing at files missing from the dist-only tarball.
0.6.0 - 2026-07-04
Full standards surface claimed complete: all four RFC status rows — 5280, 6960, 6125, 9618 — now read complete, backed by the full NIST PKITS suite and RFC-exact name-constraint handling.
The PKITS sweep behind that claim runs the full NIST suite — all 224 test procedures across sections 4.1–4.16, expanded to 249 runs including every documented subtest variation — with every manifest expectation verified against the official PKITS document, now vendored as docs/rfc/pkits.txt. 4.1.4/4.1.5 (DSA chains) are expected-fail per the WebCrypto algorithm boundary.
Changed
- Name constraints now fail closed for unsupported GeneralName forms per RFC 5280 §4.2.1.10. A critical
nameConstraintsextension imposingotherName,x400Address,ediPartyName, orregisteredIDconstraints rejects a subsequent certificate only when a SAN of that form actually appears (unsupported_name_constraints); previously any such chain was rejected outright, even when the constrained form never occurred. SRV-ID and unknown-tag SANs now participate in that fail-closed check — they were previously skipped. Unsupported forms in non-critical extensions are ignored, and supported forms in the same extension are still enforced.
0.5.0 - 2026-07-03
OCSP responder authorization, URI/SRV service identities, and a best-available revocation preference that actually compares evidence freshness.
Added
URI-ID and SRV-ID service identities are now accepted by the verification helpers:
VerifyServiceIdentityInputwidened to the fullServiceIdentityInputunion, soverifyCertificateChain({ serviceIdentity })andvalidateForTlsServermatch{ type: 'uri' | 'srv' }alongside DNS/IP. An SRV service-label mismatch surfaces assubject_alt_name_mismatchwith the matcher's details.OCSP responder authorization completed (RFC 6960 §4.2.2.2):
trustedResponderCertificatesonvalidateOcspResponse()andtrustedOcspRespondersoncheckChainRevocation()/verifyCertificateChain({ revocation })— criterion-1 local responder configuration; a matching signer skips delegated issuance/EKU checks and is consulted during responder discovery.- Delegated responder revocation policy (§4.2.2.2.1):
responderRevocationPolicy='honor-nocheck'(default) /'require-evidence'/'skip'withresponderRevocationCrlsas evidence; chain orchestration reuses its CRLs automatically. New failure codesresponder_revokedandresponder_revocation_unknown. hasOcspNoCheckExtension()— parsesid-pkix-ocsp-nocheck.- Delegated responder chains now validate at the caller-supplied
at(historical-time validation) instead of always at the current time.
Changed
policy.prefer: 'best-available'(the default) now genuinely picks the freshest evidence: when CRL and OCSP both yield a validatedgoodverdict, the source with the laterthisUpdateis reported (an applied delta CRL counts as its ownthisUpdate; ties favor OCSP). Previously'best-available'behaved identically to'ocsp'. Fail-closed combination is unchanged — a validatedrevokedverdict from either source still always wins.RevocationSourcegainedthisUpdate— the timestamp of the evidence backing the verdict (OCSP single-response entry or freshest contributing CRL), i.e. the value'best-available'compares.signerCertificatefor a multi-CRLgoodverdict is now the freshest contributing CRL's signer rather than the last one processed, so it always matches the reported freshness.
0.4.0 - 2026-07-03
OCSP joins chain-level revocation, and the npm package no longer ships broken Bun export conditions.
Added
- OCSP evidence is now consumed by chain-level revocation:
checkChainRevocation()(andverifyCertificateChain({ revocation })) validates caller-suppliedocspResponses— signature, responder binding and authorization, freshness — and combines them with CRL evidence. A validatedrevokedverdict from either source always denies, regardless ofpolicy.prefer(fail-closed). Delegated responder certificates can be supplied viaextraCertificates. VERIFY_ERROR_CODES: runtime array of everyVerifyErrorCode, exported from the root andmicro509/verify. The docs error-code table is now test-enforced against it.- CI now builds, validates the npm tarball against the published exports map, and smoke-tests the dist output under Node and Deno.
Changed
- BREAKING —
VerifyErrorCode: renamedinitial_name_constraints_not_implemented→unsupported_initial_name_constraints(it reports unsupported/malformed initial-name-constraint forms, not a missing feature). Removedpolicy_processing_not_implemented, which no code path emitted.
Fixed
- npm packaging: the published
exportsmap retained the dev-onlybun → ./src/*.tsconditions while the tarball ships onlydist/, breaking Bun consumers installing from npm (npm publishdoes not applypublishConfig.exports). The publish workflow now rewritesexportsfrompublishConfig.exportsbefore publishing, and CI fails if any published export target is missing from the tarball.
0.3.0 - 2026-07-02
Typed-error rework: trust-boundary functions (which consume untrusted external input) now return a Result as the strict, correct default, with an explicit unwrap() escape hatch. Typed-config constructors keep throwing (a bad config is a programmer error, not a runtime condition).
Added
unwrap(result)/unwrapOr(result, fallback)(root +micro509/result): the explicit escape hatch for callers who have already validated input or prefer exceptions.unwrapthrows a branded plain error carrying the structuredcode;ResultErroris exported as its type andisResultError(error)is the guard. There is no class toinstanceof— the library ships no classes.failureResult(code, message, details?)factory inmicro509/result: one source of truth for the{ ok, error, code, message }shape.rethrowIfInvariant(error)inmicro509/result: the guard the parse wrappers use to keep programmer errors out ofResultfailures (removed from the public barrel in 0.7.0).
Changed
- BREAKING —
parseCertificateDer,parseCertificatePem,parseCertificateSigningRequestDer,parseCertificateSigningRequestPemnow return aResult({ ok, value }/{ ok, error: { code: 'malformed' } }) instead of throwing. Wrap withunwrap(...)for the previous throw-on-error behavior. - BREAKING — All 16 key
import*functions now return aResultinstead of throwing. Non-encrypted failures use code'malformed'; encrypted imports distinguish a typed'invalid_password'from'malformed'.export*andgenerateKeyPairare unchanged (no untrusted input). - BREAKING —
createPfx,createPkcs7CertBagDer, andcreatePkcs7CertBagPemnow return aResult(code'invalid_certificate') instead of throwing on a malformed certificate source — matchingcreatePkcs7SignedData. Pure typed-config constructors (createCertificate,createSelfSignedCertificate,createCertificateRevocationList, …) still throw: a bad config is a programmer error, not a runtime result. - Canonical docs site is now
micro509.kjanat.dev(wasmicro509.kjanat.com, which stays live as a mirror).homepageand all documentation links point at the.devdomain. - GitHub repository renamed
kjanat/ts-x509→kjanat/micro509to match the published package name.repository.urlupdated; old URLs redirect.
Fixed
- The new parse
Resultwrappers rethrow aTypeError,RangeError,ReferenceError, orSyntaxErrorraised inside a parser instead of reporting it as a'malformed'failure, so a genuine crash surfaces as a crash rather than masquerading as bad input.
0.2.0 - 2026-06-29
Added
- PKCS#7 / CMS
SignedDatacreation (createPkcs7SignedDataDer,createPkcs7SignedDataPem): sign content with one or more signers via the RFC 5652 §5.4 signed-attributes flow (contentType+messageDigest), producing attached SignedData that round-trips throughverifyPkcs7SignedData. The content digest is selected per signer key: SHA-256 for ECDSA P-256 and RSA-SHA256, SHA-384 for P-384, and SHA-512 for P-521 and Ed25519 (the latter per RFC 8419). Returns a typed result (no_signers/invalid_signer_certificate/unsupported_signer_key) for caller-correctable input.
0.1.1 - 2026-06-29
Maintenance release — release-pipeline fixes only, no library changes.
Fixed
- Publish workflow is gated on the test suite, authenticates npm via OIDC trusted publishing, and emits correct JSR/npm release URLs.
0.1.0 - 2026-06-29
Initial prerelease. API may change before 1.0.
Added
- X.509 certificate and CSR creation, parsing, and self-signing.
- Certificate chain verification with typed results (21 error codes, failing certificate index, structured failure details) and RFC 6125 service-identity matching (DNS, IPv6, URI-ID, SRV-ID, explicit CN opt-in).
- Revocation: CRL create/parse/verify/status and OCSP request building plus response parsing and responder-authorization checks.
- PKCS#7 / CMS
SignedDataparsing and signer-signature verification. - PFX / PKCS#12 create and parse (PBES2, PKCS#12 KDF, HMAC-SHA-256 MAC).
- PEM handling and key import/export (PKCS#8, SPKI, JWK, PKCS#1, SEC1) with generation for RSA, ECDSA (
P-256/P-384/P-521), and Ed25519. - Zero runtime dependencies, WebCrypto-native, tree-shakeable subpath exports; runs on Node, Bun, Deno, browsers, and Cloudflare Workers.