import fs from "fs"; import path from "path"; import forge from "node-forge"; const cwd = process.cwd(); function computeFingerprint(cert: forge.pki.Certificate): string { const asn1Cert = forge.pki.certificateToAsn1(cert); const der = forge.asn1.toDer(asn1Cert).getBytes(); const md = forge.md.sha1.create(); md.update(der); return md.digest().toHex(); } // Save the certificate as a PEM file (named by its fingerprint) function saveCertificate(count: number, certName: string, cert: forge.pki.Certificate, outputDir: string): string { const pem = forge.pki.certificateToPem(cert); const filePath = path.join(outputDir, `${count}-${certName}.pem`); if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, pem); console.log(`Saved certificate: ${filePath}`); } return filePath; } /** * Decodes the DER-encoded AIA extension value and extracts any URLs from the * accessLocation fields (tagged as [6] for uniformResourceIdentifier). * * @param extensionValue - The raw value from the AIA extension (DER encoded). * @returns An array of URL strings. */ const parseAIAExtension = (extensionValue: string): string[] => { const urls: string[] = []; // Create a binary buffer from the extension value const buffer = forge.util.createBuffer(extensionValue, "binary"); const asn1Obj = forge.asn1.fromDer(buffer); // Validate that we have a SEQUENCE if (asn1Obj.tagClass !== forge.asn1.Class.UNIVERSAL || asn1Obj.type !== forge.asn1.Type.SEQUENCE) { throw new Error("Invalid AIA extension structure; expected a SEQUENCE."); } // Each element in the sequence should be an AccessDescription sequence for (const accessDesc of asn1Obj.value) { if ( accessDesc.tagClass === forge.asn1.Class.UNIVERSAL && accessDesc.type === forge.asn1.Type.SEQUENCE && Array.isArray(accessDesc.value) && accessDesc.value.length >= 2 ) { // The second element is the accessLocation (GeneralName) const accessLocation = accessDesc.value[1]; // Look for context-specific tag 6 (uniformResourceIdentifier) if (accessLocation.tagClass === forge.asn1.Class.CONTEXT_SPECIFIC && accessLocation.type === 6) { // Depending on parsing, accessLocation.value might be a string // or an array containing an IA5String. if (typeof accessLocation.value === "string") { urls.push(accessLocation.value); } else if ( Array.isArray(accessLocation.value) && accessLocation.value.length > 0 && typeof accessLocation.value[0].value === "string" ) { urls.push(accessLocation.value[0].value); } } } } return urls; }; // Extract AIA URLs (specifically CA Issuers URLs) from the certificate extensions const extractAIAUrls = (cert: forge.pki.Certificate): string[] => { // Look for the authorityInfoAccess extension const aiaExt = cert.extensions.find((ext) => ext.name === "authorityInfoAccess"); if (!aiaExt || !aiaExt.value) { console.warn("No Authority Info Access extension found in the certificate."); return []; } try { const urls = parseAIAExtension(aiaExt.value); return urls; } catch (error) { console.error("Error parsing AIA extension:", error); return []; } }; /** * Downloads a certificate from the given URL using fetch. * If the URL suggests DER format (via extension), the certificate is converted accordingly. */ async function fetchCertificate(url: string): Promise<forge.pki.Certificate | null> { try { console.log(`Downloading certificate from: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); let cert: forge.pki.Certificate; // If the URL ends with common DER extensions, assume DER encoding if (url.endsWith(".crt") || url.endsWith(".cer") || url.endsWith(".der")) { const derBuffer = forge.util.createBuffer(buffer.toString("binary")); const asn1Obj = forge.asn1.fromDer(derBuffer); cert = forge.pki.certificateFromAsn1(asn1Obj); } else { // Otherwise, assume the certificate is in PEM format const pem = buffer.toString("utf8"); cert = forge.pki.certificateFromPem(pem); } return cert; } catch (error) { console.error(`Failed to download certificate from ${url}:`, error); return null; } } async function processCertificateRecursively( certName: string, cert: forge.pki.Certificate, outputDir: string, visited: Set<string>, count: number, ): Promise<void> { const fingerprint = computeFingerprint(cert); if (visited.has(fingerprint)) { console.log(`Already processed certificate ${fingerprint}`); return; } visited.add(fingerprint); // Save the current certificate saveCertificate(count, certName, cert, outputDir); // Extract URLs from the AIA extension const aiaUrls = extractAIAUrls(cert); for (const url of aiaUrls) { console.log({ url }); // Process only HTTP/HTTPS URLs if (!url.startsWith("http")) { continue; } if (!url.includes('.crt')) { continue; } const currentCertName = url.split('/').pop(); const downloadedCert = await fetchCertificate(url); if (downloadedCert) { // Recursively process the downloaded certificate await processCertificateRecursively(currentCertName, downloadedCert, outputDir, visited, count+1); } } } const run = async () => { const certName = 'inicial-cert.crt'; const certPath = path.join(cwd, "inicial-cert.crt"); const outputDir = path.join(cwd, "cert-chain"); const pem = fs.readFileSync(certPath, "utf8"); const cert = forge.pki.certificateFromPem(pem); const visited = new Set<string>(); const count = 1; if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } await processCertificateRecursively(certName, cert, outputDir, visited, count); } (async () => { await run(); })();