Files
hy2-panel/lib/certificates.ts

86 lines
2.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}