← All tasks
javascriptcodex/javascript-t1 #16Not a task: already works

X.509 Certificate Parser (javascript, written by Codex)

envgap__codex__javascript-t1-16

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #16 · read the task the agent was given
Codex wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: X.509 Certificate Parser

Write a program that parses X.509 digital certificates in PEM and DER formats, extracts all fields, validates the certificate chain, and checks expiration status.

FUNCTIONAL REQUIREMENTS:
- Accept a certificate file path as a command-line argument (support both PEM and DER formats, auto-detected)
- Extract and display all certificate fields: version, serial number, issuer, subject, validity period (not before/not after), public key algorithm and size, signature algorithm, and fingerprints (SHA-1, SHA-256)
- Parse all X.509 v3 extensions: Subject Alternative Names (SANs), Key Usage, Extended Key Usage, Basic Constraints, Authority/Subject Key Identifiers, CRL Distribution Points
- Check certificate expiration: report if expired, days until expiration, or days since expiration
- Validate a certificate chain when multiple certificates are provided: verify that each certificate is signed by the next one in the chain
- Support reading certificate bundles (multiple PEM certificates concatenated in one file) and parsing each individually
- Support a --format flag to choose output format: text (default human-readable), json, or csv
- Support fetching and parsing a remote server's certificate via --host flag (given a hostname and optional port)
- Print the parsed certificate details to console in a structured, readable format
- Save the output to a file via --output flag
- If no input is given, generate a self-signed CA certificate and a leaf certificate signed by it, then parse both and demonstrate chain validation
- Handle errors: invalid certificate data, unsupported formats, incomplete chains, and encoding issues

Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "x509-certificate-parser",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "x509-certificate-parser",
      "version": "1.0.0",
      "dependencies": {
        "node-forge": "1.3.1"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/node-forge": {
      "version": "1.3.1"
    }
  }
}
package.json
{
  "name": "x509-certificate-parser",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {
    "node-forge": "1.3.1"
  }
}

README.md
# X.509 Certificate Parser (JavaScript)

Parses X.509 certificates (PEM/DER/bundles/remote host), extracts fields/extensions, checks expiration, and validates chain signatures.

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies

- Direct:
  - `node-forge@1.3.1`
- Transitive:
  - none

Pinned in `package.json` and `package-lock.json`.

## Setup

```bash
npm install
```

## Run

```bash
node src/index.js ./cert.pem
node src/index.js ./bundle.pem --format json
node src/index.js ./cert.der --format csv --output ./report.csv
node src/index.js --host example.com:443 --format text
node src/index.js
```

## Features

- PEM and DER auto-detection
- PEM bundle parsing (multiple concatenated certs)
- Full field extraction:
  - version, serial
  - issuer, subject
  - validity period
  - public key algorithm/size
  - signature algorithm
  - SHA-1/SHA-256 fingerprints
- X.509 v3 extension extraction:
  - SAN
  - Key Usage
  - Extended Key Usage
  - Basic Constraints
  - SKID/AKID
  - CRL Distribution Points
- Expiration status and day counts
- Chain signature validation (`cert[i]` signed by `cert[i+1]`)
- Remote fetch with `--host`
- Output formats: `text`, `json`, `csv`
- Output file support (`--output`)
- No-args demo generates CA + leaf and validates chain

src/index.js
import crypto from "crypto";
import fs from "fs";
import path from "path";
import tls from "tls";
import forge from "node-forge";

function parseArgs(argv) {
  const options = {};
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const t = argv[i];
    if (t.startsWith("--")) {
      const k = t.slice(2);
      const n = argv[i + 1];
      if (n && !n.startsWith("--")) {
        options[k] = n;
        i += 1;
      } else options[k] = true;
    } else positional.push(t);
  }
  return { options, positional };
}

function pemBlocks(text) {
  const blocks = text.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
  return blocks || [];
}

function bufferToForgeCert(buffer) {
  const asn1 = forge.asn1.fromDer(forge.util.createBuffer(buffer.toString("binary")));
  return forge.pki.certificateFromAsn1(asn1);
}

