runlocally

runlocally engineering notes

Decode Certificate

How Decode Certificate is built

By Geppetto · · Open Decode Certificate →

Decode Certificate parses X.509 certificates and PKCS#10 certificate signing requests from pasted PEM text or an uploaded .pem/.crt/.cer/.csr/.der file. This post is about the ASN.1/DER encoding those formats sit on top of, how @peculiar/x509 is used to decode them, and the field-extraction logic that turns a parsed certificate or CSR into a display.

Tech used

ASN.1, DER, and PEM

X.509 certificates and PKCS#10 CSRs are both defined as ASN.1 structures — a schema language for describing nested, typed records (sequences, integers, bit strings, object identifiers) independent of any particular byte layout. DER (Distinguished Encoding Rules) is the byte layout: a tag-length-value encoding where every value has exactly one valid representation, which is what lets a certificate be hashed or signature-verified deterministically. PEM is a text wrapper around DER — the DER bytes, base64-encoded, framed by a -----BEGIN <TYPE>----- / -----END <TYPE>----- header pair naming what’s inside (CERTIFICATE, CERTIFICATE REQUEST, PRIVATE KEY, and others). A .pem/.crt/.cer/.csr file is normally PEM text; a .der file is the same DER bytes with no PEM wrapper at all.

This tool’s format-detection logic runs on that distinction directly. certEngine.ts’s decode() checks the input text for a -----BEGIN marker: if present, it’s treated as PEM and handed to x509.PemConverter.decodeWithHeaders; otherwise, if a file buffer is present, it’s treated as raw DER bytes. PemConverter.decodeWithHeaders splits the PEM text into every armored block it contains along with each block’s header tag — which is what makes multi-certificate chains and mixed-content pastes possible to handle at all, described below.

@peculiar/x509

@peculiar/x509 is the library doing the actual ASN.1/DER parsing, exposed as X509Certificate and Pkcs10CertificateRequest classes that wrap the parsed structure and expose typed accessors (subjectName, issuerName, notBefore/notAfter, publicKey, signatureAlgorithm, getExtension(oid), and more) instead of requiring callers to walk ASN.1 nodes by hand. It’s built on WebCrypto for anything cryptographic (public key parsing, signature algorithm identification) rather than shipping its own crypto primitives.

One integration detail the code comments flag explicitly: @peculiar/x509 uses tsyringe for internal dependency injection, which throws “tsyringe requires a reflect polyfill” unless reflect-metadata is imported before anything from the package is touched. certEngine.ts imports reflect-metadata as its first line specifically to satisfy that requirement — the package’s own README doesn’t foreground this in its usage examples.

Decoding tries X509Certificate first; if the constructor throws, the same raw bytes are retried as Pkcs10CertificateRequest; if that also throws, the tool surfaces an errNotCertOrCsr error rather than a raw exception. This certificate-then-CSR order is fixed in decodeOne() in certEngine.ts and applies uniformly whether the input is a single DER file or one block out of a multi-block PEM paste.

PKCS#10 CSR structure

A PKCS#10 certificate signing request is a smaller ASN.1 structure than a certificate: it carries a requested subject name, a public key, and an optional set of requested extensions (most commonly a requested Subject Alternative Name list) inside an extensionRequest attribute — but it has no issuer, no validity period, and no signature over an issuer’s decision, since nothing has certified it yet. It’s what a server operator generates and sends to a CA; the CA turns it into a certificate by adding an issuer, a validity window, and a serial number, then signing the result.

certFields.ts reflects this directly in CertDisplayItem: the certificate-only fields (issuer, notBefore/notAfter, expiry, serialNumberHex, fingerprintSha1/fingerprintSha256) are typed as optional and only populated in toDisplayItem() when item.kind === 'certificate'. A decoded CSR gets subject, san, publicKey, and signatureAlgorithm (the signature the requester put on the CSR to prove possession of the private key) and nothing else.

One cross-type inconsistency the code works around: getExtension(SubjectAlternativeNameExtension) — the class-reference overload — reliably returns the SAN extension on X509Certificate but returned null on Pkcs10CertificateRequest in testing against real CSR fixtures. sanEntries() in certFields.ts uses the OID-string overload instead (getExtension('2.5.29.17'), the SAN extension’s dotted OID), which was verified to work uniformly on both types, and reads that codepath in favor of the class-reference form everywhere in this tool.

Implementation & operational notes

Certificate chains are decoded block-by-block, independently. A pasted PEM can contain multiple -----BEGIN CERTIFICATE----- blocks — a full chain dumped from a server. decodePemText() iterates every block PemConverter.decodeWithHeaders returns and decodes each on its own, so a chain becomes a list of independently-inspectable items rather than one opaque blob; the UI (DecodeCertificateTool.tsx) renders this as a clickable chain list when more than one item decodes successfully.

Private-key blocks are recognized and skipped, never parsed. Blocks whose PEM header matches PRIVATE KEY (via a /PRIVATE KEY/i regex against the block’s tag — catching PRIVATE KEY, RSA PRIVATE KEY, EC PRIVATE KEY, ENCRYPTED PRIVATE KEY, and similar) are counted and skipped before any parsing is attempted, tracked separately as skippedPrivateKeyCount in the returned DecodeSummary. This exists because people do paste a fullchain.pem alongside its private key into decoders by habit; the engine never reads that block’s content at all — the skip happens on the header tag alone, before the bytes are touched. Blocks that decode as neither certificate nor CSR (a CRL, a PKCS#7 bundle, garbage) are separately counted as skippedOtherCount rather than aborting the whole paste.

Everything, including hashing, runs on the main thread. Unlike sibling tools in this catalog that decode formats like HEIC through a Web Worker because the decode step is CPU-heavy, certificate/CSR parsing and the SHA-1/SHA-256 fingerprint digests (computed over each certificate’s raw DER bytes via crypto.subtle.digest) are cheap enough that DecodeCertificateTool.tsx runs them directly in an effect keyed on the input state, no worker involved. That digest call is asynchronous, though, which is why this component uses a useEffect with a cancelled guard flag instead of the synchronous useMemo most of its sibling text-input tools use — without the guard, a fast edit could let a stale digest result overwrite a newer one after the fact.

No certificate or CSR data leaves the browser. Both the PEM-block splitting (PemConverter.decodeWithHeaders) and the ASN.1 parsing (X509Certificate/Pkcs10CertificateRequest) run against bytes already sitting in browser memory — a File’s arrayBuffer() or the textarea’s string value — and the fingerprint digests use the browser’s own crypto.subtle API. There is no network call anywhere in this decode path, so a certificate pasted or dropped into the tool has no server round-trip to leak through.

Public key sizing has an explicit no-fabrication rule. publicKeyInfo() reports an RSA key’s bit length directly from modulusLength. For an EC key, it looks up the named curve (P-256, P-384, P-521, Ed25519, and a handful of others) in a small hardcoded table to render a bit size alongside the curve name; a curve not in that table shows only the curve name, deliberately, rather than guessing at a bit count for a curve the code doesn’t recognize.

File-type acceptance is extension-only, not MIME-type-based. isAcceptedCertFile() in fileValidation.ts checks the file’s extension against .pem/.crt/.cer/.csr/.der and nothing else — the code notes that browsers and OSes report inconsistent, often-empty, or generic application/octet-stream MIME types for these extensions, so extension is the only reliable signal at the file-picker stage. An accepted extension with content that isn’t actually a valid certificate or CSR still fails cleanly at the parsing step, surfacing the same errNotCertOrCsr message.

Try it / source

Decode Certificate

Open the tool → All posts →