Created
February 26, 2025 12:13
-
-
Save sibelius/936c71ba44920f2db2603f56ab5df236 to your computer and use it in GitHub Desktop.
download cert chain
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment