Skip to content

Keys

Generate a key pair

ts
import { generateKeyPair } from 'micro509';

// Ed25519
const ed = await generateKeyPair({ kind: 'ed25519' });

// ECDSA P-256
const ec = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});

// RSA 2048
const rsa = await generateKeyPair({
  kind: 'rsa',
  modulusLength: 2048,
});

console.log(
  'generated:',
  ed.publicKey.algorithm.name,
  ec.publicKey.algorithm.name,
  rsa.publicKey.algorithm.name,
);

Import and export

PKCS#8 (private keys)

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportPkcs8Pem,
  importPkcs8Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});
const pem = await exportPkcs8Pem(keys.privateKey);

const privateKey = unwrap(
  await importPkcs8Pem(pem, {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);
const exported = await exportPkcs8Pem(privateKey);

console.log(`\
algorithm:  ${JSON.stringify(privateKey.algorithm)}
usages:     ${privateKey.usages.join(', ')}
PEM base64: ${pem.split('\n')[1]?.slice(0, 44)}…
round-trip: ${exported === pem}`);

The algorithm argument is optional. A PKCS#8 PrivateKeyInfo encodes its own privateKeyAlgorithm (and, for EC keys, the curve), so importPkcs8Pem, importPkcs8Der, and importPkcs8Base64 — and their encrypted siblings — infer it when no hint is given, which is what you want for user-provided key material whose type isn't known ahead of time:

ts
// Inferred from the DER — no { kind, curve } needed.
const privateKey = unwrap(await importPkcs8Pem(pem));

Passing an explicit algorithm still works and additionally asserts that the key matches it, failing with a 'malformed' result on a mismatch. An RSA key inferred without a hint defaults to pkcs1-v1_5 with SHA-256 (a plain rsaEncryption envelope encodes neither padding scheme nor hash) — pass { kind: 'rsa', scheme: 'pss' } or an explicit hash when you need something else.

SPKI (public keys)

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportSpkiPem,
  importSpkiPem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});
const pem = await exportSpkiPem(keys.publicKey);

const publicKey = unwrap(
  await importSpkiPem(pem, {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);
const exported = await exportSpkiPem(publicKey);

console.log(`\
algorithm:  ${JSON.stringify(publicKey.algorithm)}
usages:     ${publicKey.usages.join(', ')}
PEM base64: ${pem.split('\n')[1]?.slice(0, 44)}…
round-trip: ${exported === pem}`);

The algorithm argument is optional for SPKI imports. A SubjectPublicKeyInfo already encodes its algorithm OID (and, for EC keys, the curve), so importSpkiPem, importSpkiDer, and importSpkiBase64 infer it when no hint is given — handy when the key type isn't known ahead of time:

ts
// Inferred from the DER — no { kind, curve } needed.
const publicKey = unwrap(await importSpkiPem(pem));

Passing an explicit algorithm still works and additionally asserts that the key matches it, failing with a 'malformed' result on a mismatch.

Derive a public key from a private key

The import* functions for private keys hand back a bare CryptoKey with only sign usage — there is no matching public handle. derivePublicKey bridges that gap, so a private key loaded from disk can go straight to exportSpkiPem/exportSpkiDer (e.g. to rebuild a self-signed certificate or distribute the public key). It supports RSA, ECDSA, and Ed25519.

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  derivePublicKey,
  exportSpkiPem,
  importPkcs8Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});

// Start from a private key that carries no public handle.
const privateKey = unwrap(
  await importPkcs8Pem(await keys.exportPkcs8Pem(), {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);

const publicKey = await derivePublicKey(privateKey);
const spki = await exportSpkiPem(publicKey);

console.log(
  'derived spki matches:',
  spki === (await keys.exportSpkiPem()),
);

JWK

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportPrivateJwk,
  exportPublicJwk,
  importPrivateJwk,
  importPublicJwk,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});
const pubJwk = await exportPublicJwk(keys.publicKey);
const privJwk = await exportPrivateJwk(keys.privateKey);

