import { X509Certificate, createPrivateKey } from "node:crypto"; import { ensureDb } from "@/lib/db"; import { encryptText } from "@/lib/crypto"; export class ProvisionError extends Error {} export type ManagedCertificate = { id: number; name: string; certificate_pem: string; private_key_encrypted: string; expires_at: string; created_at: string; }; export function validateCertificate(pem: string, key: string, sni?: string) { if (pem.length > 128 * 1024 || key.length > 32 * 1024) throw new ProvisionError("证书或私钥文件过大。"); let certificate: X509Certificate; try { certificate = new X509Certificate(pem); const privateKey = createPrivateKey(key); if (!certificate.checkPrivateKey(privateKey)) throw new Error(); const chain = pem.match( /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g, ); if (!chain?.length) throw new Error(); chain.forEach((part) => new X509Certificate(part)); } catch { throw new ProvisionError( "无法解析 PEM 证书/私钥,或私钥与证书不匹配。请上传未加密的私钥和完整证书链。", ); } if ( Date.parse(certificate.validFrom) > Date.now() || Date.parse(certificate.validTo) <= Date.now() ) throw new ProvisionError("证书尚未生效或已过期。"); if (sni && !certificate.checkHost(sni, { subject: "never" })) throw new ProvisionError("SNI 域名与证书的 SAN 不匹配。"); return certificate; } export async function saveCertificate(name: string, pem: string, key: string) { if (!name.trim() || name.length > 100) throw new ProvisionError("请填写 1–100 字的证书名称。"); const certificate = validateCertificate(pem, key); const db = await ensureDb(); return db .prepare( `INSERT INTO managed_certificates (name,certificate_pem,private_key_encrypted,expires_at,created_at) VALUES (?,?,?,?,?)`, ) .run( name.trim(), pem, encryptText(key), new Date(certificate.validTo).toISOString(), new Date().toISOString(), ).lastInsertRowid; } export async function listCertificates() { const db = await ensureDb(); return db .prepare( "SELECT id,name,expires_at,created_at FROM managed_certificates ORDER BY id DESC", ) .all() as Pick< ManagedCertificate, "id" | "name" | "expires_at" | "created_at" >[]; } export async function deleteCertificate(id: number) { const db = await ensureDb(); if ( db .prepare("SELECT 1 FROM node_installations WHERE certificate_id=?") .get(id) ) throw new ProvisionError( "此证书仍关联安装记录,请先删除相关安装记录。已安装服务器的证书文件不会随之删除。", ); db.prepare("DELETE FROM managed_certificates WHERE id=?").run(id); }