Files
certd/packages/plugins/plugin-lib/src/cert/cert-reader.ts
T
xiaojunnuo 7cff1a9842 perf: 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip
自定义插件需要压缩包时可以调用new CertReader(certInfo).buildZip() 方式获取
2026-06-30 23:41:59 +08:00

372 lines
9.9 KiB
TypeScript
Raw 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 fs from "fs";
import os from "os";
import path from "path";
import { CertificateInfo, crypto } from "@certd/acme-client";
import cryptoLib from "crypto";
import { ILogger } from "@certd/basic";
import dayjs from "dayjs";
import { uniq } from "lodash-es";
import JSZip from "jszip";
export interface ICertInfoGetter {
getByPipelineId: (pipelineId: number) => Promise<CertInfo>;
}
export type CertInfo = {
crt: string; //fullchain证书
key: string; //私钥
csr?: string; //csr
oc?: string; //仅证书,非fullchain证书
ic?: string; //中间证书
pfx?: string;
der?: string;
jks?: string;
one?: string;
p7b?: string;
};
export type CertReaderHandleContext = {
reader: CertReader;
tmpCrtPath: string;
tmpKeyPath: string;
tmpOcPath?: string;
tmpPfxPath?: string;
tmpDerPath?: string;
tmpIcPath?: string;
tmpJksPath?: string;
tmpOnePath?: string;
tmpP7bPath?: string;
};
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
const formats = {
pem: ["crt", "key", "ic"],
one: ["one"],
pfx: ["pfx"],
der: ["der"],
jks: ["jks"],
p7b: ["p7b", "key"],
};
export type SimpleCertDetail = {
notBefore: Date;
notAfter: Date;
domains: string[];
};
export class CertReader {
cert: CertInfo;
detail: CertificateInfo;
//毫秒时间戳
effective: number;
//毫秒时间戳
expires: number;
constructor(certInfo: CertInfo) {
this.cert = certInfo;
if (!certInfo.ic) {
this.cert.ic = this.getIc();
}
if (!certInfo.oc) {
this.cert.oc = this.getOc();
}
if (!certInfo.one) {
this.cert.one = this.cert.crt + "\n" + this.cert.key;
}
try {
const { detail, effective, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail;
this.effective = effective.getTime();
this.expires = expires.getTime();
} catch (e) {
throw new Error("证书解析失败:" + e.message);
}
}
getIc() {
//中间证书ic 就是crt的第一个 -----END CERTIFICATE----- 之后的内容
const endStr = "-----END CERTIFICATE-----";
const firstBlockEndIndex = this.cert.crt.indexOf(endStr);
const start = firstBlockEndIndex + endStr.length + 1;
if (this.cert.crt.length <= start) {
return "";
}
const ic = this.cert.crt.substring(start);
if (ic == null) {
return "";
}
return ic?.trim();
}
getOc() {
//原始证书 就是crt的第一个 -----END CERTIFICATE----- 之前的内容
const endStr = "-----END CERTIFICATE-----";
const arr = this.cert.crt.split(endStr);
return arr[0] + endStr;
}
toCertInfo(format?: string): CertInfo {
if (!format) {
return this.cert;
}
const formatArr = formats[format];
const res: any = {};
formatArr.forEach((key: string) => {
res[key] = this.cert[key];
});
return res;
}
getCrtDetail(crt: string = this.cert.crt) {
return CertReader.readCertDetail(crt);
}
getSimpleDetail() {
const { detail } = this.getCrtDetail();
return {
notBefore: detail.notBefore,
notAfter: detail.notAfter,
domains: this.getAllDomains(),
};
}
static readCertDetail(crt: string) {
let detail: CertificateInfo;
try {
detail = crypto.readCertificateInfo(crt.toString());
} catch (e) {
throw new Error("证书解析失败:" + e.message + "(请确定证书格式,是否与私钥搞反?)");
}
const effective = detail.notBefore;
const expires = detail.notAfter;
const fingerprints = CertReader.getFingerprintX509(crt);
// @ts-ignore
detail.fingerprints = fingerprints;
return { detail, effective, expires };
}
static getFingerprintX509(crt: string) {
try {
// 创建X509Certificate实例
const cert = new cryptoLib.X509Certificate(crt);
// 获取指纹
return {
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,
fingerprint512: cert.fingerprint512,
};
} catch (error) {
console.error("处理证书失败:", error.message);
return null;
}
}
getAllDomains() {
const { detail } = this.getCrtDetail();
const domains = [];
if (detail.domains?.commonName) {
domains.push(detail.domains.commonName);
}
domains.push(...detail.domains.altNames);
//去重
return uniq(domains);
}
getAltNames() {
const { detail } = this.getCrtDetail();
return detail.domains.altNames;
}
static getMainDomain(crt: string) {
const { detail } = CertReader.readCertDetail(crt);
return CertReader.getMainDomainFromDetail(detail);
}
getMainDomain() {
const { detail } = this.getCrtDetail();
return CertReader.getMainDomainFromDetail(detail);
}
static getMainDomainFromDetail(detail: CertificateInfo) {
let domain = detail?.domains?.commonName;
if (domain == null) {
domain = detail?.domains?.altNames?.[0];
}
if (domain == null) {
domain = "unknown";
}
return domain;
}
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks" | "p7b", filepath?: string) {
if (!this.cert[type]) {
return;
}
if (filepath == null) {
//写入临时目录
filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.${type}`);
}
const dir = path.dirname(filepath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one" || type === "p7b") {
fs.writeFileSync(filepath, this.cert[type]);
} else {
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
}
return filepath;
}
async readCertFile(opts: HandleOpts) {
const logger = opts.logger;
logger.info("将证书写入本地缓存文件");
const tmpCrtPath = this.saveToFile("crt");
const tmpKeyPath = this.saveToFile("key");
const tmpPfxPath = this.saveToFile("pfx");
const tmpIcPath = this.saveToFile("ic");
const tmpOcPath = this.saveToFile("oc");
const tmpDerPath = this.saveToFile("der");
const tmpJksPath = this.saveToFile("jks");
const tmpOnePath = this.saveToFile("one");
const tmpP7bPath = this.saveToFile("p7b");
logger.info("本地文件写入成功");
try {
return await opts.handle({
reader: this,
tmpCrtPath,
tmpKeyPath,
tmpPfxPath,
tmpDerPath,
tmpIcPath,
tmpJksPath,
tmpOcPath,
tmpP7bPath,
tmpOnePath,
});
} catch (err) {
logger.error("处理失败", err);
throw err;
} finally {
//删除临时文件
logger.info("清理临时文件");
function removeFile(filepath?: string) {
if (filepath) {
fs.unlinkSync(filepath);
}
}
removeFile(tmpCrtPath);
removeFile(tmpKeyPath);
removeFile(tmpPfxPath);
removeFile(tmpOcPath);
removeFile(tmpDerPath);
removeFile(tmpIcPath);
removeFile(tmpJksPath);
removeFile(tmpOnePath);
removeFile(tmpP7bPath);
}
}
buildCertFileName(suffix: string, applyTime: any, prefix = "cert") {
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
const timeStr = dayjs(applyTime).format("YYYYMMDDHHmmss");
return `${prefix}_${domain}_${timeStr}.${suffix}`;
}
buildCertName(prefix: string = "", useHash: boolean = false) {
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
if (useHash) {
const domains = JSON.stringify(this.getAllDomains());
const hash = cryptoLib.createHash("md5").update(domains).digest("hex").slice(0, 16);
return `${prefix}_${domain}_${hash}`;
}
return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
}
static appendTimeSuffix(name?: string) {
if (name == null) {
name = "certd";
}
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
}
static buildCertName(cert: CertInfo, useHash: boolean = false) {
return new CertReader(cert).buildCertName("", useHash);
}
async buildZip(): Promise<Buffer> {
const cert = this.cert;
const zip = new JSZip();
if (cert.crt) {
zip.file("证书.pem", cert.crt);
}
if (cert.key) {
zip.file("私钥.pem", cert.key);
}
if (cert.ic) {
zip.file("中间证书.pem", cert.ic);
}
if (cert.crt) {
zip.file("cert.crt", cert.crt);
}
if (cert.key) {
zip.file("cert.key", cert.key);
}
if (cert.ic) {
zip.file("intermediate.crt", cert.ic);
}
if (cert.oc) {
zip.file("origin.crt", cert.oc);
}
if (cert.one) {
zip.file("one.pem", cert.one);
}
if (cert.p7b) {
zip.file("cert.p7b", cert.p7b);
}
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}
if (cert.der) {
zip.file("cert.der", Buffer.from(cert.der, "base64"));
}
if (cert.jks) {
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
}
zip.file(
"说明.txt",
`证书文件说明
cert.crt:证书文件,包含证书链,pem格式
cert.key:私钥文件,pem格式
intermediate.crt:中间证书文件,pem格式
origin.crt:原始证书文件,不含证书链,pem格式
one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文
cert.pfxpfx格式证书文件,iis服务器使用
cert.derder格式证书文件
cert.jksjks格式证书文件,java服务器使用
`
);
return zip.generateAsync({ type: "nodebuffer" });
}
buildZipFilename(prefix = "cert"): string {
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
const timeStr = dayjs().format("YYYYMMDDHHmmss");
return `${prefix}_${domain}_${timeStr}.zip`;
}
}