const publicKey = unwrap(
  await importPublicJwk(pubJwk, {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);
const privateKey = unwrap(
  await importPrivateJwk(privJwk, {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);

console.log(`\
kty/crv: ${pubJwk.kty}/${pubJwk.crv}
x:       ${pubJwk.x?.slice(0, 22)}…
y:       ${pubJwk.y?.slice(0, 22)}…
usages:  ${publicKey.usages.join(', ')} | ${privateKey.usages.join(', ')}`);

The algorithm argument is optional for JWK imports too: kty, crv, and alg already describe the key, so importPublicJwk(jwk) and importPrivateJwk(jwk) infer it. The JWA alg member selects the RSA scheme and hash (RS384 → PKCS#1 v1.5/SHA-384, PS256 → PSS/SHA-256, RSA-OAEP-256 → OAEP/SHA-256); an RSA JWK without alg defaults to PKCS#1 v1.5 with SHA-256.

PKCS#1 (RSA-specific)

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportPkcs1Pem,
  importPkcs1Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'rsa',
  modulusLength: 2048,
});
const pem = await exportPkcs1Pem(keys.privateKey);

const privateKey = unwrap(
  await importPkcs1Pem(pem, {
    kind: 'rsa',
    scheme: 'pkcs1-v1_5',
  }),
);
const exported = await exportPkcs1Pem(privateKey);

console.log(`\
algorithm:  ${privateKey.algorithm.name}
pem lines:  ${pem.split('\n').length}
PEM base64: ${pem.split('\n')[2]?.slice(0, 44)}…
round-trip: ${exported === pem}`);

SEC1 (EC-specific)

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportSec1Pem,
  importSec1Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});
const pem = await exportSec1Pem(keys.privateKey);

const privateKey = unwrap(
  await importSec1Pem(pem, {
    kind: 'ecdsa',
    curve: 'P-256',
  }),
);
const exported = await exportSec1Pem(privateKey);

console.log(`\
algorithm:  ${JSON.stringify(privateKey.algorithm)}
PEM base64: ${pem.split('\n')[1]?.slice(0, 44)}…
round-trip: ${exported === pem}`);

Exported SEC1 keys always embed the RFC 5915 parameters [0] named curve (matching OpenSSL), so importSec1Pem(pem) infers the curve when the algorithm argument is omitted. A minimal SEC1 encoding without the embedded curve still needs the explicit { kind: 'ecdsa', curve }.

Encrypted keys

Encrypted PKCS#8

ts
import { generateKeyPair } from 'micro509';
import {
  exportEncryptedPkcs8Pem,
  importEncryptedPkcs8Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'ecdsa',
  curve: 'P-256',
});

// Export with PBES2 encryption
const pem = await exportEncryptedPkcs8Pem(keys.privateKey, {
  password: 'password',
});

// Import with the same password — returns a Result
const result = await importEncryptedPkcs8Pem(
  pem,
  'password',
  {
    kind: 'ecdsa',
    curve: 'P-256',
  },
);

if (!result.ok) {
  // result.error.code is 'invalid_password' on a wrong password,
  // or 'malformed' on structurally invalid input
  throw new Error(result.error.code);
}

// A wrong password is a typed failure, not an exception
const wrong = await importEncryptedPkcs8Pem(pem, 'nope');

console.log(`\
algorithm:  ${JSON.stringify(result.value.algorithm)}
ciphertext: ${pem.split('\n')[1]?.slice(0, 44)}…
wrong pw:   ok=${wrong.ok} (${wrong.ok ? '' : wrong.error.code})`);

Legacy encrypted PEM (OpenSSL format)

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  exportEncryptedPkcs1Pem,
  importEncryptedPkcs1Pem,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'rsa',
  modulusLength: 2048,
});

// Export RSA key with AES-256-CBC
const pem = await exportEncryptedPkcs1Pem(keys.privateKey, {
  password: 'password',
  cipher: 'AES-256-CBC',
});

// Import with the same password
const privateKey = unwrap(
  await importEncryptedPkcs1Pem(pem, 'password', {
    kind: 'rsa',
    scheme: 'pkcs1-v1_5',
  }),
);

// The DEK-Info header carries the (random) AES-CBC IV
const dekInfo = pem
  .split('\n')
  .find((line) => line.startsWith('DEK-Info'));

console.log(`\
DEK-Info:  ${dekInfo?.slice('DEK-Info: '.length)}
algorithm: ${privateKey.algorithm.name}
usages:    ${privateKey.usages.join(', ')}`);

RSA-OAEP encryption

Generate or import RSA keys with scheme: 'oaep' to get an encryption pair (encrypt/decrypt usages instead of sign/verify), then use encryptRsaOaep / decryptRsaOaep. RSA-OAEP fits at most modulus bytes − 2 × hash bytes − 2 per call (190 bytes for RSA-2048 with SHA-256) — encrypt a symmetric key, not bulk data.

An optional label is bound to the ciphertext: decryption fails unless the exact same label is presented. Decryption failures are deliberately opaque (decryption_failed) — OAEP does not reveal whether the key, label, or ciphertext was wrong.

ts
import { generateKeyPair, unwrap } from 'micro509';
import {
  decryptRsaOaep,
  encryptRsaOaep,
} from 'micro509/keys';

const keys = await generateKeyPair({
  kind: 'rsa',
  scheme: 'oaep',
  modulusLength: 2048,
});

const label = new TextEncoder().encode('context-v1');
const ciphertext = unwrap(
  await encryptRsaOaep(
    keys.publicKey,
    new TextEncoder().encode('session key'),
    { label },
  ),
);

const plaintext = unwrap(
  await decryptRsaOaep(keys.privateKey, ciphertext, {
    label,
  }),
);

// Wrong label: opaque failure, never a partial plaintext
const wrongLabel = await decryptRsaOaep(
  keys.privateKey,
  ciphertext,
);

console.log(`\
ciphertext:  ${ciphertext.length} bytes, starts ${[...ciphertext.slice(0, 4)].join(' ')}
roundtrip:   ${new TextDecoder().decode(plaintext)}
wrong label: ok=${wrongLabel.ok} (${wrongLabel.ok ? '' : wrongLabel.code})`);

Released under the MIT License.