mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 10:10:30 +00:00
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:
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 & {
|
||||||
|
|||||||
Reference in New Issue
Block a user