function certToDerBuffer(cert) {
  const asn1 = forge.pki.certificateToAsn1(cert);
  const der = forge.asn1.toDer(asn1).getBytes();
  return Buffer.from(der, "binary");
}

function parseCertFile(filePath) {
  const raw = fs.readFileSync(filePath);
  const text = raw.toString("utf8");
  if (text.includes("BEGIN CERTIFICATE")) {
    const blocks = pemBlocks(text);
    if (blocks.length === 0) throw new Error("No PEM certificate blocks found.");
    return blocks.map((b) => forge.pki.certificateFromPem(b));
  }
  return [bufferToForgeCert(raw)];
}

function parseDistinguishedName(attrs) {
  return attrs.map((a) => `${a.shortName || a.name}=${a.value}`).join(", ");
}

function pubKeyInfo(pub) {
  if (pub?.n?.bitLength) return { algorithm: "RSA", size: pub.n.bitLength() };
  if (pub?.curve?.length) return { algorithm: "EC", size: pub.curve.length };
  return { algorithm: "Unknown", size: null };
}

function extensionValue(cert, name) {
  return cert.extensions.find((e) => e.name === name || e.id === name) || null;
}

function parseExtensions(cert) {
  const san = extensionValue(cert, "subjectAltName");
  const ku = extensionValue(cert, "keyUsage");
  const eku = extensionValue(cert, "extKeyUsage");
  const bc = extensionValue(cert, "basicConstraints");
  const skid = extensionValue(cert, "subjectKeyIdentifier");
  const akid = extensionValue(cert, "authorityKeyIdentifier");
  const crl = extensionValue(cert, "cRLDistributionPoints");
  return {
    subject_alt_names: (san?.altNames || []).map((x) => x.value || x.ip || x.dns || JSON.stringify(x)),
    key_usage: ku ? Object.fromEntries(Object.entries(ku).filter(([k, v]) => typeof v === "boolean" && v)) : {},
    extended_key_usage: eku ? Object.fromEntries(Object.entries(eku).filter(([k, v]) => typeof v === "boolean" && v)) : {},
    basic_constraints: bc ? { ca: Boolean(bc.cA), path_length: bc.pathLenConstraint ?? null } : {},
    subject_key_identifier: skid?.subjectKeyIdentifier || null,
    authority_key_identifier: akid?.keyIdentifier || null,
    crl_distribution_points: (crl?.altNames || []).map((x) => x.value || x.uri || JSON.stringify(x)),
  };
}

function expirationStatus(notAfter) {
  const now = Date.now();
  const deltaDays = Math.floor((notAfter.getTime() - now) / (1000 * 60 * 60 * 24));
  if (deltaDays >= 0) return { expired: false, days_until_expiration: deltaDays, days_since_expiration: 0 };
  return { expired: true, days_until_expiration: 0, days_since_expiration: Math.abs(deltaDays) };
}

function analyzeCert(cert) {
  const der = certToDerBuffer(cert);
  const sha1 = crypto.createHash("sha1").update(der).digest("hex");
  const sha256 = crypto.createHash("sha256").update(der).digest("hex");
  const pk = pubKeyInfo(cert.publicKey);
  const sigOid = cert.siginfo?.algorithmOid || cert.signatureOid || "";
  const sigAlg = forge.pki.oids[sigOid] || sigOid || "unknown";
  return {
    version: cert.version + 1,
    serial_number: cert.serialNumber,
    issuer: parseDistinguishedName(cert.issuer.attributes),
    subject: parseDistinguishedName(cert.subject.attributes),
    validity: {
      not_before: cert.validity.notBefore.toISOString(),
      not_after: cert.validity.notAfter.toISOString(),
    },
    expiration: expirationStatus(cert.validity.notAfter),
    public_key: pk,
    signature_algorithm: sigAlg,
    fingerprints: { sha1, sha256 },
    extensions: parseExtensions(cert),
  };
}

