Getting Started
Prerelease
micro509 is 0.x — API may change before 1.0.
Usage
Install
npm install micro509pnpm add micro509yarn add micro509bun add micro509deno add jsr:@kjanat/micro509Browser
No build step: micro509 is WebCrypto and nothing else, so a module script can import it straight from a CDN.
<script type="module">
import { createSelfSignedCertificate } from 'https://esm.run/micro509';
const { certificate } = await createSelfSignedCertificate(
{
subject: { commonName: 'example.com' },
validity: { days: 30 },
},
);
console.log(certificate.pem);
</script>Subpaths work the same way — https://esm.run/micro509/x509 — and pinning a version (https://esm.run/micro509@0.14.0) is what you want in production, so a release cannot change under you.
examples/browseris that, in full: one HTML file that issues a certificate and parses it back, with nothing installed and nothing built. open in stackblitzexamples/vite: the same demo with types and a dev server. open in stackblitz
Deno
import * as micro509 from '@kjanat/micro509';
// or import directly in code:
import * as micro509 from 'jsr:@kjanat/micro509';Quick Start
Create a self-signed certificate
import { createSelfSignedCertificate } from 'micro509';
const { certificate, keyPair } =
await createSelfSignedCertificate({
subject: {
commonName: 'example.com',
organization: 'Acme',
country: 'US',
},
validity: { days: 30 },
extensions: {
keyUsage: ['digitalSignature', 'keyEncipherment'],
subjectAltNames: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'www.example.com' },
],
},
});
console.log(certificate.pem);
console.log(await keyPair.exportPkcs8Pem());Create a CSR
import {
createCertificateSigningRequest,
generateKeyPair,
} from 'micro509';
const keyPair = await generateKeyPair({ kind: 'ed25519' });
const csr = await createCertificateSigningRequest({
subject: { commonName: 'csr.example' },
publicKey: keyPair.publicKey,
signerPrivateKey: keyPair.privateKey,
extensions: {
subjectAltNames: [
{ type: 'dns', value: 'csr.example' },
],
},
});
console.log(csr.pem);Parse a certificate
import {
parseCertificatePem,
createSelfSignedCertificate,
subjectAltNameToString,
} from 'micro509';
const { certificate } = await createSelfSignedCertificate({
subject: {
commonName: 'example.com',
organization: 'Acme',
},
validity: { days: 365 },
extensions: {
keyUsage: ['digitalSignature', 'keyEncipherment'],
subjectAltNames: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' },
],
},
});
const result = parseCertificatePem(certificate.pem);
if (!result.ok) {
console.log(`parse failed: ${result.error.code}`);
} else {
const parsed = result.value;
const sans = (parsed.subjectAltNames ?? [])
.map((name) => subjectAltNameToString(name))
.join(', ');
console.log(`\
subject: ${parsed.subject.values.commonName}
org: ${parsed.subject.values.organization}
serial: ${parsed.serialNumberHex}
sig algo: ${parsed.signatureAlgorithmName}
pubkey: ${parsed.publicKeyAlgorithmName}
key usage: ${parsed.keyUsage?.flags.join(', ') ?? 'none'}
SANs: ${sans}`);
}Verify a chain
import {
createSelfSignedCertificate,
createCertificate,
generateKeyPair,
verifyCertificateChain,
} from 'micro509';
// Create a CA root
const ca = await createSelfSignedCertificate({
subject: { commonName: 'Demo Root CA' },
extensions: {
basicConstraints: { ca: true },
keyUsage: ['keyCertSign', 'cRLSign'],
},
});
// Issue a leaf signed by the CA
const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
issuer: { commonName: 'Demo Root CA' },
subject: { commonName: 'app.example.com' },
publicKey: leafKeys.publicKey,
signerPrivateKey: ca.keyPair.privateKey,
issuerPublicKey: ca.keyPair.publicKey,
extensions: {
subjectAltNames: [
{ type: 'dns', value: 'app.example.com' },
],
},
});
// Verify the CA → leaf chain
const result = await verifyCertificateChain({
leaf: leaf.pem,
roots: [ca.certificate.pem],
serviceIdentity: {
type: 'dns',
value: 'app.example.com',
},
});
if (result.ok) {
const { leaf: parsed } = result.value;
console.log(`\
verified ${parsed.subject.values.commonName}
issuer: ${parsed.issuer.values.commonName}
serial: ${parsed.serialNumberHex}
chain length: ${result.value.chain.length}`);
}Reject a self-signed leaf
import {
createSelfSignedCertificate,
verifyCertificateChain,
} from 'micro509';
const { certificate } = await createSelfSignedCertificate({
subject: { commonName: 'rogue.example' },
});
// Self-signed leaf is rejected even when listed as a root
const trusted = await verifyCertificateChain({
leaf: certificate.pem,
roots: [certificate.pem],
});
// Explicit opt-in allows it for development use
const selfSigned = await verifyCertificateChain({
leaf: certificate.pem,
roots: [certificate.pem],
/** Allow a self-signed leaf. @default false */
allowSelfSignedLeaf: true,
});
console.log(`\
trusted: ${trusted.ok} (${!trusted.ok && trusted.error.code})
opt-in: ${selfSigned.ok}
serial: ${selfSigned.ok ? selfSigned.value.leaf.serialNumberHex : ''}`);Working with results
Anything that parses untrusted input returns a Result: { ok: true, value } or { ok: false, error } with a stable machine-readable error.code. Builders taking developer-supplied config throw instead, and the thrown error carries the same kind of code.
import {
createSelfSignedCertificate,
parseCertificatePem,
unwrap,
unwrapOr,
} from 'micro509';
import { isResultError } from 'micro509/result';
const { certificate } = await createSelfSignedCertificate({
subject: { commonName: 'results.example' },
});
// Branch on ok for full control...
const parsed = parseCertificatePem(certificate.pem);
if (parsed.ok) {
console.log(
`subject: ${parsed.value.subject.values.commonName}, serial ${parsed.value.serialNumberHex}`,
);
}
// ...unwrap() to throw on failure...
const known = unwrap(parseCertificatePem(certificate.pem));
// ...or unwrapOr() for a fallback value.
const fallback = unwrapOr(
parseCertificatePem('not a pem'),
known,
);
console.log(
`fallback: ${fallback.subject.values.commonName}`,
);
// An unwrap() throw still carries the typed code.
try {
unwrap(parseCertificatePem('not a pem'));
} catch (error) {
if (isResultError(error)) {
console.log(`caught code: ${error.code}`);
}
}When wrapping a throwing operation into your own Result-returning code, rethrowIfInvariant(error) keeps the library's discipline: it rethrows programmer bugs (TypeError, RangeError, …) so they are never flattened into a "malformed input" failure, and returns for everything else.
Imports
Use the root package for most applications:
import {
createCertificate,
parseCertificatePem,
verifyCertificateChain,
} from 'micro509';Use domain entrypoints for exhaustive advanced types or a narrower workflow surface:
import { parseCertificatePem } from 'micro509/x509';
import {
verifyCertificateChain,
matchServiceIdentity,
} from 'micro509/verify';
import {
createOcspRequest,
checkCertificateRevocation,
} from 'micro509/revocation';
import { createPfx } from 'micro509/pkcs';
import { signData, verifySignature } from 'micro509/crypto';
import { generateKeyPair } from 'micro509/keys';
import { pemDecode, pemEncode } from 'micro509/pem';
import { readDerRoot, decodeDerOid } from 'micro509/der';
import type { Micro509Error } from 'micro509/result';