Skip to content

Instantly share code, notes, and snippets.

@sibelius
Created February 26, 2025 12:13
Show Gist options
  • Save sibelius/936c71ba44920f2db2603f56ab5df236 to your computer and use it in GitHub Desktop.
Save sibelius/936c71ba44920f2db2603f56ab5df236 to your computer and use it in GitHub Desktop.
download cert chain
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