function validateChain(certs) {
  const steps = [];
  let ok = true;
  for (let i = 0; i < certs.length - 1; i += 1) {
    let valid = false;
    try {
      valid = certs[i + 1].verify(certs[i]);
    } catch {
      valid = false;
    }
    steps.push({
      index: i,
      child_subject: parseDistinguishedName(certs[i].subject.attributes),
      issuer_subject: parseDistinguishedName(certs[i + 1].subject.attributes),
      valid_signature: valid,
    });
    if (!valid) ok = false;
  }
  return { valid: ok, steps };
}

function renderText(result) {
  const lines = [];
  for (let i = 0; i < result.certificates.length; i += 1) {
    const c = result.certificates[i];
    lines.push(`Certificate #${i + 1}`);
    lines.push(`  Subject: ${c.subject}`);
    lines.push(`  Issuer : ${c.issuer}`);
    lines.push(`  Version: ${c.version}`);
    lines.push(`  Serial : ${c.serial_number}`);
    lines.push(`  Validity: ${c.validity.not_before} -> ${c.validity.not_after}`);
    lines.push(`  Signature Algorithm: ${c.signature_algorithm}`);
    lines.push(`  Public Key: ${c.public_key.algorithm} ${c.public_key.size || ""}`.trim());
    lines.push(`  Fingerprint SHA-1  : ${c.fingerprints.sha1}`);
    lines.push(`  Fingerprint SHA-256: ${c.fingerprints.sha256}`);
    lines.push(`  Expired: ${c.expiration.expired} | Days until: ${c.expiration.days_until_expiration} | Days since: ${c.expiration.days_since_expiration}`);
    lines.push(`  SANs: ${(c.extensions.subject_alt_names || []).join("; ")}`);
    lines.push(`  Key Usage: ${Object.keys(c.extensions.key_usage || {}).join(", ")}`);
    lines.push(`  Extended Key Usage: ${Object.keys(c.extensions.extended_key_usage || {}).join(", ")}`);
    lines.push(`  Basic Constraints: CA=${c.extensions.basic_constraints.ca ?? ""} pathLen=${c.extensions.basic_constraints.path_length ?? ""}`);
    lines.push(`  Subject Key ID: ${c.extensions.subject_key_identifier || ""}`);
    lines.push(`  Authority Key ID: ${c.extensions.authority_key_identifier || ""}`);
    lines.push(`  CRL Distribution Points: ${(c.extensions.crl_distribution_points || []).join("; ")}`);
    lines.push("");
  }
  lines.push(`Chain validation: ${result.chain_validation.valid}`);
  for (const step of result.chain_validation.steps) {
    lines.push(`  [${step.index}] ${step.valid_signature ? "OK" : "FAIL"} :: ${step.child_subject} <- ${step.issuer_subject}`);
  }
  return `${lines.join("\n")}\n`;
}

function renderCsv(result) {
  const header = [
    "index", "subject", "issuer", "serial_number", "not_before", "not_after", "expired",
    "days_until_expiration", "days_since_expiration", "public_key_algorithm", "public_key_size",
    "signature_algorithm", "fingerprint_sha1", "fingerprint_sha256",
  ];
  const rows = [header.join(",")];
  for (let i = 0; i < result.certificates.length; i += 1) {
    const c = result.certificates[i];
    const row = [
      i + 1,
      JSON.stringify(c.subject),
      JSON.stringify(c.issuer),
      c.serial_number,
      c.validity.not_before,
      c.validity.not_after,
      c.expiration.expired,
      c.expiration.days_until_expiration,
      c.expiration.days_since_expiration,
      c.public_key.algorithm,
      c.public_key.size ?? "",
      c.signature_algorithm,
      c.fingerprints.sha1,
      c.fingerprints.sha256,
    ];
    rows.push(row.join(","));
  }
  return `${rows.join("\n")}\n`;
}

function outputResult(result, format) {
  const fmt = (format || "text").toLowerCase();
  if (fmt === "json") return `${JSON.stringify(result, null, 2)}\n`;
  if (fmt === "csv") return renderCsv(result);
  return renderText(result);
}

