Merge pull request #5003 from Infisical/PKI-48-issue-cert-from-acme-in-bg

improvement(api): issue cert from acme in a background queue
This commit is contained in:
Fang-Pen Lin
2025-12-12 19:41:44 -08:00
committed by GitHub
8 changed files with 310 additions and 111 deletions

View File

@@ -0,0 +1,38 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { dropConstraintIfExists } from "./utils/dropConstraintIfExists";
const FOREIGN_KEY_CONSTRAINT_NAME = "certificate_requests_acme_order_id_fkey";
const INDEX_NAME = "certificate_requests_acme_order_id_idx";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateRequests)) {
const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId");
if (!hasAcmeOrderId) {
await knex.schema.alterTable(TableName.CertificateRequests, (t) => {
t.uuid("acmeOrderId").nullable();
t.foreign("acmeOrderId", FOREIGN_KEY_CONSTRAINT_NAME)
.references("id")
.inTable(TableName.PkiAcmeOrder)
.onDelete("SET NULL");
t.index("acmeOrderId", INDEX_NAME);
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateRequests)) {
const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId");
if (hasAcmeOrderId) {
await dropConstraintIfExists(TableName.CertificateRequests, FOREIGN_KEY_CONSTRAINT_NAME, knex);
await knex.schema.alterTable(TableName.CertificateRequests, (t) => {
t.dropIndex("acmeOrderId", INDEX_NAME);
t.dropColumn("acmeOrderId");
});
}
}
}

View File

@@ -26,7 +26,8 @@ export const CertificateRequestsSchema = z.object({
keyAlgorithm: z.string().nullable().optional(), keyAlgorithm: z.string().nullable().optional(),
signatureAlgorithm: z.string().nullable().optional(), signatureAlgorithm: z.string().nullable().optional(),
errorMessage: z.string().nullable().optional(), errorMessage: z.string().nullable().optional(),
metadata: z.string().nullable().optional() metadata: z.string().nullable().optional(),
acmeOrderId: z.string().uuid().nullable().optional()
}); });
export type TCertificateRequests = z.infer<typeof CertificateRequestsSchema>; export type TCertificateRequests = z.infer<typeof CertificateRequestsSchema>;

View File

