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
Added
The DER and BER readers accept high-tag-number identifiers (X.690 §8.1.2.4) for tag numbers from 31 up, and
DerElement.tagNumbercarries the tag number within its class. An otherName value, a SafeBag value and a OneAsymmetricKey extension field may use them. A tag number below 31 in that form, one whose first subsequent octet is0x80, and one that ends before its final octet, however long, aremalformed.checkStrictDerreports UNIVERSAL 31 to 36 asunsupportedand the reserved numbers from 37 up asmalformed.IDNA2008 (RFC 5890-5893, RFC 8753) over frozen Unicode 12.0.0 tables derived from the IANA registry. The builder converts U-labels to A-labels in dNSName and rfc822Name SANs, SmtpUTF8Mailbox domains, the Name of a SRVName, and dNSName and rfc822Name constraints, and fails with
invalid_idnon a label that is not valid IDNA2008, anxn--label that is not an A-label, or an ASCII label beside an IDN label that is not NR-LDH. A dNSName or rfc822Name domain, SAN or constraint, that ends in the root dot fails withdomain_trailing_dot. RFC 5280 §4.2.1.6 requires the RFC 1034 §3.5 preferred name syntax, which has no trailing dot.RFC 9608
noRevAvail(id-ce 56). Parsing exposes it asParsedCertificate.noRevAvail, andextensions.noRevAvail: trueemits it. The builder refuses it beside cA TRUE,crlDistributionPoints, freshestCRL or anocspauthorityInfoAccess entry (no_rev_avail_conflict), and path validation rejects such a certificate with the newno_rev_avail_conflictverify code.checkChainRevocationandverifyCertificateChain({ revocation })report a certificate carryingnoRevAvailorid-pkix-ocsp-nocheckas the newstatus: 'skipped'with askipReason, without consulting evidence (RFC 9608 §4). The exemption is read from the certificate's signed DER.maxKdfIterationson the encrypted PKCS#8 imports (ImportEncryptedKeyOptions, fourth argument),parsePfxDer/parsePfxPemoptions, andparsePkcs12MacDatabounds the PBKDF2 and PKCS#12 KDF iteration counts a file may demand. Above the bound the import fails withkdf_iterations_exceededbefore any derivation runs. The default is 2,000,000 for PBKDF2, including the PBKDF2 behind PBMAC1, and 100,000 for the PKCS#12 KDF. RFC 7292 Appendix B makes one hash call per round, and each call is a separate WebCrypto digest, while PBKDF2 runs natively inside WebCrypto. Without a passwordparsePkcs12MacDataderives no key and does not apply the bound. See Security.maxAgeMsonvalidateCertificateRevocationListandcheckCertificateRevocationAgainstCrl,crlMaxAgeMsoncheckCertificateRevocationand the chain-levelRevocationPolicy, andresponderRevocationCrlMaxAgeMsonvalidateOcspResponsebound how old a CRL'sthisUpdatemay be. The chain-levelcrlMaxAgeMsalso covers the CRLs for CRL signers and delegated OCSP responders. A CRL older than the bound fails withstale_crl, and the chain reportscrl_expiredfor it.clockSkewMswidens each bound. No bound is set by default. RFC 5280 §3.3 leaves the required recency of revocation data to local policy. See Security.clockSkewMson the chain-levelRevocationPolicysets the tolerance for CRL and OCSPthisUpdateandnextUpdatechecks incheckChainRevocationandverifyCertificateChain({ revocation }).maxPathBuildingChecksonverifyCertificateChain,buildCandidatePathand thevalidateFor*profiles bounds how many issuer candidates and bare trust anchors one path search may examine, counting candidates it skips as already on the path or as a name mismatch. When the bound stops the search, the result ispath_building_limit_exceeded, which joinsVERIFY_ERROR_CODES. The default is 100,000.RFC 9879 PBMAC1 for the PKCS#12 MacData.
parsePfxDer,parsePfxPemandparsePkcs12MacDataverify a PBMAC1 MAC keyed by PBKDF2 with an HMAC-SHA-256, HMAC-SHA-384 or HMAC-SHA-512 PRF and MAC. The PBKDF2 params must carrykeyLength, and the password is encoded as UTF-8 with no NULL terminator.createPfxandcreatePkcs12MacDatacreate one withmac: { type: 'pbmac1', password }, using PBKDF2-HMAC-SHA-256, a 32-octet key and HMAC-SHA-256. The default MAC is unchanged.parsePkcs12MacDatareturns aParsedPkcs12MacDatadiscriminated bytype('pkcs12-kdf'or'pbmac1'). New codes onParsePfxErrorCodeandParsePkcs12MacDataErrorCode:unsupported_mac_algorithmfor a MAC or PBMAC1 variant micro509 does not handle,weak_mac_key_lengthfor a PBKDF2keyLengthbelow 20 octets, andpassword_not_utf8for a password with an unpaired UTF-16 surrogate. A PBMAC1 PBKDF2 count above 4294967295 returnsmalformed, as it does for PBES2.An opt-in RFC 9919 OCSP client profile.
profile: 'rfc9919'onvalidateOcspResponse, andocspProfile: 'rfc9919'oncheckCertificateRevocationand the chain-levelRevocationPolicy, reject a response withoutnextUpdate(RFC 9919 §5).validateOcspResponseandcheckCertificateRevocationreportnext_update_missing, and the chain reportsocsp_next_update_missing. The default'rfc6960'profile accepts such a response, as RFC 6960 §4.2.2.1 allows.CreatePkcs12MacDataErrorCode(invalid_iterations,password_not_bmp_string,password_not_utf8) is exported frommicro509andmicro509/pkcs.createPkcs12MacData, andcreatePfxthroughmac, throw these codes as aResultError.RFC 4985 §4 SRVName name constraints.
NameConstraintFormgains{ type: 'srv', value }, whose value is_Service.Name,_Service, orName. The builder writes it as an id-on-dnsSRV otherName with the Name in A-labels and refuses any other shape (invalid_srv_name_constraint); parsing decodes an id-on-dnsSRV otherName base to it, and it is accepted as an initial constraint. Path validation matches a SRVName SAN's service case-insensitively and its Name as that domain or a subdomain, both parts when the restriction has both. While a SRVName constraint is in force, a SRVName SAN that is not_Service.Namefails, and a malformed restriction fails every SRVName. Every SRVName, typed or received, is held to one profile: an RFC 6335 §5.1 service name and a Name of STD3 LDH labels with IDNA2008 A-labels. The builder stores U+3002, U+FF0E and U+FF61 as "." (RFC 4985 §3), in SANs, restrictions and initial constraints. A Name may end in the root dot of an absolute name. It is kept as written, and restriction matching compares its labels without the root label (RFC 3490 §2, §3.1).SubjectAltNamegainsotherName(typeIdand the DER of its value),x400Address,ediPartyName(content octets), andregisteredID(dotted OID). Parsing produces them where it producedunknown, and the builder encodes them; anotherNamewith the SRVName or SmtpUTF8Mailbox type-id is refused (other_name_type_id_has_variant), as is a value that is not one DER element or holds, at any depth, a universal-class element whose form or contents break X.690 or whose rules micro509 cannot check, or a SET or SET OF whose children follow neither DER order (invalid_other_name_value).x400Addresscontents must follow the RFC 5280 Appendix A.1 ORAddress schema andediPartyNamecontents the EDIPartyName, with each DirectoryString validated by its encoding (invalid_general_name_content); TeletexString, the Teletex and extended-network-address extension attributes, and extension-attribute types RFC 5280 does not define are refused as unsupported. A value or contents nested deeper than 64 levels throwlimit_exceeded.unknownremains as raw builder input.TeletexString Name attribute values decode in certificate, CRL, OCSP and PKCS #7 names and in
decodeDerString, which refused them before. Decoding follows the initial state X.690 §8.23.5.2 fixes: the T.61 primary set (register entry 102) with SPACE and DELETE, reading 2/3 as # and 2/4 as ¤ (T.61 Figure 2 Note 4). A C0 control function, an escape or shift sequence, a position T.61 Table 1 leaves empty, or an octet from 0x80 up is unsupported and returnsunsupported. A decoded TeletexString value compares like the other DirectoryString alternatives, after RFC 4518 preparation (RFC 5280 §7.1), so it matches a UTF8String or PrintableString value with the same characters in issuer/subject chaining, CRL and OCSP issuer matching and directoryName name constraints, with case and insignificant spaces disregarded.unsupportedandlimit_exceededon the parse error codes.unsupported, onParseCertificateErrorCode,ParseCertificateSigningRequestErrorCode,ParseCertificateRevocationListErrorCode,ParseOcspRequestErrorCode,ParseOcspResponseErrorCode,ParsePkcs7ErrorCode,ParsePfxErrorCodeandDecodeDerErrorCode, is input the profile may allow but micro509 does not decode, today a TeletexString octet outside the X.690 §8.23.5.2 initial state.limit_exceeded, on those and onImportKeyErrorCode,ImportEncryptedKeyErrorCode,ParsePfxErrorCodeandParsePkcs12MacDataErrorCode, is an implementation limit of micro509's own: an OBJECT IDENTIFIER sub-identifier encoded in more than 64 octets, a tag number of 2^53 or more, or DER or BER nested deeper than 64 levels, which returnedmalformedbefore. The throwing parsers throw aResultErrorcarrying the code.DECODE_REFUSAL_CODES, itsDecodeRefusalCodetype andDecodeFailureCode(malformedplus both) are exported frommicro509andmicro509/result, and each of those error code types is built from them.VERIFY_ERROR_CODES,MatchServiceIdentityErrorCodeand theverifyCertificateSigningRequestandcheckExtendedKeyUsagefailures carry both codes too, so a certificate or CSR source thatbuildCandidatePath,validateCandidatePath,verifyCertificateChain, thevalidateFor*profiles ormatchServiceIdentitycannot decode reports its refusal, andtrustAnchorFromCertificatethrows it.validateCandidatePathandcheckExtendedKeyUsagereport it at the index of the chain element they cannot decode. Anintermediatesorrootsentry thatbuildCandidatePathorverifyCertificateChaincannot load has no chain index, so its refusal, or itsissuer_not_foundwhen malformed, names the entry in the newdetails.source(VerifyFailureSource). A directoryName name constraint that micro509 cannot decode fails the chain with its refusal at the index of the certificate checked against it. The same holds forMatchCertificatePrivateKeyErrorCode,CheckCertificateRevocationAgainstCrlErrorCode,ValidateOcspResponseErrorCode,CheckCertificateRevocationErrorCode,RevocationIndeterminateReasonCode,CreatePkcs7CertBagErrorCode,CreatePkcs7SignedDataErrorCodeand theverifyCertificateRevocationListSignature,validateCertificateRevocationListandverifyOcspResponseSignaturefailures, where a CRL, OCSP response or request, or certificate that micro509 cannot decode was reported assignature_invalid,non_applicable,request_mismatch,malformed_certificate,invalid_certificateorinvalid_signer_certificate. A distribution point, issuing distribution point or CRL issuer directoryName that micro509 cannot decode failscheckCertificateRevocationAgainstCrlwith its refusal. The chain level treats an OCSP refusal like thesignature_invalidit replaces.checkChainRevocationrecords a CRL or OCSP response it cannot parse as aRevocationParseErrorwhosecodeismalformedor the refusal, andRevocationExecutionErroris the union of it andRevocationProcessingError.An OBJECT IDENTIFIER over the 64-octet limit inside RSASSA-PSS parameters made the parameters
malformed, so a certificate, CSR or CRL carrying them parsed, and verifying it reported unsupported algorithm parameters. Parsing now returnslimit_exceeded, andverifySignaturereturns it as the newVerifySignatureLimitFailure(VerifySignatureResult), as it does for PKCS #1 or ECDSA parameters or a signer SPKI past a decoding limit, which it reported as unsupported parameters or averification_error. A bare trust anchor whose SPKI is past a decoding limit fails path validation withlimit_exceededrather thansignature_invalid.verifyPkcs7SignedDatareports a refusal from signature verification with its code instead ofmalformed.rejectOversizedDisplayTextonverifyCertificateChainandvalidateCandidatePathrejects a certificate whose user noticeexplicitTextornoticeReforganization exceeds 200 characters with the newdisplay_text_oversizedverify code, which joinsVERIFY_ERROR_CODESwith the character count indetails.actualand the field in the newdetails.userNoticeField('explicitText'or'noticeRefOrganization'). By default such a certificate validates, and its parsed user notice carriesoversizedExplicitText, or itsnoticeRefcarriesoversizedOrganization, with the count (the newOversizedDisplayTexttype). RFC 5280 §4.2.1.4 asks certificate users to handle an oversized explicitText gracefully, and PKITS 4.8.19 leaves rejection to the application. The RFC says nothing about an oversized organization, and keeping it is micro509's receiving policy.URI-ID and SRV-ID matching accept a presented wildcard where RFC 9525 §6.3 allows one in the DNS domain name portion, such as
https://*.example.com/or_imap.*.example.com. It is the whole left-most label and matches one label. AsiporsipsURI-ID takes none (RFC 5922 §7.2).A URI-ID whose host is an IPv4 address or a bracketed IPv6 address matches by its octets (RFC 9525 §6.4).
Changed
- A typed SRVName SAN outside the RFC 6335 service grammar or STD3 LDH Name syntax is refused with the new
invalid_srv_name. - A critical subjectAltName holding a SRVName that is not
_Service.Nameunder that profile, or a SmtpUTF8Mailbox that holds a Byte Order Mark or lacks a non-ASCII RFC 6531 Local-part and a domain of NR-LDH labels and A-labels, carries information path validation cannot process (RFC 5280 §4.2), and the chain fails withunrecognized_critical_extension. - SRV-ID matching compares the Name of the reference identifier and of each presented SRVName without the root label, so
_imaps.example.com.and_imaps.example.commatch (RFC 3490 §2, §3.1). A Name with an empty label, a repeated terminal dot or no labels still matches nothing. - SRV-ID matching holds the reference identifier and each presented SRVName to the RFC 6335 §5.1 service name and STD3 LDH labels that the builder and name constraints already enforce. A reference such as
_123.example.comor_mail.example_comis malformed input, and a presented SRVName outside the grammar matches nothing. - A critical
otherNamename constraint fails closed only for SANs of the same type-id, following X.509 §9.4.2.2, where each type-id is its own name form. A UPN constraint no longer rejects a SRVName or SmtpUTF8Mailbox SAN, and a certificate carrying a UPN still fails under it. Theunsupported_name_constraintsdetail names the type-id (otherName 1.3.6.1.4.1.311.20.2.3). AnotherNameconstraint base is decoded intotypeIdandvalue, and a malformed one fails the parse. - A
registeredIDGeneralName whose OID is malformed fails the parse, in a certificate and in a CRL. A critical subjectAltName carrying aregisteredIDis processed rather than reported as anunrecognized_critical_extension; a criticalregisteredIDname constraint still fails closed. - A reference identifier's domain converts to A-labels by IDNA2008 lookup after RFC 5895 mapping (RFC 9525 §6.3), replacing the URL parser's UTS #46 processing. The mapping follows RFC 5895 §2 in order: each character to its Lowercase_Mapping,
<wide>and<narrow>characters to their decompositions from the Unicode 12.0.0 UCD, NFC, and U+3002 to ".". A reference that IDNA2008 disallows, such as♚.example, or with an ASCII label outside letters, digits, hyphens and underscores, matches nothing. A trailing root dot is kept. The host of a URI-ID and of a presented URI decodes its percent-encoded UTF-8 once before conversion (RFC 3986 §3.2.2), and a malformed encoding matches nothing. - An rfc822Name name constraint that names a particular mailbox (
user@example.com) is refused: the builder throwsemail_name_constraint_names_mailbox, and a caller-supplied initial constraint fails withunsupported_initial_name_constraints. RFC 9549 §2.2 removed that form. A certificate issued with one still validates as before. initialPolicySetcanonicalizes every OID. A list holding the anyPolicy OID means'any', and a list with a malformed OID matches no policy.- npm and JSR packages include
CHANGELOG.mdin the published tarball (package.jsonfiles,jsr.jsonpublish.include). trustedOcspRespondersoncheckChainRevocation()andverifyCertificateChain({ revocation })takesTrustedOcspResponderentries ({ issuerCertificate, responderCertificate }) instead of bare certificates. See Security.- BREAKING
createCertificateRevocationListrequiresnextUpdateand always encodes it (RFC 5280 §5.1.2.5). It throwsResultErrorcodenext_update_not_after_this_updatewhennextUpdatedoes not encode a later second thanthisUpdate, which defaults to now. This ordering is a micro509 builder invariant.CrlEncoderErrorCodegainsnext_update_not_after_this_update. Parsed CRLs keepnextUpdateoptional. - An RFC 7292 MAC password containing a UTF-16 surrogate (a non-BMP character or a lone surrogate), U+FFFE or U+FFFF is not a BMPString (RFC 7292 Appendix B.1, X.680 §41.15).
createPkcs12MacDataandcreatePfxthrowResultErrorcodepassword_not_bmp_stringfor it, andparsePkcs12MacData,parsePfxDerandparsePfxPemreturn that code.parsePfxDerandparsePfxPemreported it asmalformedbefore. - A PFX MAC with a digest other than SHA-256 returns
unsupported_mac_algorithminstead ofmalformed. parsePfxDerandparsePfxPemaccept BER for the PFX, the authSafe ContentInfo, the AuthenticatedSafe and each SafeContents: indefinite lengths, non-minimal lengths and constructed OCTET STRINGs (RFC 7292 §4). The MAC is verified over the AuthenticatedSafe octets as received. Certificates and PKCS#8 keys inside bags must still be DER. BER nesting is limited to 64 levels.ParsedPfxAttribute.valuesHexand the unknown bag'svalueDerhold the received value with definite lengths and universal constructed strings joined, so they are not guaranteed to be DER.- A presented URI-ID whose host is a percent-encoded U-label, such as
https://b%C3%BCcher.example/, no longer matches the referencehttps://xn--bcher-kva.example/. RFC 9525 §2 requires A-labels in the DNS domain name portion of a URI-ID, and RFC 3986 §6 decodes only percent-encoded unreserved characters. A reference URI-ID's host still decodes as UTF-8 and converts to A-labels (RFC 9525 §6.3).
Fixed
- Key import returned
malformedfor a decode limit inside a SubjectPublicKeyInfo, PKCS#8, SEC 1 or EncryptedPrivateKeyInfo, andinvalid_passwordfor one inside decrypted PKCS#8, PKCS#1 or SEC 1 content.verifyPkcs7SignedDatareturnedmalformedfor one inside signedAttrs. They now returnlimit_exceeded. - PFX parsing accepted any context-specific constructed tag where ContentInfo content, SafeBag bagValue and CertBag certValue are
[0] EXPLICIT. Any other tag is nowmalformed. - A BER implementation limit inside decrypted PFX SafeContents returned
invalid_password. It now returnslimit_exceeded. - Policy validation applied a policyMappings extension found in the end-entity certificate. RFC 5280 §6.1.3 runs the policy-mapping step of §6.1.4 only for certificates before the last, and RFC 9618 keeps that, so a leaf's mappings no longer rewrite or, under inhibitPolicyMapping, delete the policies that certificate asserts.
- The certificate builder accepted a
customExtensionscertificatePolicies payload whose user noticeexplicitTextwas an IA5String, which RFC 6818 §3 forbids for conforming CAs.explicitTextnow follows RFC 6818 §3 on both builder paths: IA5String fails withdisplay_text_ia5_string, control characters withdisplay_text_control_character, and UTF8String or BMPString text outside Unicode NFC withdisplay_text_not_nfc. A user notice gainsexplicitTextType, which parsing sets to the received string type and which the builder accepts as'visibleString'or'bmpString'(default'utf8String'), failing withinvalid_visible_stringorinvalid_bmp_stringwhen the text does not fit. - 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. - PBES2 parsing (encrypted PKCS#8 import and inspection, PFX key bags) rejects a PBKDF2
iterationCountthat is not an INTEGER. (https://github.com/kjanat/micro509/pull/113) - A PBKDF2
iterationCountabove 4294967295 returnsmalformed. Before, withmaxKdfIterationsset above 2^32, it reached WebCrypto and threw an uncaughtTypeErrorfrom a Result-returning import. exportEncryptedPkcs8Der/exportEncryptedPkcs8Pemand PFX creation throwRangeErrorforiterationsabove 4294967295, which previously passed validation.- An RFC 7292 MacData iteration count of 2^53 or more returns
kdf_iterations_exceededwhen a password is given. Without a password it returnsmalformed. - Chain evaluation applied the first delta CRL whose base CRL number matched, even an expired one. It now uses only a current delta (the evaluation time lies between its
thisUpdateandnextUpdate) whose CRL number exceeds the base CRL's number, and it prefers the one with the latestthisUpdate(RFC 5280 §5.2.4), then the higher CRL number when two share athisUpdatesecond (§5.2.3). The delta'sthisUpdatemust also be no earlier than the base CRL's (X.509 Annex E.5.2), and an equalthisUpdateis accepted.checkCertificateRevocationAgainstCrlreturnsnon_applicablewith reasondelta_crl_incompatiblefor a delta whosethisUpdateprecedes the complete CRL's (X.509 Annex E.5.2). - The CRL issuingDistributionPoint decoder read any non-zero BOOLEAN octet as TRUE and empty content as FALSE. A BOOLEAN whose content is not a single
0x00or0xFFoctet is nowmalformed(X.690 §11.1). - An invalid
Datepassed tocreateCertificate,createSelfSignedCertificate,createCertificateRevocationListorcreateOcspResponsecompared asNaN, skipped the ordering checks and surfaced as an uncodedRangeErrorfrom the DER encoder.CreateCertificateErrorCodegainsvalidity_date_invalid, andCrlEncoderErrorCodeandOcspEncoderErrorCodegaininvalid_date, thrown as aResultErrorbefore encoding. - Decoding ASN.1 text dropped a leading U+FEFF as a byte order mark. A UTCTime or GeneralizedTime whose contents began with the octets EF BB BF parsed as the time that followed, in certificates, CRLs, OCSP responses and
decodeDerTime; it is now malformed. A UTF8String keeps the character indecodeDerString, parsed names and DisplayText. - BMPString decoding and
derBmpStringaccepted U+FFFE and U+FFFF, which X.680 §41.15 leaves out of BMPString.decodeDerStringand name parsing now reject them as they reject surrogates,derBmpStringthrows, and a BMPString explicitText holding one fails withinvalid_bmp_string. - OBJECT IDENTIFIER decoding refused any arc above 2^53 − 1, so a certificate or CRL carrying a 2.25 UUID OID failed to parse, and the builder refused such an OID with
invalid_oid. Arcs now decode, encode and canonicalize exactly up to 64 octets per sub-identifier (values below 2^448). X.660 §7.6 leaves arcs unbounded; the bound is an implementation limit, checked before the arc is accumulated, since decoding grew quadratically with an arc's length. A longer arc returnslimit_exceededfrom every parser and from the builder, which checks each arc's decimal length before it builds the arc and adds the code toExtensionEncoderErrorCode. createPfxwrote anyfriendlyNameinto its BMPString, including surrogate pairs, U+FFFE, U+FFFF, an empty name and names over 255 characters, and PFX parsing accepted them. RFC 2985 §5.5.1 makes friendlyName one BMPString of 1 to 255 characters.createPfxnow throwsResultErrorcodeinvalid_friendly_namefrom the newPfxEncoderErrorCode, and parsing returnsmalformed.- DisplayText parsing (user-notice
explicitTextand thenoticeReforganization) replaced invalid UTF-8 with U+FFFD, accepted octets outside the IA5String and VisibleString repertoires, and accepted any length. It now decodes the tagged type strictly. An empty DisplayText and any other encoding returnmalformed. AnexplicitTextover 200 characters, such as PKITS 4.8.19's, is kept whole and reported asoversizedExplicitText, and an organization over 200 characters asoversizedOrganization. See Added. - CRL issuer and PKCS #7 signer issuer parsing decoded an attribute value that failed its string type's decoding as UTF-8 and replaced invalid sequences with U+FFFD. Such a value now returns
malformed, as it does in a certificate. - A UTF8String the builder wrote from a string holding a lone UTF-16 surrogate carried U+FFFD in its place, so the certificate held different text from the input. A lone surrogate in a name attribute now fails with the new
name_attribute_lone_surrogate, and in an explicitText ornoticeReforganization with the newdisplay_text_lone_surrogate. A surrogate pair and U+FFFD itself encode as before.
Security
- A received dNSName or rfc822Name ending in the root dot, such as
evil.example.com., matched no dNSName or rfc822Name constraint, so an excludedexample.comdid not exclude it. While constraints of its type are in force, such a name now fails withname_constraints_violated, and an initial dNSName or rfc822Name constraint ending in the root dot returnsunsupported_initial_name_constraints. - Chain-level revocation skipped a certificate carrying
id-pkix-ocsp-nocheckwhatever the extension held, andhasOcspNoCheckExtensioncounted it the same way. Only the NULL value RFC 6960 §4.2.2.2.1 defines now counts. - DNS service-identity matching ran the certificate's dNSName through the URL parser, which percent-decodes and parses IPv4 forms, so a SAN such as
a%62c.examplematched the referenceabc.exampleand0x7f.0.0.1matched127.0.0.1. A presented dNSName, and the Common Name fallback, now compare by a case-insensitive exact match (RFC 9549 §2.3). - A directoryName name constraint whose DN could not be decoded matched no name, so an excluded subtree excluded nothing. While one is in force, every subject DN and directoryName SAN fails with
name_constraints_violated. - A directoryName comparison that RFC 4518 string preparation cannot perform, such as one over a value holding a private-use character, counted as a mismatch, so an excluded subtree did not exclude the name. Such a comparison is now Undefined. It fails an excluded subtree and does not satisfy a permitted one. A multi-valued RDN matches when some one-to-one pairing of its attributes matches every pair, and is Undefined when no such pairing exists but one does with Undefined pairs allowed, whatever order its attributes take. Matching an RDN takes time linear in its attribute count, in CRL distribution point names too.
- URI name constraints and URI-ID matching read a URI SAN's host differently. The constraint took the WHATWG URL hostname, which left
ldap://%62locked.example/percent-encoded and kept the trailing dot ofhttps://blocked.example./, so both escaped ablocked.exampleexclusion, and the first still matched the URI-IDldap://blocked.example/. URI-ID matching cut every host at ";", sohttps://blocked.example;extra/matchedhttps://blocked.example/, and kept the trailing dot, sohttps://blocked.example./did not. Both now read the host by RFC 3986: a percent-encoded unreserved character decodes, the dot after the rightmost label is dropped, and a reg-name that is not a domain name after decoding, a percent-encoded U-label included, fails every URI constraint and matches no URI-ID. - A SIP or SIPS URI-ID was cut at "/", "?" or "#" before its userinfo "@" was found, so
sip:alice/phone@attacker.exampletook the hostaliceand matchedsip:alice/phone@victim.example. The host now follows the one "@" RFC 3261 §25.1 allows and ends at the first ";" or "?". A second "@", an empty user part, a "/" in the hostport, or a ":" with no port digits after it makes the URI-ID invalid, and so does asip://orsips://URI-ID, which §25.1 does not allow. - A presented SIP URI with a userinfo, such as
sip:alice@example.com, matched the URI-IDsip:example.com. It identifies a user, and RFC 5922 §7.1 forbids accepting it as a SIP domain identity, so it now matches nothing. A reference identifier may still carry a userinfo, and only its host is compared (RFC 5922 §7.3). - A URI's userinfo was dropped unread, so a presented
https://bad%zz@example.comtook the hostexample.com, matched the URI-IDhttps://example.com, and was evaluated against URI constraints. A userinfo outside RFC 3986 §3.2.1, or forsipandsipsoutside the RFC 3261 §25.1 user and password, now makes the host invalid. A reference identifier's userinfo follows RFC 3987 §2.2iuserinfo. - The path, query and fragment of a URI-ID or URI SAN, and the parameters and headers of a SIP URI-ID, were never read, so
https://example.com/%zzandsip:victim.example;%zzmatched the URI-IDshttps://example.com/andsip:victim.example, and the first was evaluated against URI constraints. They must now follow RFC 3986 §3.3 to §3.5, RFC 3987 §2.2 for a reference identifier, and the RFC 3261 §25.1 uri-parameters and headers, with every "%" opening an escaped octet and no SIP parameter name repeated. A SIP host must be a §25.1 hostname or an IP address. - A URI name constraint that was not a DNS name, such as
https://blocked.example, matched no host, so an excluded subtree excluded nothing. The builder refuses it with the newinvalid_uri_name_constraint, initial constraints returnunsupported_initial_name_constraints, and while a received one is in force every URI SAN fails withname_constraints_violated. - A dNSName or rfc822Name name constraint whose domain is malformed, such as
.example.com., matched no name, so an excluded subtree excluded nothing. While one is in force, every dNSName, rfc822Name or SmtpUTF8Mailbox of its type now fails withname_constraints_violated. - Caller-supplied initial name constraints accepted a dNSName, rfc822Name or URI base written with U-labels. Certificates carry A-labels (RFC 9549 §1), so such a base never matched, and an excluded subtree excluded nothing. A dNSName or rfc822Name base now converts to A-labels, and a URI base or a domain that is not valid IDNA2008 fails with
unsupported_initial_name_constraints. - A SmtpUTF8Mailbox subjectAltName (RFC 9598) parsed as an unrecognized otherName, so rfc822Name name constraints never reached it and an internationalized mailbox outside a permitted domain, or inside an excluded one, passed path validation. It now parses as
{ type: 'smtpUtf8Mailbox', value }and rfc822Name constraints bind it by domain (RFC 9598 §6). A received mailbox fails whenever rfc822Name constraints apply unless it holds no Byte Order Mark, its Local-part is a non-ASCII RFC 6531 Local-part, and its domain is NR-LDH labels and A-labels that pass the RFC 5893 Bidi rule. The builder emits it and enforces RFC 9598 §3:invalid_smtp_utf8_mailboxfor a missing@, a Byte Order Mark, a Local-part outside the RFC 6531 Dot-string or Quoted-string grammar, or a domain that is not lowercase NR-LDH labels and A-labels, andsmtp_utf8_mailbox_ascii_local_partfor a Local-part that fits an rfc822Name. A U-label domain is stored as A-labels, and acustomExtensionsSAN or IAN whose mailbox domain is not already stored that way fails withinvalid_smtp_utf8_mailbox. - CRL validation accepted a v3 issuer certificate with no keyUsage extension, so a CRL signed with a key certified for another purpose under the CRL issuer's name validated. A v3 CRL issuer certificate now needs keyUsage with
cRLSign, and fails withcrl_sign_not_permittedwithout it; v1 and v2 issuer certificates have no extensions and skip the check (RFC 10007 §4). - Chain-level CRL evaluation picked one delta CRL per base CRL from its unauthenticated
thisUpdateand CRL number, and when that delta failed validation it tried neither another delta nor the base CRL alone. A forged delta with a laterthisUpdatetherefore hid an authentic revoking delta, or a revocation listed in the base CRL itself, and a validated OCSPgoodthen allowed the revoked certificate under hard-fail. Applicable deltas are now tried newest first, and the base CRL is evaluated alone once every delta fails authentication, freshness, compatibility or applicability. A delta that passes those checks but whose revoked entries cannot settle the status makes the result indeterminate with the new reasondelta_crl_unusable. At most four deltas are checked per base CRL and 32 per call, skipping base CRLs that do not cover the certificate and repeated copies of a CRL, and a delta left unchecked makes the result indeterminate with the new reasondelta_crl_retry_limit_exceeded. Either reason outranks agoodverdict from OCSP or another CRL. Chain evaluation now reads a pre-parsed CRL from itsder, ascheckCertificateRevocationAgainstCrldoes, and reports one withoutderinexecutionErrors. Neither delta reason replaces a revocation that the base CRL lists for a reason other thancertificateHold, since no delta can remove it while the certificate is unexpired (RFC 5280 §5.3.1). - Decoding a DER INTEGER above
Number.MAX_SAFE_INTEGER, such as a PKCS#12 MacData or PBMAC1 iteration count, folded every octet into abigint, so a file with a very long INTEGER cost CPU and memory before the KDF budget could reject it. Decoding now stops at the first octet past the safe range. - Chain-level OCSP evaluation returned on the first validated
goodresponse, so arevokedresponse later inocspResponseswas never read and a revoked certificate passed withdecision: 'allow'. Every applicable response is now validated; any validatedrevokedverdict wins regardless of position, and otherwise the freshest validatedgoodresponse bythisUpdateis reported. AcertificateHold(RFC 5280 §5.3.1 reason 6) still denies unless a validatedgoodresponse carries a laterthisUpdate, which clears the hold. (https://github.com/kjanat/micro509/pull/108, https://github.com/kjanat/micro509/pull/110) trustedOcspResponderson chain-level revocation was a flat list applied to every issuer in the chain, so a responder trusted for one CA could assert status for certificates issued by any other CA in the same path (RFC 6960 §4.2.2.2 criterion 1 binds local trust to the issuing CA). Each entry now names the issuer it is trusted for, and only responders bound to the issuer under evaluation reachvalidateOcspResponse. (https://github.com/kjanat/micro509/pull/111)- URI-ID matching accepted a scheme-specific path as an authority, so a hostless SAN such as
https:verify.examplematched the reference identifierhttps://verify.exampleinverifyCertificateChainandvalidateForTlsServer. A URI now needs a syntactically valid scheme and an explicit//authority (RFC 3986 §3.2), and its reg-name must normalize as a DNS name;sip:andsips:keep their authority-less form (RFC 3261 §19.1). (https://github.com/kjanat/micro509/pull/109) ecdsaSignatureDerToRawand the ECDSA verify path trimmed leading zero bytes from eachECDSA-Sig-ValueINTEGER without checking the encoding, so an empty, negative, or non-minimally encodedrors(X.690 §8.3) normalized into a raw signature WebCrypto could verify, giving a second accepted encoding of one signature. Such INTEGERs are now rejected. (https://github.com/kjanat/micro509/pull/100)verifyPkcs7SignedDatareportedok: truefor aSignedDatawhosesignerInfosset was empty, with embeddedeContentor with detachedcontentfrom the caller, verifying nothing. ASignedDatawith no signer now fails withno_signers, a newVerifyPkcs7SignedDataErrorCode. (https://github.com/kjanat/micro509/pull/106)- Distinguished-name comparison classified combining marks with the runtime's
\p{M}, which tracks the host Unicode version. RFC 4518 §2.6.1 keys insignificant-space handling on combining marks, and Appendix A lists them definitively against the Unicode 3.2 repertoire §2.1 fixes. A code point reclassified since 3.2 (U+1885, U+06DE) changed which spaces survived, so a directoryName excluded subtree could fail to match. The Appendix A set is now generated from the vendored RFC text and guarded by a test that re-derives it. (https://github.com/kjanat/micro509/pull/102) - A caller-supplied extension decoder that threw a native error (for example
TextDecoderwithfatal: trueon invalid UTF-8) escaped the Result-returning certificate and CSR parse functions as an exception. The decoder boundary now maps such errors tocode: 'malformed', so attacker bytes in an extension cannot crash a parser call. (https://github.com/kjanat/micro509/pull/107) - The
site/guide/revocation.mdCRL example looked up revoked entries after a failed signature check. It now callsvalidateCertificateRevocationListand reads entries only from the validated value. (https://github.com/kjanat/micro509/pull/104) - Third-party GitHub Actions in the release, test, and site workflows are pinned to commit SHAs again; mutable tags had let upstream ref movement run unreviewed code with
id-token: writeandcontents: write. (https://github.com/kjanat/micro509/pull/103) - Encrypted PKCS#8 import and PFX parsing ran PBKDF2, and PFX MAC verification ran the PKCS#12 KDF, for whatever iteration count the file encoded before the password or ciphertext could be rejected. A 122-byte EncryptedPrivateKeyInfo or a 177-byte PFX carrying
0x7fffffffiterations held the CPU for minutes. Counts above the ceiling are now refused before derivation;maxKdfIterationsadjusts it. - A PFX gave every encrypted entry the full
maxKdfIterationsallowance, so a file with many small entries could demand unbounded work without any encoded count standing out: 100 entries at the default ceiling ask for 200 million PBKDF2 rounds. One budget now covers the whole file. - Bare trust anchors were re-verified on every visit to a certificate, because the anchor match ran before the dead-end lookup. A bundle of same-subject CAs plus a few subject-matching anchors made anchor signature checks grow with the search graph rather than the input: 40 candidates and 20 anchors cost 3.7s. Each certificate-and-anchor pair is now checked once per search.
- Path building memoized dead ends per visited set, so a bundle of
nsame-subject CA certificates sharing one key cost on the order ofn³signature verifications beforeno_trusted_root(30 candidates: 9,426 key imports and 18,852 verify calls). Dead ends are now memoized per certificate and CA count, and each certificate-to-key signature check runs once per search. - A CRL without
nextUpdatevalidates at any later time by default, so a replayed CRL from before a revocation stays usable. RFC 5280 §5.1.2.5 requires conforming issuers to includenextUpdateand does not specify how a client handles a CRL without it, and §3.3 leaves the required recency of revocation data to local policy. The newmaxAgeMs,crlMaxAgeMsandresponderRevocationCrlMaxAgeMsoptions reject a CRL whosethisUpdateis older than the bound. The CRL APIs reportstale_crl, and the chain reportscrl_expired. No bound is set by default. - The dprint TOML formatter installed
tombiunversioned and globally from npm on every fresh setup, so the registry chose the code that ran.tombiis now a catalog-pinned devDependency; the exec plugin runsnode_modules/.bin/tombiand its setup command isbun install. - The OpenSSL differential tests ran against whatever OpenSSL the runner image shipped. CI now installs a pinned OpenSSL 4.0.2 from
ghcr.io/kjanat/openssl-prebuilt, verifies its build provenance attestation, and fails unlessopenssl versionreports 4.0.2, so an oracle change surfaces as a version assertion instead of as verdict or formatting drift. Usage of the public builds is documented in.github/openssl.
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 under DER). 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 under DER);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.