@f-o-t/digital-certificate
v2.4.4
Published
Brazilian A1 digital certificate handling with XML/PDF signing and mTLS support.
Readme
@f-o-t/digital-certificate
Brazilian A1 digital certificate handling with XML/PDF signing and mTLS support.
Features
- Certificate Management: Parse and validate Brazilian A1 digital certificates (.pfx/.p12)
- Brazilian Standards: Extract CNPJ/CPF from certificate fields
- Type Safety: Full TypeScript support with Zod schema validation
- XML Digital Signatures: Sign XML documents with XML-DSig (via plugin)
- Mutual TLS: Create mTLS contexts for secure HTTPS connections (via plugin)
- Pure JavaScript: No system dependencies — PKCS#12 parsing via
@f-o-t/crypto - Browser Compatible: All APIs use
Uint8Array; works in browsers, Edge Runtime, and Cloudflare Workers - Validity Checking: Built-in certificate expiry validation
- Framework Agnostic: Works with any JavaScript/TypeScript project
Installation
# npm
npm install @f-o-t/digital-certificate
# bun
bun add @f-o-t/digital-certificate
# yarn
yarn add @f-o-t/digital-certificate
# pnpm
pnpm add @f-o-t/digital-certificateRequirements:
- For XML signing:
@f-o-t/xml(automatically included)
Quick Start
import { parseCertificate, isCertificateValid, daysUntilExpiry } from "@f-o-t/digital-certificate";
import { readFileSync } from "fs";
// Load certificate file (Node/Bun)
const pfxBuffer = new Uint8Array(readFileSync("certificate.pfx"));
const password = "your-certificate-password";
// In a browser, read from a File input:
// const pfxBuffer = new Uint8Array(await file.arrayBuffer());
// Parse certificate
const cert = await parseCertificate(pfxBuffer, password);
// Check validity
console.log("Valid:", isCertificateValid(cert));
console.log("Days until expiry:", daysUntilExpiry(cert));
// Access certificate information
console.log("Common Name:", cert.subject.commonName);
console.log("Organization:", cert.subject.organization);
console.log("CNPJ:", cert.brazilian.cnpj);
console.log("CPF:", cert.brazilian.cpf);
console.log("Valid from:", cert.validity.notBefore);
console.log("Valid until:", cert.validity.notAfter);
console.log("Fingerprint:", cert.fingerprint);Core API
Certificate Parsing
Parse .pfx/.p12 certificate files:
import { parseCertificate } from "@f-o-t/digital-certificate";
const cert = await parseCertificate(pfxBuffer, password);
// Certificate structure
console.log(cert.serialNumber); // Certificate serial number
console.log(cert.subject); // Subject information
console.log(cert.issuer); // Issuer information
console.log(cert.validity); // Validity period
console.log(cert.fingerprint); // SHA-256 fingerprint
console.log(cert.isValid); // Current validity status
console.log(cert.brazilian); // Brazilian-specific fields
console.log(cert.certPem); // Certificate in PEM format
console.log(cert.keyPem); // Private key in PEM format
console.log(cert.pfxBuffer); // Original PFX buffer
console.log(cert.pfxPassword); // PFX passwordSubject Information
Access subject (certificate holder) details:
const { subject } = cert;
console.log(subject.commonName); // CN - Common Name
console.log(subject.organization); // O - Organization
console.log(subject.organizationalUnit); // OU - Organizational Unit
console.log(subject.country); // C - Country
console.log(subject.state); // ST - State
console.log(subject.locality); // L - Locality
console.log(subject.raw); // Raw DN stringIssuer Information
Access issuer (CA) details:
const { issuer } = cert;
console.log(issuer.commonName); // CN - CA name
console.log(issuer.organization); // O - CA organization
console.log(issuer.country); // C - CA country
console.log(issuer.raw); // Raw DN stringBrazilian-Specific Fields
Extract CNPJ/CPF from certificate:
const { brazilian } = cert;
// Company certificate
if (brazilian.cnpj) {
console.log("CNPJ:", brazilian.cnpj); // e.g., "12345678000190"
}
// Individual certificate
if (brazilian.cpf) {
console.log("CPF:", brazilian.cpf); // e.g., "12345678900"
}Validity Checking
Check certificate validity:
import { isCertificateValid, daysUntilExpiry } from "@f-o-t/digital-certificate";
// Check if currently valid
const isValid = isCertificateValid(cert);
// Get days until expiry (negative if expired)
const days = daysUntilExpiry(cert);
if (days < 0) {
console.log(`Certificate expired ${Math.abs(days)} days ago`);
} else if (days < 30) {
console.log(`Certificate expires in ${days} days - renewal recommended`);
} else {
console.log(`Certificate valid for ${days} more days`);
}
// Access validity dates directly
console.log("Valid from:", cert.validity.notBefore);
console.log("Valid until:", cert.validity.notAfter);PEM Extraction
Get PEM-formatted certificate and key:
import { getPemPair } from "@f-o-t/digital-certificate";
// Extract PEM pair
const { cert: certPem, key: keyPem } = getPemPair(cert);
// Use with custom HTTP clients
import https from "https";
const agent = new https.Agent({
cert: certPem,
key: keyPem
});
// Make authenticated request
https.get("https://api.example.com", { agent }, (res) => {
// Handle response
});Type System
Full TypeScript support:
import type {
CertificateInfo,
CertificateSubject,
CertificateIssuer,
CertificateValidity,
BrazilianFields,
PemPair,
SignatureAlgorithm
} from "@f-o-t/digital-certificate";
// Certificate information
const certInfo: CertificateInfo = {
serialNumber: "1234567890",
subject: {
commonName: "Company Name",
organization: "Company Inc",
organizationalUnit: "IT Department",
country: "BR",
state: "SP",
locality: "São Paulo",
raw: "CN=Company Name,O=Company Inc,..."
},
issuer: {
commonName: "CA Name",
organization: "Certificate Authority",
country: "BR",
raw: "CN=CA Name,O=Certificate Authority,..."
},
validity: {
notBefore: new Date("2024-01-01"),
notAfter: new Date("2025-01-01")
},
fingerprint: "abcdef...",
isValid: true,
brazilian: {
cnpj: "12345678000190",
cpf: null
},
certPem: "-----BEGIN CERTIFICATE-----...",
keyPem: "-----BEGIN PRIVATE KEY-----...",
pfxBuffer: new Uint8Array([]),
pfxPassword: "password"
};Zod Schemas
Validate certificate-related data:
import {
signatureAlgorithmSchema,
signOptionsSchema,
mtlsOptionsSchema
} from "@f-o-t/digital-certificate";
// Validate signature algorithm
const algorithm = signatureAlgorithmSchema.parse("RSA-SHA256");
// Available algorithms: "RSA-SHA1", "RSA-SHA256"Utilities
Low-level utilities for working with certificates:
import {
extractCnpj,
extractCpf,
parseDistinguishedName,
pemToBase64,
base64ToBytes,
bytesToBase64,
BRAZILIAN_OIDS,
DIGEST_ALGORITHMS,
SIGNATURE_ALGORITHMS,
TRANSFORM_ALGORITHMS,
XMLDSIG_NS,
EXC_C14N_NS
} from "@f-o-t/digital-certificate";
// Extract CNPJ from text
const cnpj = extractCnpj("serialNumber=12345678000190");
// "12345678000190"
// Extract CPF from text
const cpf = extractCpf("CN=John Doe:12345678900");
// "12345678900"
// Parse distinguished name
const dn = parseDistinguishedName("CN=Company,O=Inc,C=BR");
// { CN: "Company", O: "Inc", C: "BR" }
// Convert PEM to base64
const base64 = pemToBase64(certPem);
// Cross-platform base64 helpers (work in Node, Bun, browsers, Edge Runtime)
const bytes = base64ToBytes("SGVsbG8gV29ybGQ="); // Uint8Array
const b64 = bytesToBase64(new Uint8Array([1, 2])); // "AQI="
// Brazilian OIDs
console.log(BRAZILIAN_OIDS.CNPJ); // "2.16.76.1.3.3"
console.log(BRAZILIAN_OIDS.CPF); // "2.16.76.1.3.1"
// Algorithm constants
console.log(DIGEST_ALGORITHMS["RSA-SHA256"]); // "http://www.w3.org/2001/04/xmlenc#sha256"
console.log(SIGNATURE_ALGORITHMS["RSA-SHA256"]); // "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"Plugins
XML Digital Signatures
Sign XML documents with XML-DSig:
import { parseCertificate } from "@f-o-t/digital-certificate";
import { signXml } from "@f-o-t/digital-certificate/plugins/xml-signer";
import { parseXml } from "@f-o-t/xml";
// Parse certificate
const cert = await parseCertificate(pfxBuffer, password);
// Parse XML document
const doc = parseXml(`
<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
<infNFe Id="NFe12345678901234567890123456789012345678901234">
<ide>
<cUF>35</cUF>
<nNF>123</nNF>
</ide>
</infNFe>
</NFe>
`);
// Sign XML
const signedXml = signXml(doc, cert, {
algorithm: "RSA-SHA256",
referenceUri: "#NFe12345678901234567890123456789012345678901234",
transforms: ["enveloped-signature", "c14n"]
});
console.log(signedXml); // XML with <Signature> elementFeatures:
- Enveloped signatures (inside the signed document)
- RSA-SHA1 and RSA-SHA256 algorithms
- Exclusive C14N canonicalization
- Reference URI support for partial document signing
- Compatible with Brazilian fiscal XML standards (NF-e, NFS-e, etc.)
Sign options:
import type { SignOptions } from "@f-o-t/digital-certificate";
const options: SignOptions = {
algorithm: "RSA-SHA256", // or "RSA-SHA1"
referenceUri: "#elementId", // URI to signed element
transforms: [ // Optional transforms
"enveloped-signature",
"c14n"
]
};Mutual TLS (mTLS)
Create mTLS contexts for HTTPS connections:
import { parseCertificate } from "@f-o-t/digital-certificate";
import { createMtlsContext, createMtlsAgent } from "@f-o-t/digital-certificate/plugins/mtls";
import https from "https";
// Parse certificate
const cert = await parseCertificate(pfxBuffer, password);
// Create mTLS context
const context = createMtlsContext(cert);
// Returns: { cert: string, key: string, passphrase?: string, pfx?: Buffer }
// Create HTTPS agent
const agent = createMtlsAgent(cert, {
rejectUnauthorized: true, // Verify server certificate
ca: [caCertPem] // Optional CA certificates
});
// Use with https module
https.get("https://api.sefaz.sp.gov.br/ws/nfe", { agent }, (res) => {
let data = "";
res.on("data", (chunk) => data += chunk);
res.on("end", () => console.log(data));
});
// Use with fetch (Node.js 18+)
const response = await fetch("https://api.example.com", {
// @ts-ignore - dispatcher not yet typed
dispatcher: agent
});Create custom agent:
import https from "https";
const agent = new https.Agent({
cert: cert.certPem,
key: cert.keyPem,
rejectUnauthorized: true,
// Additional options
keepAlive: true,
maxSockets: 10
});PDF Signing
Removed in v2.0.0. PDF signing has moved to
@f-o-t/e-signature.
Advanced Usage
Certificate Chain Validation
import { parseCertificate, isCertificateValid } from "@f-o-t/digital-certificate";
// Parse certificate
const cert = await parseCertificate(pfxBuffer, password);
// Basic validation
if (!isCertificateValid(cert)) {
throw new Error("Certificate is expired or not yet valid");
}
// Check expiry threshold
const days = daysUntilExpiry(cert);
if (days < 30) {
console.warn(`Certificate expires soon: ${days} days remaining`);
}
// Verify certificate fields
if (!cert.brazilian.cnpj && !cert.brazilian.cpf) {
throw new Error("Not a Brazilian certificate");
}Working with Multiple Certificates
import { parseCertificate, isCertificateValid } from "@f-o-t/digital-certificate";
// Load and validate multiple certificates
const certificates = [
{ file: "cert1.pfx", password: "pass1" },
{ file: "cert2.pfx", password: "pass2" }
].map(({ file, password }) => {
const buffer = readFileSync(file);
return await parseCertificate(buffer, password);
});
// Find valid certificates
const validCerts = certificates.filter(isCertificateValid);
// Find certificate by CNPJ
function findByCnpj(cnpj: string) {
return validCerts.find(cert => cert.brazilian.cnpj === cnpj);
}
// Find certificate expiring soonest
const expiringFirst = [...validCerts].sort((a, b) =>
daysUntilExpiry(a) - daysUntilExpiry(b)
)[0];Batch XML Signing
import { signXml } from "@f-o-t/digital-certificate/plugins/xml-signer";
// Sign multiple XML documents
const xmlDocuments = [doc1, doc2, doc3];
const signedDocuments = xmlDocuments.map(doc =>
signXml(doc, cert, {
algorithm: "RSA-SHA256",
referenceUri: `#${doc.root?.attributes.find(a => a.name === "Id")?.value}`
})
);Custom HTTPS Client with mTLS
import { createMtlsAgent } from "@f-o-t/digital-certificate/plugins/mtls";
import axios from "axios";
// Create axios instance with mTLS
const client = axios.create({
httpsAgent: createMtlsAgent(cert, {
rejectUnauthorized: true
}),
timeout: 30000
});
// Make authenticated requests
const response = await client.post("https://api.sefaz.sp.gov.br/ws/nfe", xmlData, {
headers: { "Content-Type": "application/xml" }
});Best Practices
1. Secure Certificate Storage
// Don't commit certificates or passwords to version control
// Use environment variables or secure vaults
const pfxPath = process.env.CERTIFICATE_PATH;
const password = process.env.CERTIFICATE_PASSWORD;
if (!pfxPath || !password) {
throw new Error("Certificate configuration missing");
}
const cert = await parseCertificate(
readFileSync(pfxPath),
password
);2. Cache Parsed Certificates
// Parse once, reuse many times
let cachedCert: CertificateInfo | null = null;
async function getCertificate(): Promise<CertificateInfo> {
if (!cachedCert) {
cachedCert = await parseCertificate(pfxBuffer, password);
}
// Verify still valid
if (!isCertificateValid(cachedCert)) {
throw new Error("Certificate expired");
}
return cachedCert;
}3. Monitor Certificate Expiry
import { daysUntilExpiry } from "@f-o-t/digital-certificate";
// Check expiry regularly
function checkCertificateExpiry(cert: CertificateInfo) {
const days = daysUntilExpiry(cert);
if (days < 0) {
throw new Error("Certificate expired");
}
if (days < 30) {
console.warn(`Certificate expires in ${days} days - renewal required`);
// Send alert, email, etc.
}
}
// Run daily
setInterval(() => checkCertificateExpiry(cert), 24 * 60 * 60 * 1000);4. Handle Parsing Errors Gracefully
try {
const cert = await parseCertificate(pfxBuffer, password);
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("password")) {
console.error("Invalid certificate password");
} else if (error.message.includes("PFX")) {
console.error("Invalid PFX file");
} else {
console.error("Certificate parsing failed:", error.message);
}
}
throw error;
}5. Validate Before Signing
import { signXml } from "@f-o-t/digital-certificate/plugins/xml-signer";
import { isCertificateValid } from "@f-o-t/digital-certificate";
function safeSignXml(doc: XmlDocument, cert: CertificateInfo) {
// Validate certificate first
if (!isCertificateValid(cert)) {
throw new Error("Cannot sign with expired certificate");
}
// Check expiry threshold
if (daysUntilExpiry(cert) < 7) {
console.warn("Certificate expires soon - consider renewing");
}
// Proceed with signing
return signXml(doc, cert, {
algorithm: "RSA-SHA256"
});
}Error Handling
try {
const cert = await parseCertificate(pfxBuffer, password);
if (!isCertificateValid(cert)) {
throw new Error(`Certificate expired on ${cert.validity.notAfter}`);
}
// Use certificate...
} catch (error) {
if (error instanceof Error) {
console.error("Certificate error:", error.message);
// Handle specific error cases
if (error.message.includes("wrong password")) {
// Handle authentication error
} else if (error.message.includes("OpenSSL")) {
// Handle OpenSSL errors
}
}
}Performance
Measured on modern hardware with Bun runtime (pure-TS implementation, no OpenSSL):
| Operation | Typical time |
|---|---|
| Parse certificate (parseCertificate) | ~30–40ms |
| Get PEM pair (getPemPair) | < 0.1ms |
| Check validity (isCertificateValid) | < 0.1ms |
| Days until expiry (daysUntilExpiry) | < 0.1ms |
Note: parseCertificate includes PKCS#12 decryption (PBES2/PBKDF2-SHA256), which runs in pure TypeScript. With @f-o-t/crypto ≥ 1.2.0 this is ~3× faster than earlier versions (PBKDF2-SHA256 improved from ~100ms to ~13ms). A WebCrypto/native fallback for PBKDF2 is planned to improve this further.
Best practice: Parse once and cache the result; subsequent operations are sub-millisecond.
let cachedCert: CertificateInfo | null = null;
async function getCertificate(): Promise<CertificateInfo> {
if (!cachedCert) {
cachedCert = await parseCertificate(pfxBuffer, password); // ~30–40ms, once
}
if (!isCertificateValid(cachedCert)) throw new Error("Certificate expired");
return cachedCert;
}Contributing
Contributions are welcome! Please check the repository for guidelines.
License
MIT License - see LICENSE file for details.
