1import * as ACME from "@/ACME";
2import { describe, expect } from "alchemy-test";
3import * as Test from "@/Test/Alchemy";
4import * as Stream from "effect/Stream";
5import * as ChildProcess from "effect/process/ChildProcess";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import { ROOT_CERTIFICATE } from "./fixtures/root-certificate.ts";
9
10const { test } = Test.make({ providers: ACME.providers() });
11
12/** `openssl req -text -verify` over a DER request. */
13const inspectCsr = (der: Uint8Array) =>
14 Effect.gen(function* () {
15 const process = yield* ChildProcess.make(
16 "openssl",
17 ["req", "-inform", "DER", "-noout", "-text", "-verify"],
18 {
19 stdin: Stream.make(der),
20 stdout: "pipe",
21 stderr: "pipe",
22 },
23 );
24 const [stdout, stderr, code] = yield* Effect.all(
25 [
26 process.stdout.pipe(Stream.decodeText(), Stream.runCollect),
27 process.stderr.pipe(Stream.decodeText(), Stream.runCollect),
28 process.exitCode,
29 ],
30 { concurrency: "unbounded" },
31 );
32 expect(code).toBe(0);
33 return [...stdout, ...stderr].join("");
34 });
35
36describe("ACME PKI", { tags: ["provider:acme", "local"] }, () => {
37 for (const algorithm of ["ES256", "RS256"] as const) {
38 test(
39 `${algorithm} CSR verifies and carries every identifier as a SAN`,
40 Effect.gen(function* () {
41 const key = yield* ACME.generateKey(algorithm);
42 expect(Redacted.value(key.privateKeyPem)).toContain(
43 "BEGIN PRIVATE KEY",
44 );
45 const csr = yield* ACME.createCsr({
46 key,
47 identifiers: ["*.example.test", "example.test"],
48 });
49 const text = yield* inspectCsr(csr);
50 expect(text).toMatch(/verify OK/i);
51 expect(text).toContain("CN=*.example.test");
52 expect(text).toContain("DNS:*.example.test");
53 expect(text).toContain("DNS:example.test");
54 expect(text).toContain(
55 algorithm === "ES256"
56 ? "ecdsa-with-SHA256"
57 : "sha256WithRSAEncryption",
58 );
59 }),
60 );
61 }
62
63 test(
64 "parses a certificate's serial, validity, issuer and SANs",
65 Effect.gen(function* () {
66 const parsed = yield* ACME.parseCertificate(ROOT_CERTIFICATE);
67 expect(parsed.issuer).toContain("CN=minica root ca");
68 expect(parsed.subject).toBe(parsed.issuer);
69 expect(parsed.serial).toMatch(/^[0-9a-f]+$/);
70 expect(parsed.notBefore.getUTCFullYear()).toBe(2025);
71 expect(parsed.notAfter.getUTCFullYear()).toBe(2125);
72 expect(parsed.dnsNames).toEqual([]);
73 }),
74 { tags: ["unit"] },
75 );
76
77 test(
78 "round-trips a PKCS#8 key through import and JWK export",
79 Effect.gen(function* () {
80 const key = yield* ACME.generateKey("ES256");
81 const imported = yield* ACME.importPrivateKey(key.privateKeyPem);
82 expect(imported.algorithm).toBe("ES256");
83 const jwk = JSON.parse(
84 Redacted.value(yield* ACME.privateKeyToJwk(key.privateKeyPem)),
85 ) as { kty: string; crv: string; d?: string };
86 expect(jwk.kty).toBe("EC");
87 expect(jwk.crv).toBe("P-256");
88 expect(jwk.d).toBeDefined();
89 }),
90 { tags: ["unit"] },
91 );
92});