@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
export type TPkiAcmeOrderDALFactory = ReturnType<typeof pkiAcmeOrderDALFactory>; export type TPkiAcmeOrderDALFactory = ReturnType<typeof pkiAcmeOrderDALFactory>;
@@ -19,6 +20,43 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
} }
}; };
const findWithCertificateRequestForSync = async (id: string, tx?: Knex) => {
try {
const order = await (tx || db)(TableName.PkiAcmeOrder)
.leftJoin(
TableName.CertificateRequests,
`${TableName.PkiAcmeOrder}.id`,
`${TableName.CertificateRequests}.acmeOrderId`
)
.select(
selectAllTableCols(TableName.PkiAcmeOrder),
db.ref("id").withSchema(TableName.CertificateRequests).as("certificateRequestId"),
db.ref("status").withSchema(TableName.CertificateRequests).as("certificateRequestStatus"),
db.ref("certificateId").withSchema(TableName.CertificateRequests).as("certificateId")
)
.forUpdate(TableName.PkiAcmeOrder)
.where(`${TableName.PkiAcmeOrder}.id`, id)
.first();
if (!order) {
return null;
}
const { certificateRequestId, certificateRequestStatus, certificateId, ...details } = order;
return {
...details,
certificateRequest:
certificateRequestId && certificateRequestStatus
? {
id: certificateRequestId,
status: certificateRequestStatus as CertificateRequestStatus,
certificateId
}
: undefined
};
} catch (error) {
throw new DatabaseError({ error, name: "Find PKI ACME order by id with certificate request" });
}
};
const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => { const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => {
try { try {
const rows = await (tx || db)(TableName.PkiAcmeOrder) const rows = await (tx || db)(TableName.PkiAcmeOrder)
@@ -72,6 +110,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
return { return {
...pkiAcmeOrderOrm, ...pkiAcmeOrderOrm,
findByIdForFinalization, findByIdForFinalization,
findWithCertificateRequestForSync,
findByAccountAndOrderIdWithAuthorizations, findByAccountAndOrderIdWithAuthorizations,
listByAccountId listByAccountId
}; };

View File

@@ -6,8 +6,8 @@ export enum AcmeIdentifierType {
export enum AcmeOrderStatus { export enum AcmeOrderStatus {
Pending = "pending", Pending = "pending",
Processing = "processing",
Ready = "ready", Ready = "ready",
Processing = "processing",
Valid = "valid", Valid = "valid",
Invalid = "invalid" Invalid = "invalid"
} }

View File

@@ -7,8 +7,10 @@ import {
importJWK, importJWK,
JWSHeaderParameters JWSHeaderParameters
} from "jose"; } from "jose";
import { Knex } from "knex";
import { z, ZodError } from "zod"; import { z, ZodError } from "zod";
import { TPkiAcmeOrders } from "@app/db/schemas";
import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts";
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
@@ -17,20 +19,15 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange"; import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { CertSubjectAlternativeNameType } from "@app/services/certificate/certificate-types";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyUsage,
CertSubjectAlternativeNameType
} from "@app/services/certificate/certificate-types";
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import {
TCertificateIssuanceQueueFactory,
TIssueCertificateFromProfileJobData
} from "@app/services/certificate-authority/certificate-issuance-queue";
import { import {
extractAlgorithmsFromCSR, extractAlgorithmsFromCSR,
extractCertificateRequestFromCSR extractCertificateRequestFromCSR
@@ -40,6 +37,8 @@ import {
EnrollmentType, EnrollmentType,
TCertificateProfileWithConfigs TCertificateProfileWithConfigs
} from "@app/services/certificate-profile/certificate-profile-types"; } from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service";
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
import { TCertificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal"; import { TCertificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
@@ -100,13 +99,9 @@ import {
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateTemplateV2DAL: Pick<TCertificateTemplateV2DALFactory, "findById">; certificateTemplateV2DAL: Pick<TCertificateTemplateV2DALFactory, "findById">;
acmeAccountDAL: Pick< acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory, TPkiAcmeAccountDALFactory,
@@ -114,11 +109,13 @@ type TPkiAcmeServiceFactoryDep = {
>; >;
acmeOrderDAL: Pick< acmeOrderDAL: Pick<
TPkiAcmeOrderDALFactory, TPkiAcmeOrderDALFactory,
| "findById"
| "create" | "create"
| "transaction" | "transaction"
| "updateById" | "updateById"
| "findByAccountAndOrderIdWithAuthorizations" | "findByAccountAndOrderIdWithAuthorizations"
| "findByIdForFinalization" | "findByIdForFinalization"
| "findWithCertificateRequestForSync"
| "listByAccountId" | "listByAccountId"
>; >;
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">; acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
@@ -135,6 +132,8 @@ type TPkiAcmeServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">; certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">; certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
certificateIssuanceQueue: Pick<TCertificateIssuanceQueueFactory, "queueCertificateIssuance">;
acmeChallengeService: Pick<TPkiAcmeChallengeServiceFactory, "markChallengeAsReady">; acmeChallengeService: Pick<TPkiAcmeChallengeServiceFactory, "markChallengeAsReady">;
pkiAcmeQueueService: Pick<TPkiAcmeQueueServiceFactory, "queueChallengeValidation">; pkiAcmeQueueService: Pick<TPkiAcmeQueueServiceFactory, "queueChallengeValidation">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
@@ -142,13 +141,9 @@ type TPkiAcmeServiceFactoryDep = {
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
projectDAL, projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
certificateTemplateV2DAL, certificateTemplateV2DAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
@@ -160,6 +155,8 @@ export const pkiAcmeServiceFactory = ({
licenseService, licenseService,
certificateV3Service, certificateV3Service,
certificateTemplateV2Service, certificateTemplateV2Service,
certificateRequestService,
certificateIssuanceQueue,
acmeChallengeService, acmeChallengeService,
pkiAcmeQueueService, pkiAcmeQueueService,
auditLogService auditLogService
@@ -367,6 +364,52 @@ export const pkiAcmeServiceFactory = ({
}; };
}; };
const checkAndSyncAcmeOrderStatus = async ({ orderId }: { orderId: string }): Promise<TPkiAcmeOrders> => {
const order = await acmeOrderDAL.findById(orderId);
if (!order) {
throw new NotFoundError({ message: "ACME order not found" });
}
if (order.status !== AcmeOrderStatus.Processing) {
// We only care about processing orders, as they are the ones that have async certificate requests
return order;
}
return acmeOrderDAL.transaction(async (tx) => {
// Lock the order for syncing with async cert request
const orderWithCertificateRequest = await acmeOrderDAL.findWithCertificateRequestForSync(orderId, tx);
if (!orderWithCertificateRequest) {
throw new NotFoundError({ message: "ACME order not found" });
}
// Check the status again after we have acquired the lock, as things may have changed since we last checked
if (
orderWithCertificateRequest.status !== AcmeOrderStatus.Processing ||
!orderWithCertificateRequest.certificateRequest
) {
return orderWithCertificateRequest;
}
let newStatus: AcmeOrderStatus | undefined;
let newCertificateId: string | undefined;
switch (orderWithCertificateRequest.certificateRequest.status) {
case CertificateRequestStatus.PENDING:
break;
case CertificateRequestStatus.ISSUED:
newStatus = AcmeOrderStatus.Valid;
newCertificateId = orderWithCertificateRequest.certificateRequest.certificateId ?? undefined;
break;
case CertificateRequestStatus.FAILED:
newStatus = AcmeOrderStatus.Invalid;
break;
default:
throw new AcmeServerInternalError({
message: `Invalid certificate request status: ${orderWithCertificateRequest.certificateRequest.status as string}`
});
}
if (newStatus) {
return acmeOrderDAL.updateById(orderId, { status: newStatus, certificateId: newCertificateId }, tx);
}
return orderWithCertificateRequest;
});
};
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => { const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
return { return {
@@ -734,9 +777,12 @@ export const pkiAcmeServiceFactory = ({
if (!order) { if (!order) {
throw new NotFoundError({ message: "ACME order not found" }); throw new NotFoundError({ message: "ACME order not found" });
} }
// Sync order first in case if there is a certificate request that needs to be processed
await checkAndSyncAcmeOrderStatus({ orderId });
const updatedOrder = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId))!;
return { return {
status: 200, status: 200,
body: buildAcmeOrderResource({ profileId, order }), body: buildAcmeOrderResource({ profileId, order: updatedOrder }),
headers: { headers: {
Location: buildUrl(profileId, `/orders/${orderId}`), Location: buildUrl(profileId, `/orders/${orderId}`),
Link: `<${buildUrl(profileId, "/directory")}>;rel="index"` Link: `<${buildUrl(profileId, "/directory")}>;rel="index"`
@@ -744,6 +790,129 @@ export const pkiAcmeServiceFactory = ({
}; };
}; };
const processCertificateIssuanceForOrder = async ({
caType,
accountId,
actorOrgId,
profileId,
orderId,
csr,
finalizingOrder,
certificateRequest,
profile,
ca,
tx
}: {
caType: CaType;
accountId: string;
actorOrgId: string;
profileId: string;
orderId: string;
csr: string;
finalizingOrder: {
notBefore?: Date | null;
notAfter?: Date | null;
};
certificateRequest: ReturnType<typeof extractCertificateRequestFromCSR>;
profile: TCertificateProfileWithConfigs;
ca: Awaited<ReturnType<typeof certificateAuthorityDAL.findByIdWithAssociatedCa>>;
tx?: Knex;
}): Promise<{ certificateId?: string; certIssuanceJobData?: TIssueCertificateFromProfileJobData }> => {
if (caType === CaType.INTERNAL) {
const result = await certificateV3Service.signCertificateFromProfile({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
validity: !finalizingOrder.notAfter
? {
// 47 days, the default TTL comes with Let's Encrypt
// TODO: read config from the profile to get the expiration time instead
ttl: `${47}d`
}
: // ttl is not used if notAfter is provided
({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
return {
certificateId: result.certificateId
};
}
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
extractAlgorithmsFromCSR(csr);
const updatedCertificateRequest = {
...certificateRequest,
keyAlgorithm: extractedKeyAlgorithm,
signatureAlgorithm: extractedSignatureAlgorithm,
validity: finalizingOrder.notAfter
? (() => {
const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date();
const notAfter = new Date(finalizingOrder.notAfter);
const diffMs = notAfter.getTime() - notBefore.getTime();
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
return { ttl: `${diffDays}d` };
})()
: certificateRequest.validity
};
const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
template.id,
updatedCertificateRequest
);
if (!validationResult.isValid) {
throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` });
}
const certRequest = await certificateRequestService.createCertificateRequest({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
projectId: profile.projectId,
caId: ca.id,
profileId: profile.id,
commonName: updatedCertificateRequest.commonName ?? "",
keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [],
extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [],
keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "",
signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "",
altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","),
notBefore: updatedCertificateRequest.notBefore,
notAfter: updatedCertificateRequest.notAfter,
status: CertificateRequestStatus.PENDING,
acmeOrderId: orderId,
csr,
tx
});
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem");
return {
certIssuanceJobData: {
certificateId: orderId,
profileId: profile.id,
caId: profile.caId || "",
ttl: updatedCertificateRequest.validity?.ttl || "1y",
signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "",
keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "",
commonName: updatedCertificateRequest.commonName || "",
altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [],
extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [],
certificateRequestId: certRequest.id,
csr: csrPem
}
};
};
const finalizeAcmeOrder = async ({ const finalizeAcmeOrder = async ({
profileId, profileId,
accountId, accountId,
@@ -768,7 +937,11 @@ export const pkiAcmeServiceFactory = ({
throw new NotFoundError({ message: "ACME order not found" }); throw new NotFoundError({ message: "ACME order not found" });
} }
if (order.status === AcmeOrderStatus.Ready) { if (order.status === AcmeOrderStatus.Ready) {
const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => { const {
order: updatedOrder,
error,
certIssuanceJobData
} = await acmeOrderDAL.transaction(async (tx) => {
const finalizingOrder = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!; const finalizingOrder = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!;
// TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead? // TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead?
const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!; const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!;
@@ -815,94 +988,31 @@ export const pkiAcmeServiceFactory = ({
} }
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
let errorToReturn: Error | undefined; let errorToReturn: Error | undefined;
let certIssuanceJobDataToReturn: TIssueCertificateFromProfileJobData | undefined;
try { try {
const { certificateId } = await (async () => { const result = await processCertificateIssuanceForOrder({
if (caType === CaType.INTERNAL) { caType,
const result = await certificateV3Service.signCertificateFromProfile({ accountId,
actor: ActorType.ACME_ACCOUNT, actorOrgId,
actorId: accountId, profileId,
actorAuthMethod: null, orderId,
actorOrgId, csr,
profileId, finalizingOrder,
csr, certificateRequest,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, profile,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, ca,
validity: !finalizingOrder.notAfter tx
? { });
// 47 days, the default TTL comes with Let's Encrypt
// TODO: read config from the profile to get the expiration time instead
ttl: `${47}d`
}
: // ttl is not used if notAfter is provided
({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
return { certificateId: result.certificateId };
}
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem");
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
extractAlgorithmsFromCSR(csr);
certificateRequest.keyAlgorithm = extractedKeyAlgorithm;
certificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
if (finalizingOrder.notAfter) {
const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date();
const notAfter = new Date(finalizingOrder.notAfter);
const diffMs = notAfter.getTime() - notBefore.getTime();
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
certificateRequest.validity = { ttl: `${diffDays}d` };
}
const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
template.id,
certificateRequest
);
if (!validationResult.isValid) {
throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` });
}
// TODO: this is pretty slow, and we are holding the transaction open for a long time,
// we should queue the certificate issuance to a background job instead
const cert = await orderCertificate(
{
caId: certificateAuthority!.id,
// It is possible that the CSR does not have a common name, in which case we use an empty string
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
commonName: certificateRequest.commonName ?? "",
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
csr: Buffer.from(csrPem),
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
return { certificateId: cert.id };
})();
await acmeOrderDAL.updateById( await acmeOrderDAL.updateById(
orderId, orderId,
{ {
status: AcmeOrderStatus.Valid, status: result.certificateId ? AcmeOrderStatus.Valid : AcmeOrderStatus.Processing,
csr, csr,
certificateId certificateId: result.certificateId
}, },
tx tx
); );
certIssuanceJobDataToReturn = result.certIssuanceJobData;
} catch (exp) { } catch (exp) {
await acmeOrderDAL.updateById( await acmeOrderDAL.updateById(
orderId, orderId,
@@ -920,17 +1030,25 @@ export const pkiAcmeServiceFactory = ({
} else if (exp instanceof AcmeError) { } else if (exp instanceof AcmeError) {
errorToReturn = exp; errorToReturn = exp;
} else { } else {
errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" }); errorToReturn = new AcmeServerInternalError({
message: "Failed to sign certificate with internal error"
});
} }
} }
return { return {
order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!, order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!,
error: errorToReturn error: errorToReturn,
certIssuanceJobData: certIssuanceJobDataToReturn
}; };
}); });
if (error) { if (error) {
throw error; throw error;
} }
if (certIssuanceJobData) {
// TODO: ideally, this should be done inside the transaction, but the pg-boss queue doesn't support external transactions
// as it seems to be. we need to commit the transaction before queuing the job, otherwise the job will fail (not found error).
await certificateIssuanceQueue.queueCertificateIssuance(certIssuanceJobData);
}
order = updatedOrder; order = updatedOrder;
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId: profile.projectId, projectId: profile.projectId,
@@ -976,14 +1094,16 @@ export const pkiAcmeServiceFactory = ({
if (!order) { if (!order) {
throw new NotFoundError({ message: "ACME order not found" }); throw new NotFoundError({ message: "ACME order not found" });
} }
if (order.status !== AcmeOrderStatus.Valid) { // Sync order first in case if there is a certificate request that needs to be processed
const syncedOrder = await checkAndSyncAcmeOrderStatus({ orderId });
if (syncedOrder.status !== AcmeOrderStatus.Valid) {
throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" }); throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" });
} }
if (!order.certificateId) { if (!syncedOrder.certificateId) {
throw new NotFoundError({ message: "The certificate for this ACME order no longer exists" }); throw new NotFoundError({ message: "The certificate for this ACME order no longer exists" });
} }
const certBody = await certificateBodyDAL.findOne({ certId: order.certificateId }); const certBody = await certificateBodyDAL.findOne({ certId: syncedOrder.certificateId });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: profile.projectId, projectId: profile.projectId,
projectDAL, projectDAL,

View File

@@ -2314,13 +2314,9 @@ export const registerRoutes = async (
const pkiAcmeService = pkiAcmeServiceFactory({ const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL, projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
certificateTemplateV2DAL, certificateTemplateV2DAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
@@ -2332,6 +2328,8 @@ export const registerRoutes = async (
licenseService, licenseService,
certificateV3Service, certificateV3Service,
certificateTemplateV2Service, certificateTemplateV2Service,
certificateRequestService,
certificateIssuanceQueue,
acmeChallengeService, acmeChallengeService,
pkiAcmeQueueService, pkiAcmeQueueService,
auditLogService auditLogService

View File

@@ -91,6 +91,7 @@ export const certificateRequestServiceFactory = ({
permissionService permissionService
}: TCertificateRequestServiceFactoryDep) => { }: TCertificateRequestServiceFactoryDep) => {
const createCertificateRequest = async ({ const createCertificateRequest = async ({
acmeOrderId,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -123,6 +124,7 @@ export const certificateRequestServiceFactory = ({
{ {
status, status,
projectId, projectId,
acmeOrderId,
...validatedData ...validatedData
}, },
tx tx

View File

@@ -21,6 +21,7 @@ export type TCreateCertificateRequestDTO = TProjectPermission & {
metadata?: string; metadata?: string;
status: CertificateRequestStatus; status: CertificateRequestStatus;
certificateId?: string; certificateId?: string;
acmeOrderId?: string;
}; };
export type TGetCertificateRequestDTO = TProjectPermission & { export type TGetCertificateRequestDTO = TProjectPermission & {