Files
caddy-proxy-manager/app/(dashboard)/certificates/ca-actions.ts
akanealw 99819b70ff
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
added caddy-proxy-manager for testing
2026-04-21 22:49:08 +00:00

189 lines
7.1 KiB
TypeScript
Executable File

"use server";
import { revalidatePath } from "next/cache";
import { requireAdmin } from "@/src/lib/auth";
import { createCaCertificate, deleteCaCertificate, updateCaCertificate, getCaCertificatePrivateKey } from "@/src/lib/models/ca-certificates";
import { createIssuedClientCertificate, revokeIssuedClientCertificate } from "@/src/lib/models/issued-client-certificates";
import { X509Certificate } from "node:crypto";
import forge from "node-forge";
function validatePem(pem: string): void {
try {
new X509Certificate(pem);
} catch {
throw new Error("Invalid certificate PEM: could not parse as X.509 certificate");
}
}
export async function createCaCertificateAction(formData: FormData) {
const session = await requireAdmin();
const userId = Number(session.user.id);
const name = String(formData.get("name") ?? "").trim();
const certificatePem = String(formData.get("certificate_pem") ?? "").trim();
if (!name) throw new Error("Name is required");
if (!certificatePem) throw new Error("Certificate PEM is required");
validatePem(certificatePem);
await createCaCertificate({ name, certificatePem: certificatePem }, userId);
revalidatePath("/certificates");
}
export async function updateCaCertificateAction(id: number, formData: FormData) {
const session = await requireAdmin();
const userId = Number(session.user.id);
const name = formData.get("name") ? String(formData.get("name")).trim() : undefined;
const certificatePem = formData.get("certificate_pem") ? String(formData.get("certificate_pem")).trim() : undefined;
if (certificatePem) {
validatePem(certificatePem);
}
await updateCaCertificate(id, {
...(name ? { name } : {}),
...(certificatePem ? { certificatePem: certificatePem } : {})
}, userId);
revalidatePath("/certificates");
}
export async function deleteCaCertificateAction(id: number): Promise<{ success: boolean; error?: string }> {
const session = await requireAdmin();
const userId = Number(session.user.id);
try {
await deleteCaCertificate(id, userId);
revalidatePath("/certificates");
return { success: true };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Failed to delete CA certificate" };
}
}
export async function generateCaCertificateAction(formData: FormData): Promise<{ id: number }> {
const session = await requireAdmin();
const userId = Number(session.user.id);
const name = String(formData.get("name") ?? "").trim();
const commonName = String(formData.get("common_name") ?? name).trim() || name;
const validityDays = Math.min(3650, Math.max(1, parseInt(String(formData.get("validity_days") ?? "3650"), 10) || 3650));
if (!name) throw new Error("Name is required");
const keypair = forge.pki.rsa.generateKeyPair({ bits: 4096 });
const cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + validityDays);
const attrs = [
{ name: "commonName", value: commonName },
{ name: "organizationName", value: "Caddy Proxy Manager" },
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([
{ name: "basicConstraints", cA: true, critical: true },
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
{ name: "subjectKeyIdentifier" },
]);
cert.sign(keypair.privateKey, forge.md.sha256.create());
const certificatePem = forge.pki.certificateToPem(cert);
const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey);
const record = await createCaCertificate({ name, certificatePem: certificatePem, privateKeyPem: privateKeyPem }, userId);
revalidatePath("/certificates");
return { id: record.id };
}
export type IssuedClientCert = {
pkcs12Base64: string;
passwordProtected: boolean;
exportAlgorithm: "3des" | "aes256";
};
export async function issueClientCertificateAction(
caCertId: number,
formData: FormData
): Promise<IssuedClientCert> {
const session = await requireAdmin();
const userId = Number(session.user.id);
const commonName = String(formData.get("common_name") ?? "").trim();
const validityDays = Math.min(3650, Math.max(1, parseInt(String(formData.get("validity_days") ?? "365"), 10) || 365));
const exportPassword = String(formData.get("export_password") ?? "");
const compatibilityMode = formData.get("compatibility_mode") === "on";
const exportAlgorithm: IssuedClientCert["exportAlgorithm"] = compatibilityMode ? "3des" : "aes256";
if (!commonName) throw new Error("Common name is required");
if (!exportPassword) throw new Error("Export password is required");
const caPrivateKeyPem = await getCaCertificatePrivateKey(caCertId);
if (!caPrivateKeyPem) throw new Error("This CA has no stored private key — cannot issue client certificates");
const caCertRecord = await import("@/src/lib/models/ca-certificates").then(m => m.getCaCertificate(caCertId));
if (!caCertRecord) throw new Error("CA certificate not found");
const caKey = forge.pki.privateKeyFromPem(caPrivateKeyPem);
const caCert = forge.pki.certificateFromPem(caCertRecord.certificatePem);
const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048 });
const cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = Date.now().toString(16);
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + validityDays);
cert.setSubject([{ name: "commonName", value: commonName }]);
cert.setIssuer(caCert.subject.attributes);
cert.setExtensions([
{ name: "basicConstraints", cA: false },
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
{ name: "extKeyUsage", clientAuth: true },
]);
cert.sign(caKey, forge.md.sha256.create());
const certificatePem = forge.pki.certificateToPem(cert);
const certificate = new X509Certificate(certificatePem);
await createIssuedClientCertificate(
{
caCertificateId: caCertId,
commonName: commonName,
serialNumber: cert.serialNumber.toUpperCase(),
fingerprintSha256: certificate.fingerprint256,
certificatePem: certificatePem,
validFrom: new Date(certificate.validFrom).toISOString(),
validTo: new Date(certificate.validTo).toISOString()
},
userId
);
revalidatePath("/certificates");
const pkcs12Asn1 = forge.pkcs12.toPkcs12Asn1(
keypair.privateKey,
[cert, caCert],
exportPassword,
{
algorithm: exportAlgorithm,
friendlyName: commonName,
}
);
const pkcs12Der = forge.asn1.toDer(pkcs12Asn1).getBytes();
return {
pkcs12Base64: forge.util.encode64(pkcs12Der),
passwordProtected: true,
exportAlgorithm,
};
}
export async function revokeIssuedClientCertificateAction(id: number): Promise<{ revokedAt: string }> {
const session = await requireAdmin();
const userId = Number(session.user.id);
const record = await revokeIssuedClientCertificate(id, userId);
revalidatePath("/certificates");
return { revokedAt: record.revokedAt! };
}