mirror of https://github.com/certd/certd
				
				
				
			feat: 支持ECC类型
							parent
							
								
									d4092e4929
								
							
						
					
					
						commit
						a7424e02f5
					
				| 
						 | 
					@ -14,7 +14,7 @@ export type CertInfo = {
 | 
				
			||||||
  csr: string;
 | 
					  csr: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
 | 
					export type SSLProvider = "letsencrypt" | "google" | "zerossl";
 | 
				
			||||||
export type PrivateKeyType = "rsa" | "ec";
 | 
					export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
 | 
				
			||||||
type AcmeServiceOptions = {
 | 
					type AcmeServiceOptions = {
 | 
				
			||||||
  userContext: IContext;
 | 
					  userContext: IContext;
 | 
				
			||||||
  logger: Logger;
 | 
					  logger: Logger;
 | 
				
			||||||
| 
						 | 
					@ -226,12 +226,16 @@ export class AcmeService {
 | 
				
			||||||
    /* Create CSR */
 | 
					    /* Create CSR */
 | 
				
			||||||
    const { commonName, altNames } = this.buildCommonNameByDomains(domains);
 | 
					    const { commonName, altNames } = this.buildCommonNameByDomains(domains);
 | 
				
			||||||
    let privateKey = null;
 | 
					    let privateKey = null;
 | 
				
			||||||
    if (options.privateKeyType == "ec") {
 | 
					    const privateKeyArr = options.privateKeyType.split("_");
 | 
				
			||||||
      privateKey = await acme.crypto.createPrivateEcdsaKey();
 | 
					    const type = privateKeyArr[0];
 | 
				
			||||||
 | 
					    const size = parseInt(privateKeyArr[1]);
 | 
				
			||||||
 | 
					    if (type == "ec") {
 | 
				
			||||||
 | 
					      const name: any = "P-" + size;
 | 
				
			||||||
 | 
					      privateKey = await acme.crypto.createPrivateEcdsaKey(name);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      privateKey = await acme.crypto.createPrivateRsaKey();
 | 
					      privateKey = await acme.crypto.createPrivateRsaKey(size);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const [key, csr] = await acme.forge.createCsr(
 | 
					    const [key, csr] = await acme.crypto.createCsr(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        commonName,
 | 
					        commonName,
 | 
				
			||||||
        ...csrInfo,
 | 
					        ...csrInfo,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -133,10 +133,10 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
 | 
				
			||||||
    const cert: CertInfo = certReader.toCertInfo();
 | 
					    const cert: CertInfo = certReader.toCertInfo();
 | 
				
			||||||
    this.cert = cert;
 | 
					    this.cert = cert;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.validity.notAfter).valueOf();
 | 
					    this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isNew) {
 | 
					    if (isNew) {
 | 
				
			||||||
      const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
 | 
					      const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss");
 | 
				
			||||||
      await this.zipCert(cert, applyTime);
 | 
					      await this.zipCert(cert, applyTime);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.extendsFiles();
 | 
					      this.extendsFiles();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
import { CertInfo } from "./acme.js";
 | 
					import { CertInfo } from "./acme.js";
 | 
				
			||||||
import fs from "fs";
 | 
					import fs from "fs";
 | 
				
			||||||
import os from "os";
 | 
					import os from "os";
 | 
				
			||||||
import forge from "node-forge";
 | 
					 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
 | 
					import { crypto } from "@certd/acme-client";
 | 
				
			||||||
export class CertReader implements CertInfo {
 | 
					export class CertReader implements CertInfo {
 | 
				
			||||||
  crt: string;
 | 
					  crt: string;
 | 
				
			||||||
  key: string;
 | 
					  key: string;
 | 
				
			||||||
| 
						 | 
					@ -29,9 +29,8 @@ export class CertReader implements CertInfo {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getCrtDetail(crt: string) {
 | 
					  getCrtDetail(crt: string) {
 | 
				
			||||||
    const pki = forge.pki;
 | 
					    const detail = crypto.readCertificateInfo(crt.toString());
 | 
				
			||||||
    const detail = pki.certificateFromPem(crt.toString());
 | 
					    const expires = detail.notAfter;
 | 
				
			||||||
    const expires = detail.validity.notAfter;
 | 
					 | 
				
			||||||
    return { detail, expires };
 | 
					    return { detail, expires };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,13 +44,18 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @TaskInput({
 | 
					  @TaskInput({
 | 
				
			||||||
    title: "证书私钥类型",
 | 
					    title: "证书私钥类型",
 | 
				
			||||||
    value: "rsa",
 | 
					    value: "rsa_2048",
 | 
				
			||||||
    component: {
 | 
					    component: {
 | 
				
			||||||
      name: "a-select",
 | 
					      name: "a-select",
 | 
				
			||||||
      vModel: "value",
 | 
					      vModel: "value",
 | 
				
			||||||
      options: [
 | 
					      options: [
 | 
				
			||||||
        { value: "rsa", label: "RSA" },
 | 
					        { value: "rsa_1024", label: "RSA 1024" },
 | 
				
			||||||
        { value: "ec", label: "EC" },
 | 
					        { value: "rsa_2048", label: "RSA 2048" },
 | 
				
			||||||
 | 
					        { value: "rsa_3072", label: "RSA 3072" },
 | 
				
			||||||
 | 
					        { value: "rsa_4096", label: "RSA 4096" },
 | 
				
			||||||
 | 
					        { value: "ec_256", label: "EC 256" },
 | 
				
			||||||
 | 
					        { value: "ec_384", label: "EC 384" },
 | 
				
			||||||
 | 
					        { value: "ec_521", label: "EC 521" },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    required: true,
 | 
					    required: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -342,7 +342,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
 | 
				
			||||||
          title: "历史记录保持数",
 | 
					          title: "历史记录保持数",
 | 
				
			||||||
          type: "number",
 | 
					          type: "number",
 | 
				
			||||||
          form: {
 | 
					          form: {
 | 
				
			||||||
            value: 10,
 | 
					            value: 20,
 | 
				
			||||||
            helper: "历史记录保持条数,多余的会被删除"
 | 
					            helper: "历史记录保持条数,多余的会被删除"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          column: {
 | 
					          column: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
export const Constants = {
 | 
					export const Constants = {
 | 
				
			||||||
 | 
					  dataDir: './data',
 | 
				
			||||||
  role: {
 | 
					  role: {
 | 
				
			||||||
    defaultUser: 3,
 | 
					    defaultUser: 3,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
 | 
				
			||||||
    return id;
 | 
					    return id;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async clear(pipelineId: number, keepCount = 10) {
 | 
					  private async clear(pipelineId: number, keepCount = 20) {
 | 
				
			||||||
    const count = await this.repository.count({
 | 
					    const count = await this.repository.count({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        pipelineId,
 | 
					        pipelineId,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,99 @@
 | 
				
			||||||
 | 
					import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
 | 
				
			||||||
 | 
					import { CertInfo, CertReader } from '@certd/plugin-cert';
 | 
				
			||||||
 | 
					import * as fs from 'fs';
 | 
				
			||||||
 | 
					import { Constants } from '../../../../basic/constants.js';
 | 
				
			||||||
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@IsTaskPlugin({
 | 
				
			||||||
 | 
					  name: 'CopyToLocal',
 | 
				
			||||||
 | 
					  title: '复制到本机',
 | 
				
			||||||
 | 
					  group: pluginGroups.host.key,
 | 
				
			||||||
 | 
					  default: {
 | 
				
			||||||
 | 
					    strategy: {
 | 
				
			||||||
 | 
					      runStrategy: RunStrategy.SkipWhenSucceed,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
 | 
				
			||||||
 | 
					  @TaskInput({
 | 
				
			||||||
 | 
					    title: '证书保存路径',
 | 
				
			||||||
 | 
					    helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.pem',
 | 
				
			||||||
 | 
					    component: {
 | 
				
			||||||
 | 
					      placeholder: './tmp/cert.pem',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  crtPath!: string;
 | 
				
			||||||
 | 
					  @TaskInput({
 | 
				
			||||||
 | 
					    title: '私钥保存路径',
 | 
				
			||||||
 | 
					    helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:./tmp/cert.key',
 | 
				
			||||||
 | 
					    component: {
 | 
				
			||||||
 | 
					      placeholder: './tmp/cert.key',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  keyPath!: string;
 | 
				
			||||||
 | 
					  @TaskInput({
 | 
				
			||||||
 | 
					    title: '域名证书',
 | 
				
			||||||
 | 
					    helper: '请选择前置任务输出的域名证书',
 | 
				
			||||||
 | 
					    component: {
 | 
				
			||||||
 | 
					      name: 'pi-output-selector',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    required: true,
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  cert!: CertInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @TaskOutput({
 | 
				
			||||||
 | 
					    title: '证书保存路径',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  hostCrtPath!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @TaskOutput({
 | 
				
			||||||
 | 
					    title: '私钥保存路径',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  hostKeyPath!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async onInstance() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  copyFile(srcFile: string, destFile: string) {
 | 
				
			||||||
 | 
					    this.logger.info(`复制文件:${srcFile} => ${destFile}`);
 | 
				
			||||||
 | 
					    const dir = path.dirname(destFile);
 | 
				
			||||||
 | 
					    if (!fs.existsSync(dir)) {
 | 
				
			||||||
 | 
					      fs.mkdirSync(dir, { recursive: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fs.copyFileSync(srcFile, destFile);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async execute(): Promise<void> {
 | 
				
			||||||
 | 
					    let { crtPath, keyPath } = this;
 | 
				
			||||||
 | 
					    const certReader = new CertReader(this.cert);
 | 
				
			||||||
 | 
					    this.logger.info('将证书写入本地缓存文件');
 | 
				
			||||||
 | 
					    const saveCrtPath = certReader.saveToFile('crt');
 | 
				
			||||||
 | 
					    const saveKeyPath = certReader.saveToFile('key');
 | 
				
			||||||
 | 
					    this.logger.info('本地文件写入成功');
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      this.logger.info('复制到目标路径');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
 | 
				
			||||||
 | 
					      keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
 | 
				
			||||||
 | 
					      // crtPath = path.resolve(crtPath);
 | 
				
			||||||
 | 
					      // keyPath = path.resolve(keyPath);
 | 
				
			||||||
 | 
					      this.copyFile(saveCrtPath, crtPath);
 | 
				
			||||||
 | 
					      this.copyFile(saveKeyPath, keyPath);
 | 
				
			||||||
 | 
					      this.logger.info('证书复制成功:crtPath=', crtPath, ',keyPath=', keyPath);
 | 
				
			||||||
 | 
					      this.logger.info('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面');
 | 
				
			||||||
 | 
					      this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      this.logger.error(`复制失败:${e.message}`);
 | 
				
			||||||
 | 
					      throw e;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      //删除临时文件
 | 
				
			||||||
 | 
					      this.logger.info('删除临时文件');
 | 
				
			||||||
 | 
					      fs.unlinkSync(saveCrtPath);
 | 
				
			||||||
 | 
					      fs.unlinkSync(saveKeyPath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.logger.info('执行完成');
 | 
				
			||||||
 | 
					    //输出
 | 
				
			||||||
 | 
					    this.hostCrtPath = crtPath;
 | 
				
			||||||
 | 
					    this.hostKeyPath = keyPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					new CopyCertToLocalPlugin();
 | 
				
			||||||
		Loading…
	
		Reference in New Issue