async function fetchHostCerts(host, port) {
  return new Promise((resolve, reject) => {
    const socket = tls.connect({
      host,
      port,
      servername: host,
      rejectUnauthorized: false,
    }, () => {
      try {
        const rawChain = [];
        const seen = new Set();
        let cert = socket.getPeerCertificate(true);
        while (cert && cert.raw && cert.raw.length > 0) {
          const fp = cert.fingerprint256 || cert.fingerprint || `${cert.subject?.CN}:${cert.serialNumber}`;
          if (seen.has(fp)) break;
          seen.add(fp);
          rawChain.push(cert.raw);
          if (!cert.issuerCertificate || cert.issuerCertificate === cert) break;
          cert = cert.issuerCertificate;
        }
        socket.end();
        resolve(rawChain.map((b) => bufferToForgeCert(Buffer.from(b))));
      } catch (e) {
        reject(e);
      }
    });
    socket.on("error", reject);
  });
}

function generateDemoCerts() {
  const pki = forge.pki;
  const caKeys = pki.rsa.generateKeyPair(2048);
  const ca = pki.createCertificate();
  ca.publicKey = caKeys.publicKey;
  ca.serialNumber = "1001";
  ca.validity.notBefore = new Date(Date.now() - 60000);
  ca.validity.notAfter = new Date(Date.now() + 365 * 24 * 3600 * 1000);
  ca.setSubject([{ name: "commonName", value: "Demo Root CA" }, { name: "organizationName", value: "TMLR Demo" }]);
  ca.setIssuer(ca.subject.attributes);
  ca.setExtensions([
    { name: "basicConstraints", cA: true, pathLenConstraint: 1 },
    { name: "keyUsage", keyCertSign: true, cRLSign: true },
    { name: "subjectKeyIdentifier" },
  ]);
  ca.sign(caKeys.privateKey, forge.md.sha256.create());

  const leafKeys = pki.rsa.generateKeyPair(2048);
  const leaf = pki.createCertificate();
  leaf.publicKey = leafKeys.publicKey;
  leaf.serialNumber = "1002";
  leaf.validity.notBefore = new Date(Date.now() - 60000);
  leaf.validity.notAfter = new Date(Date.now() + 180 * 24 * 3600 * 1000);
  leaf.setSubject([{ name: "commonName", value: "demo.local" }, { name: "organizationName", value: "TMLR Demo" }]);
  leaf.setIssuer(ca.subject.attributes);
  leaf.setExtensions([
    { name: "basicConstraints", cA: false },
    { name: "keyUsage", digitalSignature: true, keyEncipherment: true },
    { name: "extKeyUsage", serverAuth: true, clientAuth: true },
    { name: "subjectAltName", altNames: [{ type: 2, value: "demo.local" }, { type: 2, value: "www.demo.local" }] },
    { name: "authorityKeyIdentifier", keyIdentifier: true, authorityCertIssuer: true, serialNumber: ca.serialNumber },
    { name: "subjectKeyIdentifier" },
    { name: "cRLDistributionPoints", altNames: [{ type: 6, value: "http://example.com/demo.crl" }] },
  ]);
  leaf.sign(caKeys.privateKey, forge.md.sha256.create());
  return [leaf, ca];
}

async function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  let certs;
  if (options.host) {
    const [hostOnly, portRaw] = String(options.host).split(":");
    const port = Number.parseInt(options.port || portRaw || "443", 10);
    certs = await fetchHostCerts(hostOnly, port);
  } else if (positional.length > 0) {
    certs = parseCertFile(path.resolve(positional[0]));
  } else {
    certs = generateDemoCerts();
  }

  const result = {
    source: options.host ? `host:${options.host}` : (positional[0] || "demo-generated"),
    certificate_count: certs.length,
    certificates: certs.map(analyzeCert),
    chain_validation: validateChain(certs),
  };

  const rendered = outputResult(result, options.format || "text");
  if (options.output) {
    const outPath = path.resolve(options.output);
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    fs.writeFileSync(outPath, rendered, "utf8");
  }
  process.stdout.write(rendered);
}

main().catch((e) => {
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
  process.exit(1);
});