Skip to content
ecdsa.com

PEM ⇄ DER · WebCrypto

How to convert a certificate and a private key between PEM and DER in WebCrypto

To convert between PEM and DER in WebCrypto, decode the base64 between the BEGIN and END lines with atob into a Uint8Array, and go back with btoa and 64-character lines. crypto.subtle.importKey takes only the DER ("pkcs8", "spki"), so this conversion is the step before every key import in the browser; the recipe checks the round trip and imports the key.

Tested against the WebCrypto API in WebCrypto Node.js 24.22.0 (globalThis.crypto.subtle, atob and btoa) — the same crypto.subtle interface browsers implement.

pem.mjs — PEM ⇄ DER for any labelled block, browser APIs only
// PEM is the base64 of the DER between two labelled lines; crypto.subtle wants the DER
export function pemToDer(pem, label) {
const match = new RegExp(`-----BEGIN ${label}-----([A-Za-z0-9+/=\\s]+)-----END ${label}-----`).exec(pem);
if (!match) throw new Error(`no ${label} block`);
const binary = atob(match[1].replace(/\\s+/g, ""));
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
}
export function derToPem(der, label) {
let binary = "";
for (const byte of new Uint8Array(der)) binary += String.fromCharCode(byte);
const lines = btoa(binary).match(/.{1,64}/g).join("\n");
return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;
}

How it works

  1. pemToDer extracts the block with the expected label, removes the line breaks and decodes with atob: 563 bytes for the certificate.
  2. crypto.subtle.importKey("pkcs8", der, …) takes the key's DER; exportKey("pkcs8", key) returns 138 bytes, and derToPem turns them into a PEM identical to the input.
  3. derToPem builds the binary string byte by byte before btoa and splits the base64 into 64-character lines, as PEM requires.
  4. Everything stays in the page: the conversion uses atob, btoa and crypto.subtle only.

Gotchas

  • importKey("pkcs8", …) with the PEM text itself rejects with DataError — it wants the decoded DER bytes.
  • "pkcs8" means PKCS#8 only: a SEC1 key (BEGIN EC PRIVATE KEY) decoded to DER also fails with DataError. Convert it to PKCS#8 first (openssl pkcs8 -topk8 -nocrypt).
  • String.fromCharCode(...bytes) spreads every byte as an argument and throws RangeError for large inputs (2 MB in the run); build the string in a loop, as derToPem does.

Related recipes