Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ function derToPem(der: Buffer, label: string): string {
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
}

// --- Exported for testing ---

export { encodeInteger as _encodeInteger, expandIpv6 as _expandIpv6 };

// --- Public API ---

/**
Expand Down Expand Up @@ -347,9 +351,19 @@ export function generateCertificate(
notAfter.setDate(notAfter.getDate() + validityDays);

// Generate random serial number (16 bytes, positive)
const serialNumber = crypto.randomBytes(16);
const serialBytes = crypto.randomBytes(16);
// Ensure positive by clearing the high bit
serialNumber[0] &= 0x7f;
serialBytes[0] &= 0x7f;
// Strip leading zero bytes to produce minimal DER encoding,
// but keep at least one byte
let start = 0;
/* v8 ignore start -- depends on random data producing leading zeros */
while (start < serialBytes.length - 1 && serialBytes[start] === 0) {
start++;
}
/* v8 ignore stop */

const serialNumber = serialBytes.subarray(start);

// Build TBS certificate
const tbsCertificate = buildTbsCertificate(
Expand Down
58 changes: 58 additions & 0 deletions test/certificate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import * as path from "node:path";
import * as tls from "node:tls";
import { afterEach, describe, expect, test } from "vitest";
import {
_encodeInteger,
_expandIpv6,
generateCertificate,
generateCertificateFiles,
} from "../src/certificate.js";
Expand Down Expand Up @@ -137,6 +139,62 @@ describe("generateCertificate", () => {
});
});

describe("encodeInteger", () => {
test("should encode zero as a single zero byte", () => {
const result = _encodeInteger(0);
// DER INTEGER tag=0x02, length=1, value=0x00
expect(result).toEqual(Buffer.from([0x02, 0x01, 0x00]));
});

test("should add leading zero when high bit is set on number", () => {
// 128 = 0x80, high bit is set so a leading 0x00 must be prepended
const result = _encodeInteger(128);
// DER INTEGER tag=0x02, length=2, value=0x00 0x80
expect(result).toEqual(Buffer.from([0x02, 0x02, 0x00, 0x80]));
});

test("should add leading zero when high bit is set on Buffer", () => {
const buf = Buffer.from([0x80, 0x01]);
const result = _encodeInteger(buf);
// DER INTEGER tag=0x02, length=3, value=0x00 0x80 0x01
expect(result).toEqual(Buffer.from([0x02, 0x03, 0x00, 0x80, 0x01]));
});
});

describe("expandIpv6", () => {
test("should pad groups in a fully-expanded IPv6 address", () => {
const result = _expandIpv6("2001:db8:0:0:0:0:0:1");
expect(result).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
});

test("should expand :: with empty left side", () => {
const result = _expandIpv6("::1");
expect(result).toBe("0000:0000:0000:0000:0000:0000:0000:0001");
});

test("should expand :: with empty right side", () => {
const result = _expandIpv6("fe80::");
expect(result).toBe("fe80:0000:0000:0000:0000:0000:0000:0000");
});

test("should expand :: in the middle of an address", () => {
const result = _expandIpv6("2001:db8::1");
expect(result).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
});
});

describe("generateCertificate with fully-expanded IPv6", () => {
test("should handle a fully-expanded IPv6 altName without ::", () => {
const result = generateCertificate({
altNames: [
{ type: "ip", value: "2001:0db8:0000:0000:0000:0000:0000:0001" },
],
});
const x509 = new crypto.X509Certificate(result.cert);
expect(x509.subjectAltName).toContain("IP Address:2001:DB8");
});
});

describe("generateCertificateFiles", () => {
let tmpDir: string;

Expand Down
Loading