Skip to content

Verification ​

Chain verification ​

ts
import {
  createSelfSignedCertificate,
  createCertificate,
  generateKeyPair,
  verifyCertificateChain,
} from 'micro509';

// Build a root CA
const root = await createSelfSignedCertificate({
  subject: { commonName: 'Demo Root CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

// Intermediate CA signed by the root
const intKeys = await generateKeyPair();
const intermediate = await createCertificate({
  issuer: { commonName: 'Demo Root CA' },
  subject: { commonName: 'Demo Intermediate CA' },
  publicKey: intKeys.publicKey,
  signerPrivateKey: root.keyPair.privateKey,
  issuerPublicKey: root.keyPair.publicKey,
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

// Leaf signed by the intermediate
const leafKeys = await generateKeyPair();
const leaf = await createCertificate({
  issuer: { commonName: 'Demo Intermediate CA' },
  subject: { commonName: 'api.example.com' },
  publicKey: leafKeys.publicKey,
  signerPrivateKey: intKeys.privateKey,
  issuerPublicKey: intKeys.publicKey,
  extensions: {
    extendedKeyUsage: ['serverAuth'],
    subjectAltNames: [
      { type: 'dns', value: 'api.example.com' },
    ],
  },
});

const result = await verifyCertificateChain({
  leaf: leaf.pem,
  intermediates: [intermediate.pem],
  roots: [root.certificate.pem],
  purpose: 'serverAuth',
  serviceIdentity: {
    type: 'dns',
    value: 'api.example.com',
  },
});

if (result.ok) {
  const parsedLeaf = result.value.leaf;
  console.log(`\
Valid chain: ${result.value.chain.length} certificates
leaf:        ${parsedLeaf.subject.values.commonName}
serial:      ${parsedLeaf.serialNumberHex}
expires:     ${parsedLeaf.notAfter.toISOString()}`);
} else {
  console.log(`\
Failed:   ${result.error.code}
At index: ${result.error.index}`);
}

Path building tries every issuer candidate and bare trust anchor whose subject matches, so an input with many same-subject certificates costs more work than its size suggests. maxPathBuildingChecks on verifyCertificateChain, buildCandidatePath and the validateFor* profiles bounds how many candidates and anchors one search may examine, including those it skips because they are already on the path or their names do not match. The default is 100,000. When the bound stops the search before a trusted path is found, the result is path_building_limit_exceeded.

A user notice explicitText or noticeRef organization longer than the 200 characters RFC 5280 §4.2.1.4 allows a DisplayText is kept whole by the parser and reported as oversizedExplicitText on the qualifier or oversizedOrganization on the noticeRef, and the chain validates. §4.2.1.4 asks certificate users to handle an oversized explicitText gracefully, and micro509 applies the same policy to the organization. rejectOversizedDisplayText: true on verifyCertificateChain or validateCandidatePath rejects such a certificate with display_text_oversized instead, and details.userNoticeField names the field. PKITS 4.8.19 leaves that choice for explicitText to the application.

Verification purposes ​

Four built-in validation profiles. serverAuth, clientAuth, and ca are passed as purpose to verifyCertificateChain, or through the equivalent validateForTlsServer, validateForTlsClient, and validateForCa wrappers; code signing has its own validateForCodeSigning profile:

ts
import {
  createSelfSignedCertificate,
  createCertificate,
  generateKeyPair,
  verifyCertificateChain,
  validateForCodeSigning,
} from 'micro509';

// Shared root CA
const root = await createSelfSignedCertificate({
  subject: { commonName: 'Demo Root CA' },
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});

// Issue a leaf with the extended key usage under test
async function issue(cn = '', eku = '') {
  if (
    eku !== 'serverAuth' &&
    eku !== 'clientAuth' &&
    eku !== 'codeSigning'
  ) {
    throw new Error(`Unsupported EKU: ${eku}`);
  }
  const keys = await generateKeyPair();
  return createCertificate({
    issuer: { commonName: 'Demo Root CA' },
    subject: { commonName: cn },
    publicKey: keys.publicKey,
    signerPrivateKey: root.keyPair.privateKey,
    issuerPublicKey: root.keyPair.publicKey,
    extensions: {
      extendedKeyUsage: [eku],
      subjectAltNames: [{ type: 'dns', value: cn }],
    },
  });
}

// TLS server (default)
const server = await issue('srv.example', 'serverAuth');
const r1 = await verifyCertificateChain({
  leaf: server.pem,
  roots: [root.certificate.pem],
  purpose: 'serverAuth',
});

// TLS client
const client = await issue('cli.example', 'clientAuth');
const r2 = await verifyCertificateChain({
  leaf: client.pem,
  roots: [root.certificate.pem],
  purpose: 'clientAuth',
});

// Code signing
const signer = await issue('sign.example', 'codeSigning');
const r3 = await validateForCodeSigning({
  leaf: signer.pem,
  roots: [root.certificate.pem],
});

// CA certificate (verify an intermediate as a CA)
const caKeys = await generateKeyPair();
const intermediate = await createCertificate({
  issuer: { commonName: 'Demo Root CA' },
  subject: { commonName: 'Demo Intermediate CA' },
  publicKey: caKeys.publicKey,
  signerPrivateKey: root.keyPair.privateKey,
  issuerPublicKey: root.keyPair.publicKey,
  extensions: {
    basicConstraints: { ca: true },
    keyUsage: ['keyCertSign', 'cRLSign'],
  },
});
const r4 = await verifyCertificateChain({
  leaf: intermediate.pem,
  roots: [root.certificate.pem],
  purpose: 'ca',
});

console.log(`\
serverAuth:  ${r1.ok ? `ok, serial ${r1.value.leaf.serialNumberHex}` : `${r1.error.code}@${r1.error.index}`}
clientAuth:  ${r2.ok ? `ok, serial ${r2.value.leaf.serialNumberHex}` : `${r2.error.code}@${r2.error.index}`}
codeSigning: ${r3.ok ? `ok, serial ${r3.value.leaf.serialNumberHex}` : `${r3.error.code}@${r3.error.index}`}
ca:          ${r4.ok ? `ok, serial ${r4.value.leaf.serialNumberHex}` : `${r4.error.code}@${r4.error.index}`}`);

Service identity matching ​

ts
import {
  createSelfSignedCertificate,
  parseCertificatePem,
  subjectAltNameToString,
  unwrap,
} from 'micro509';
import { matchServiceIdentity } from 'micro509/verify';

// Create and parse a certificate to match against
const { certificate } = await createSelfSignedCertificate({
  subject: { commonName: 'example.com' },
  extensions: {
    subjectAltNames: [
      { type: 'dns', value: 'example.com' },
    ],
  },
});

const parsed = unwrap(parseCertificatePem(certificate.pem));

const result = matchServiceIdentity({
  certificate: parsed,
  serviceIdentity: { type: 'dns', value: 'example.com' },
});

const sans = (parsed.subjectAltNames ?? [])
  .map((name) =>
    subjectAltNameToString(name, { prefix: true }),
  )
  .join(', ');

if (result.ok) {
  console.log(`\
matched: dns example.com
SANs:    ${sans}
serial:  ${parsed.serialNumberHex}`);
} else {
  console.log(result.error.code);
  // 'subject_alt_name_mismatch' | ...
}

Supported identity types:

  • DNS-ID — with wildcard matching and case-insensitive comparison
  • IP-ID — with IPv6 normalization
  • URI-ID — scheme + host matching, with wildcard matching except for SIP and IP hosts compared by octets
  • SRV-ID — service name matching via otherName SAN, with wildcard matching

Error codes ​

Stability

Error-code unions (VerifyErrorCode and every other *ErrorCode / *ReasonCode union) may gain new members in minor releases as functionality grows. Treat them as non-exhaustive: keep a default branch in switch statements over codes. Renaming or removing a code is a breaking change and only happens in a major release.

Every failure mode in the VerifyErrorCode type (the runtime list is exported as VERIFY_ERROR_CODES; this table is checked against it by a repo test). Every other error-code union in the library is tabled in the error-code reference, under the same enforcement:

CodeMeaning
no_trusted_rootNo trust anchor matched the chain
issuer_not_foundCould not find issuer for a certificate
signature_invalidCryptographic signature check failed
certificate_expiredCertificate outside validity window
ca_requiredNon-CA certificate used as issuer
key_cert_sign_requiredIssuer missing keyCertSign key usage
path_length_exceededChain exceeds pathLenConstraint
path_building_limit_exceededPath building exceeded bounded work limits
authority_key_identifier_mismatchAKI/SKI cross-check failed
extended_key_usage_invalidEKU doesn't match requested purpose
subject_alt_name_mismatchSAN doesn't match service identity
common_name_fallback_suppressedCN match suppressed by presented identifiers
self_signed_leaf_not_allowedSelf-signed leaf without explicit opt-in
unrecognized_critical_extensionUnknown critical extension
no_rev_avail_conflictnoRevAvail with cA or a revocation pointer
display_text_oversizedNotice text over 200 chars, opt-in rejection
intermediate_eku_constraintIntermediate has restrictive EKU
explicit_policy_requiredPolicy required but not satisfied
initial_policy_set_not_satisfiedInitial policy set not met
unsupported_initial_name_constraintsInitial name constraints use unsupported forms
unsupported_name_constraintsUnsupported name constraint form
name_constraints_violatedName constraints check failed
unsupported_signature_algorithm_parametersUnknown signature algorithm
ec_domain_parameters_missingEC public key without a named curve
certificate_revokedRevocation evidence confirms revocation
revocation_indeterminateRevocation unknown under hard-fail policy
unsupportedCertificate input micro509 does not decode
limit_exceededCertificate input exceeds a decoding limit

CSR verification ​

ts
import {
  createCertificateSigningRequest,
  generateKeyPair,
  subjectAltNameToString,
  verifyCertificateSigningRequest,
} from 'micro509';

// Build a CSR to verify
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' },
    ],
  },
});

const result = await verifyCertificateSigningRequest(
  csr.pem,
);

if (result.ok) {
  const sans = (result.value.subjectAltNames ?? [])
    .map((name) => subjectAltNameToString(name))
    .join(', ');
  const body = csr.pem
    .trimEnd()
    .split('\n')
    .slice(1, -1)
    .join('');
  console.log(`\
subject:   ${result.value.subject.values.commonName}
sig algo:  ${result.value.signatureAlgorithmName}
SANs:      ${sans}
signature: …${body.slice(-44)}`);
} else {
  console.log('CSR invalid:', result.error.code);
}

Detached signatures ​

micro509/crypto signs and verifies raw bytes when there is no X.509 or CMS structure around the signature: a timestamp token, a code-signing blob, a hand-built TBS. signData infers the algorithm from the key and returns the signature beside its AlgorithmIdentifier material; verifySignature checks it against the signer's SubjectPublicKeyInfo, retrying the alternate DER/raw ECDSA encoding when needed.

ts
import {
  ecdsaSignatureDerToRaw,
  ecdsaSignatureRawToDer,
  signData,
  verifySignature,
} from 'micro509/crypto';
import {
  createSelfSignedCertificate,
  parseCertificatePem,
  unwrap,
} from 'micro509';

const { certificate, keyPair } =
  await createSelfSignedCertificate({
    subject: { commonName: 'signer.example' },
    algorithm: { kind: 'ecdsa', curve: 'P-256' },
  });
const parsed = unwrap(parseCertificatePem(certificate.pem));

const data = new TextEncoder().encode('release-v1.tar.gz');
const signed = await signData(keyPair.privateKey, data);

const result = await verifySignature({
  signerSpkiDer: parsed.subjectPublicKeyInfoDer,
  signatureAlgorithm: { oid: signed.algorithmOid },
  publicKeyAlgorithm: {
    oid: parsed.publicKeyAlgorithmOid,
    parametersOid: parsed.publicKeyParametersOid,
  },
  signature: signed.signature,
  data,
});

const tampered = await verifySignature({
  signerSpkiDer: parsed.subjectPublicKeyInfoDer,
  signatureAlgorithm: { oid: signed.algorithmOid },
  publicKeyAlgorithm: {
    oid: parsed.publicKeyAlgorithmOid,
    parametersOid: parsed.publicKeyParametersOid,
  },
  signature: signed.signature,
  data: new TextEncoder().encode('release-v2.tar.gz'),
});

// ECDSA signatures convert between the DER encoding
// X.509/CMS embed and the raw r||s form WebCrypto and
// JOSE use, bridging an HSM or JWS signature into a
// PKIX structure, or the other way around.
const raw = ecdsaSignatureDerToRaw(
  signed.signature,
  'P-256',
);
const der = ecdsaSignatureRawToDer(raw, 'P-256');

const rawHex = Array.from(raw, (byte) =>
  byte.toString(16).padStart(2, '0'),
).join('');
console.log(`\
algorithm: ${signed.algorithmOid}
valid:     ${result.ok && result.valid}
tampered:  ${tampered.ok && tampered.valid}
raw r||s:  ${raw.length} bytes, ${rawHex.slice(0, 48)}…
DER again: ${der.length} bytes`);

Released under the MIT License.