mirror of
https://github.com/certd/certd.git
synced 2026-10-13 15:49:25 +08:00
fix: 支持google persist校验方式
This commit is contained in:
@@ -27,7 +27,8 @@
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"node-forge": "^1.3.1",
|
||||
"punycode.js": "^2.3.1"
|
||||
"punycode.js": "^2.3.1",
|
||||
"psl": "^1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @ts-ignore
|
||||
import psl from "psl";
|
||||
|
||||
export function getDnsPersistIssuer(directoryUrl?: string) {
|
||||
if (!directoryUrl) {
|
||||
return "letsencrypt.org";
|
||||
}
|
||||
|
||||
let directory: URL;
|
||||
try {
|
||||
directory = new URL(directoryUrl);
|
||||
} catch {
|
||||
throw new Error(`ACME Directory URL无效,无法生成DNS持久验证Issuer:${directoryUrl}`);
|
||||
}
|
||||
|
||||
const issuer = psl.get(directory.hostname);
|
||||
if (!issuer) {
|
||||
throw new Error(`无法从ACME Directory URL获取可注册主域名:${directory.hostname}`);
|
||||
}
|
||||
return issuer;
|
||||
}
|
||||
@@ -79,3 +79,4 @@ export * from './verify.js'
|
||||
export * from './error.js'
|
||||
|
||||
export * from './util.js'
|
||||
export * from './dns-persist.js'
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import assert from "assert";
|
||||
import { getDnsPersistIssuer } from "@certd/acme-client";
|
||||
import { buildDnsPersistRecordValue, DnsPersistRecordService } from "./dns-persist-record-service.js";
|
||||
|
||||
describe("DnsPersistRecordService", () => {
|
||||
it("gets issuer from the ACME directory URL", () => {
|
||||
assert.equal(getDnsPersistIssuer("https://dv.acme-v02.api.pki.goog/directory"), "pki.goog");
|
||||
assert.equal(getDnsPersistIssuer("https://acme-v02.api.letsencrypt.org/directory"), "letsencrypt.org");
|
||||
assert.equal(getDnsPersistIssuer("https://acme.example.co.uk/directory"), "example.co.uk");
|
||||
assert.equal(getDnsPersistIssuer(), "letsencrypt.org");
|
||||
assert.throws(() => getDnsPersistIssuer("not a url"), /Directory URL无效/);
|
||||
assert.throws(() => getDnsPersistIssuer("https://localhost/directory"), /可注册主域名/);
|
||||
});
|
||||
it("builds dns-persist-01 record value", () => {
|
||||
const value = buildDnsPersistRecordValue({
|
||||
accountUri: "https://example.com/acct/1",
|
||||
@@ -12,6 +21,36 @@ describe("DnsPersistRecordService", () => {
|
||||
assert.equal(value, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard; persistUntil=1893456000");
|
||||
});
|
||||
|
||||
it("uses the registrable domain from the ACME directory URL", async () => {
|
||||
const service = new DnsPersistRecordService();
|
||||
(service as any).accessService = {
|
||||
async getAccessById() {
|
||||
return {
|
||||
account: JSON.stringify({
|
||||
accountKey: "private-key",
|
||||
accountUri: "https://pki.goog/acct/12345678",
|
||||
caType: "custom",
|
||||
directoryUrl: "https://dv.acme-v02.api.pki.goog/directory",
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const buildRequest = {
|
||||
domain: "example.com",
|
||||
caType: "custom",
|
||||
acmeAccountAccessId: 12,
|
||||
userId: 1,
|
||||
};
|
||||
const acmeAccount = await service.getAcmeAccount(buildRequest);
|
||||
|
||||
assert.equal(acmeAccount.account.issuer, "pki.goog");
|
||||
|
||||
const record = await service.buildRecordByAcmeAccount(buildRequest);
|
||||
|
||||
assert.equal(record.recordValue, "pki.goog; accounturi=https://pki.goog/acct/12345678; policy=wildcard");
|
||||
});
|
||||
|
||||
it("builds validation host from wildcard domain", async () => {
|
||||
const service = new DnsPersistRecordService();
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import { BaseService } from "@certd/lib-server";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { createChallengeFn } from "@certd/acme-client";
|
||||
import { createChallengeFn, getDnsPersistIssuer } from "@certd/acme-client";
|
||||
import { AccessService } from "@certd/lib-server";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { createDnsProvider, DomainParser } from "@certd/plugin-lib";
|
||||
import { DnsPersistRecordEntity } from "../entity/dns-persist-record.js";
|
||||
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
|
||||
import { DomainEntity } from "../entity/domain.js";
|
||||
|
||||
export function buildDnsPersistRecordValue(req: { issuer?: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
|
||||
const parts = [req.issuer || "letsencrypt.org", `accounturi=${req.accountUri}`];
|
||||
if (req.wildcard !== false) {
|
||||
@@ -93,6 +92,9 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
if (!parsed.accountKey || !parsed.accountUri) {
|
||||
throw new Error("ACME账号授权无效,请重新生成ACME账号");
|
||||
}
|
||||
if (!parsed.issuer) {
|
||||
parsed.issuer = getDnsPersistIssuer(parsed.directoryUrl);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
};
|
||||
}
|
||||
|
||||
async buildRecord(req: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number; userId?: number; projectId?: number }) {
|
||||
async buildRecord(req: { domain: string; accountUri: string; issuer?: string; wildcard?: boolean; persistUntil?: number; userId?: number; projectId?: number }) {
|
||||
const domain = this.normalizeDomain(req.domain);
|
||||
const mainDomain = await this.parseMainDomain(domain, req.userId, req.projectId);
|
||||
return {
|
||||
@@ -141,6 +143,7 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
const record = await this.buildRecord({
|
||||
domain: req.domain,
|
||||
accountUri: account.accountUri,
|
||||
issuer: account.issuer,
|
||||
wildcard: true,
|
||||
persistUntil: req.persistUntil,
|
||||
userId: req.userId,
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("AcmeAccountAccess", () => {
|
||||
|
||||
assert.equal(account.accountKey, "private-key");
|
||||
assert.equal(account.accountUri, "https://example.com/acct/1");
|
||||
assert.equal(account.issuer, "example.com");
|
||||
});
|
||||
|
||||
it("generates account payload through acme service", async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AcmeAccountInfo = {
|
||||
caType: string;
|
||||
email: string;
|
||||
directoryUrl: string;
|
||||
issuer?: string;
|
||||
eab?: {
|
||||
kid?: string;
|
||||
hmacKey?: string;
|
||||
@@ -20,10 +21,11 @@ function parseAccount(account?: string | AcmeAccountInfo): AcmeAccountInfo | nul
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
if (typeof account !== "string") {
|
||||
return account;
|
||||
const parsed = typeof account === "string" ? JSON.parse(account) : account;
|
||||
if (!parsed.issuer) {
|
||||
parsed.issuer = acme.getDnsPersistIssuer(parsed.directoryUrl);
|
||||
}
|
||||
return JSON.parse(account);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@IsAccess({
|
||||
@@ -297,6 +299,7 @@ export class AcmeAccountAccess extends BaseAccess {
|
||||
caType: this.caType,
|
||||
email: this.email,
|
||||
directoryUrl,
|
||||
issuer: acme.getDnsPersistIssuer(directoryUrl),
|
||||
};
|
||||
if (externalAccountBinding) {
|
||||
account.eab = {
|
||||
|
||||
@@ -38,6 +38,7 @@ export type AcmeAccountInfo = {
|
||||
caType: SSLProvider | string;
|
||||
email: string;
|
||||
directoryUrl: string;
|
||||
issuer?: string;
|
||||
};
|
||||
|
||||
export type DnsPersistVerifyPlan = {
|
||||
|
||||
@@ -4,6 +4,14 @@ import { NonRetryableException } from "@certd/lib-server";
|
||||
import { CertApplyPlugin } from "./apply.js";
|
||||
|
||||
describe("CertApplyPlugin dns-persist verify plan", () => {
|
||||
it("builds dns-persist fallback values from the ACME account issuer", () => {
|
||||
const plugin: any = new CertApplyPlugin();
|
||||
|
||||
const recordValue = plugin.buildDnsPersistRecordValue("https://pki.goog/acct/12345678", true, undefined, "pki.goog");
|
||||
|
||||
assert.equal(recordValue, "pki.goog; accounturi=https://pki.goog/acct/12345678; policy=wildcard");
|
||||
});
|
||||
|
||||
it("keeps dns-persist entries when building mixed domain verify plans", async () => {
|
||||
const plugin: any = new CertApplyPlugin();
|
||||
plugin.acme = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { getDnsPersistIssuer } from "@certd/acme-client";
|
||||
import { utils } from "@certd/basic";
|
||||
import { CustomAcmeProvider, NonRetryableException } from "@certd/lib-server";
|
||||
|
||||
@@ -664,10 +665,10 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
|
||||
async doCertApply() {
|
||||
await this.getAcmeClient();
|
||||
// 自定义ACME不支持DNS持久验证(_validation-persist 为 Let's Encrypt 特有机制)
|
||||
if (this.acmeProvider && this.acmeProvider.builtIn !== true && this.challengeType === "dns-persist") {
|
||||
throw new Error("自定义ACME不支持DNS持久验证,请改用其他域名验证方式");
|
||||
}
|
||||
// 自定义ACME暂不支持DNS持久验证
|
||||
// if (this.acmeProvider && this.acmeProvider.builtIn !== true && this.challengeType === "dns-persist") {
|
||||
// throw new Error("自定义ACME不支持DNS持久验证,请改用其他域名验证方式");
|
||||
// }
|
||||
let email = this.email;
|
||||
if (this.eab && this.eab.email) {
|
||||
email = this.eab.email;
|
||||
@@ -786,6 +787,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
if (!parsed.accountKey || !parsed.accountUri) {
|
||||
throw new Error("ACME账号无效,请重新生成ACME账号");
|
||||
}
|
||||
if (!parsed.issuer) {
|
||||
parsed.issuer = getDnsPersistIssuer(parsed.directoryUrl);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -834,14 +838,14 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
domain,
|
||||
dnsPersistVerifyPlan: {
|
||||
hostRecord: persistRecord.hostRecord || `_validation-persist.${domain}`,
|
||||
recordValue: persistRecord.recordValue || this.buildDnsPersistRecordValue(acmeAccount.accountUri, true),
|
||||
recordValue: persistRecord.recordValue || this.buildDnsPersistRecordValue(acmeAccount.accountUri, true, undefined, acmeAccount.issuer),
|
||||
accountUri: persistRecord.accountUri || acmeAccount.accountUri,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
buildDnsPersistRecordValue(accountUri: string, wildcard = false, persistUntil?: number) {
|
||||
const parts = [`letsencrypt.org`, `accounturi=${accountUri}`];
|
||||
buildDnsPersistRecordValue(accountUri: string, wildcard = false, persistUntil?: number, issuer = "letsencrypt.org") {
|
||||
const parts = [issuer, `accounturi=${accountUri}`];
|
||||
if (wildcard !== false) {
|
||||
parts.push("policy=wildcard");
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("WangsuAccess 授权方式", () => {
|
||||
it("未选择 ApiKey 方式时,ApiKey 专属接口给出明确指引", () => {
|
||||
const { access } = createAccess({ authType: "aksk" });
|
||||
|
||||
assert.throws(() => access.encryptPrivateKey("MOCK-KEY"), /当前授权未选择「ApiKey(CDN Pro)」鉴权方式/);
|
||||
assert.throws(() => access.encryptPrivateKey("MOCK-KEY"), /当前授权未选择「ApiKey」鉴权方式/);
|
||||
});
|
||||
|
||||
it("requireApiKeyAuth 在 apikey 方式下放行,在 aksk 方式下报错", () => {
|
||||
@@ -147,7 +147,7 @@ describe("WangsuAccess 授权方式", () => {
|
||||
assert.doesNotThrow(() => access.requireApiKeyAuth());
|
||||
|
||||
const akskAccess = createAccess({ authType: "aksk" }).access;
|
||||
assert.throws(() => akskAccess.requireApiKeyAuth(), /当前授权未选择「ApiKey(CDN Pro)」鉴权方式/);
|
||||
assert.throws(() => akskAccess.requireApiKeyAuth(), /当前授权未选择「ApiKey」鉴权方式/);
|
||||
});
|
||||
|
||||
it("doRequest 传入 dateStr 时用该 Date 鉴权,保证与私钥加密用的是同一个 Date", async () => {
|
||||
@@ -196,17 +196,6 @@ describe("WangsuAccess 授权方式", () => {
|
||||
assert.deepEqual(list, certificates);
|
||||
});
|
||||
|
||||
it("getCertDnsNames 兼容 dnsNames / dns_names / domains 三种域名写法", () => {
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames({ dnsNames: ["a.example.com"] }), ["a.example.com"]);
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames({ dns_names: ["b.example.com"] }), ["b.example.com"]);
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames({ domains: ["c.example.com"] }), ["c.example.com"]);
|
||||
// 单个字符串也统一成数组
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames({ dnsNames: "d.example.com" }), ["d.example.com"]);
|
||||
// 没有域名信息时返回空数组,交给调用方提示,不要抛异常
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames({ id: "cert-1" }), []);
|
||||
assert.deepEqual(WangsuAccess.getCertDnsNames(null), []);
|
||||
});
|
||||
|
||||
it("apikey 方式下 PATCH 证书把请求体透传", async () => {
|
||||
const { access, http } = createAccess({ authType: "apikey" });
|
||||
http.push(cdnProResponse({ status: 201, headers: { location: "https://x/cdn/certificates/cert-1/versions/2" }, data: "" }));
|
||||
@@ -293,6 +282,6 @@ describe("WangsuAccess 授权方式", () => {
|
||||
it("encryptPrivateKey 在未选择 ApiKey 方式时直接报错", () => {
|
||||
const { access } = createAccess({ authType: "aksk" });
|
||||
|
||||
assert.throws(() => access.encryptPrivateKey("MOCK-KEY"), /当前授权未选择「ApiKey(CDN Pro)」鉴权方式/);
|
||||
assert.throws(() => access.encryptPrivateKey("MOCK-KEY"), /当前授权未选择「ApiKey」鉴权方式/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user