issuing cert

This commit is contained in:
Fang-Pen Lin
2025-10-31 20:16:26 -07:00
parent f3852638e5
commit b4bd05cbd0
6 changed files with 82 additions and 6 deletions

View File

@@ -19,3 +19,4 @@ Feature: Challenge
Then I select challenge with type http-01 for domain localhost from order at order as challenge
Then I serve challenge response for challenge at localhost
Then I tell ACME server that challenge is ready to be verified
Then I poll and finalize the ACME order order

View File

@@ -85,6 +85,12 @@ export async function up(knex: Knex): Promise<void> {
t.timestamp("expiresAt").notNullable();
t.string("csr").nullable();
t.string("certificate").nullable();
t.string("certificateChain").nullable();
t.string("error").nullable();
// Order status
t.string("status").notNullable(); // pending, ready, processing, valid, invalid
@@ -160,6 +166,9 @@ export async function up(knex: Knex): Promise<void> {
// Challenge status
t.string("status").notNullable(); // pending, processing, valid, invalid
// Error message when the challenge fails
t.string("error").nullable();
// Validation timestamp
t.timestamp("validatedAt").nullable();

View File

@@ -1,14 +1,12 @@
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors";
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type TPkiAcmeChallengeServiceFactoryDep = {
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "updateById">;
acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory,
"transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById"
@@ -16,7 +14,6 @@ type TPkiAcmeChallengeServiceFactoryDep = {
};
export const pkiAcmeChallengeServiceFactory = ({
acmeAuthDAL,
acmeChallengeDAL
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig();

View File

@@ -507,3 +507,24 @@ export class AcmeDnsFailureError extends AcmeError {
this.name = "AcmeDnsFailureError";
}
}
export class AcmeOrderNotReadyError extends AcmeError {
constructor({
detail = "The order is not ready",
error,
message
}: {
detail?: string;
error?: unknown;
message?: string;
} = {}) {
super({
type: AcmeErrorType.OrderNotReady,
detail,
status: 403,
error,
message
});
this.name = "AcmeOrderNotReadyError";
}
}

View File

@@ -10,6 +10,15 @@ export type TPkiAcmeOrderDALFactory = ReturnType<typeof pkiAcmeOrderDALFactory>;
export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
const pkiAcmeOrderOrm = ormify(db, TableName.PkiAcmeOrder);
const findByIdForFinalization = async (id: string, tx?: Knex) => {
try {
const order = await (tx || db)(TableName.PkiAcmeOrder).forUpdate().where({ id }).first();
return order || null;
} catch (error) {
throw new DatabaseError({ error, name: "Find PKI ACME order by id for finalization" });
}
};
const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => {
try {
const rows = await (tx || db)(TableName.PkiAcmeOrder)
@@ -53,6 +62,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
return {
...pkiAcmeOrderOrm,
findByIdForFinalization,
findByAccountAndOrderIdWithAuthorizations
};
};

View File

@@ -6,6 +6,7 @@ import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import {
EnrollmentType,
TCertificateProfileWithConfigs
@@ -27,6 +28,7 @@ import {
AcmeBadPublicKeyError,
AcmeError,
AcmeMalformedError,
AcmeOrderNotReadyError,
AcmeServerInternalError,
AcmeUnauthorizedError,
AcmeUnsupportedIdentifierError
@@ -64,11 +66,15 @@ import {
type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
>;
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">;
acmeOrderDAL: Pick<
TPkiAcmeOrderDALFactory,
"create" | "transaction" | "updateById" | "findByAccountAndOrderIdWithAuthorizations" | "findByIdForFinalization"
>;
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">;
acmeChallengeDAL: Pick<
@@ -80,6 +86,7 @@ type TPkiAcmeServiceFactoryDep = {
export const pkiAcmeServiceFactory = ({
certificateProfileDAL,
internalCertificateAuthorityService,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
@@ -497,8 +504,39 @@ export const pkiAcmeServiceFactory = ({
if (!order) {
throw new NotFoundError({ message: "ACME order not found" });
}
const { csr } = payload;
// FIXME: Implement ACME finalize order
if (order.status === AcmeOrderStatus.Ready) {
await acmeOrderDAL.transaction(async (tx) => {
const order = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!;
const profile = (await certificateProfileDAL.findById(profileId, tx))!;
if (order.status !== AcmeOrderStatus.Ready) {
throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" });
}
if (order.expiresAt < new Date()) {
throw new AcmeOrderNotReadyError({ message: "ACME order has expired" });
}
const { csr } = payload;
// TODO: validate the CSR and return badCSR error if it's invalid
const { certificate, certificateChain } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId: profile.certificateTemplateId,
csr,
notBefore: order.notBefore?.toISOString(),
notAfter: order.notAfter?.toISOString()
});
await acmeOrderDAL.updateById(
orderId,
{
status: AcmeOrderStatus.Valid,
csr,
certificate,
certificateChain
},
tx
);
});
} else if (order.status !== AcmeOrderStatus.Valid) {
throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" });
}
return {
status: 200,
body: buildAcmeOrderResource({ profileId, order }),