From 04d553f052b974b2696dbf7034c24d1537b028c3 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 16 May 2025 20:38:08 +0800 Subject: [PATCH] misc: moved cert issuance to job --- backend/src/keystore/keystore.ts | 2 + backend/src/queue/queue-service.ts | 12 ++- backend/src/server/routes/index.ts | 96 ++++++++++-------- .../server/routes/v1/pki-subscriber-router.ts | 58 +++++------ .../acme/acme-certificate-authority-fns.ts | 99 +++++++++++++------ .../acme-certificate-authority-schemas.ts | 4 + .../certificate-authority-dal.ts | 8 +- .../certificate-authority-queue.ts | 98 +++++++++++++++++- .../certificate-authority-service.ts | 28 +++++- .../internal-certificate-authority-types.ts | 7 +- .../src/services/pki-alert/pki-alert-dal.ts | 1 + .../pki-subscriber/pki-subscriber-schema.ts | 4 + .../pki-subscriber/pki-subscriber-service.ts | 59 ++++------- .../src/hooks/api/pkiSubscriber/index.tsx | 1 + .../src/hooks/api/pkiSubscriber/mutations.tsx | 14 +++ frontend/src/hooks/api/pkiSubscriber/types.ts | 6 ++ .../PkiSubscriberDetailsSection.tsx | 45 ++++++--- 17 files changed, 373 insertions(+), 169 deletions(-) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 6da6c4fa4..49128b18a 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -37,6 +37,8 @@ export const KeyStorePrefixes = { `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const, SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const, + CaOrderCertificateForSubscriberLock: (subscriberId: string) => + `ca-order-certificate-for-subscriber-lock-${subscriberId}` as const, SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const, IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => `identity-access-token-status:${identityAccessTokenId}`, diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 807d6c286..8a0248f38 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -14,6 +14,7 @@ import { } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { TFailedIntegrationSyncEmailsPayload, TIntegrationSyncPayload, @@ -44,6 +45,7 @@ export enum QueueName { UpgradeProjectToGhost = "upgrade-project-to-ghost", DynamicSecretRevocation = "dynamic-secret-revocation", CaCrlRotation = "ca-crl-rotation", + CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber SecretReplication = "secret-replication", SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication ProjectV3Migration = "project-v3-migration", @@ -84,7 +86,8 @@ export enum QueueJobs { SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", - InvalidateCache = "invalidate-cache" + InvalidateCache = "invalidate-cache", + CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber" } export type TQueueJobTypes = { @@ -245,6 +248,13 @@ export type TQueueJobTypes = { }; }; }; + [QueueName.CaLifecycle]: { + name: QueueJobs.CaOrderCertificateForSubscriber; + payload: { + subscriberId: string; + caType: CaType; + }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 738f37341..ae6b9e512 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -850,16 +850,6 @@ export const registerRoutes = async ( permissionService }); - const certificateAuthorityQueue = certificateAuthorityQueueFactory({ - certificateAuthorityCrlDAL, - certificateAuthorityDAL, - certificateAuthoritySecretDAL, - certificateDAL, - projectDAL, - kmsService, - queueService - }); - const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({ sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, @@ -906,24 +896,6 @@ export const registerRoutes = async ( groupDAL }); - const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({ - certificateAuthorityDAL, - certificateAuthorityCertDAL, - certificateAuthoritySecretDAL, - certificateAuthorityCrlDAL, - certificateTemplateDAL, - certificateAuthorityQueue, - certificateDAL, - certificateBodyDAL, - certificateSecretDAL, - pkiCollectionDAL, - pkiCollectionItemDAL, - projectDAL, - internalCertificateAuthorityDAL, - kmsService, - permissionService - }); - const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({ certificateAuthorityDAL, certificateAuthorityCrlDAL, @@ -943,17 +915,6 @@ export const registerRoutes = async ( licenseService }); - const certificateEstService = certificateEstServiceFactory({ - internalCertificateAuthorityService, - certificateTemplateService, - certificateTemplateDAL, - certificateAuthorityCertDAL, - certificateAuthorityDAL, - projectDAL, - kmsService, - licenseService - }); - const pkiAlertService = pkiAlertServiceFactory({ pkiAlertDAL, pkiCollectionDAL, @@ -1627,6 +1588,52 @@ export const registerRoutes = async ( keyStore }); + const certificateAuthorityQueue = certificateAuthorityQueueFactory({ + certificateAuthorityCrlDAL, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + certificateDAL, + projectDAL, + kmsService, + queueService, + pkiSubscriberDAL, + certificateBodyDAL, + certificateSecretDAL, + externalCertificateAuthorityDAL, + keyStore, + appConnectionDAL, + appConnectionService + }); + + const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({ + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateTemplateDAL, + certificateAuthorityQueue, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + pkiCollectionDAL, + pkiCollectionItemDAL, + projectDAL, + internalCertificateAuthorityDAL, + kmsService, + permissionService + }); + + const certificateEstService = certificateEstServiceFactory({ + internalCertificateAuthorityService, + certificateTemplateService, + certificateTemplateDAL, + certificateAuthorityCertDAL, + certificateAuthorityDAL, + projectDAL, + kmsService, + licenseService + }); + const kmipService = kmipServiceFactory({ kmipClientDAL, permissionService, @@ -1673,7 +1680,12 @@ export const registerRoutes = async ( appConnectionDAL, appConnectionService, externalCertificateAuthorityDAL, - internalCertificateAuthorityService + internalCertificateAuthorityService, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL }); const pkiSubscriberService = pkiSubscriberServiceFactory({ @@ -1688,9 +1700,7 @@ export const registerRoutes = async ( projectDAL, kmsService, permissionService, - appConnectionDAL, - appConnectionService, - externalCertificateAuthorityDAL + certificateAuthorityQueue }); await secretRotationV2QueueServiceFactory({ diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index fec4d5676..127fc8404 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -288,7 +288,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => schema: { hide: false, tags: [ApiDocsTags.PkiSubscribers], - description: "Issue certificate", + description: "Order certificate", params: z.object({ subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName) }), @@ -297,16 +297,12 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate), - issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate), - certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain), - privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey), - serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber) + message: z.string().trim() }) } }, handler: async (req) => { - await server.services.pkiSubscriber.issueSubscriberCert({ + const subscriber = await server.services.pkiSubscriber.orderSubscriberCert({ subscriberName: req.params.subscriberName, projectId: req.body.projectId, actor: req.permission.type, @@ -315,35 +311,31 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId }); - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: subscriber.projectId, - event: { - type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, - metadata: { - subscriberId: subscriber.id, - name: subscriber.name, - serialNumber - } - } - }); + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: subscriber.projectId, + // event: { + // type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, + // metadata: { + // subscriberId: subscriber.id, + // name: subscriber.name, + // serialNumber + // } + // } + // }); - await server.services.telemetry.sendPostHogEvents({ - event: PostHogEventTypes.IssueCert, - distinctId: getTelemetryDistinctId(req), - properties: { - subscriberId: subscriber.id, - commonName: subscriber.commonName, - ...req.auditLogInfo - } - }); + // await server.services.telemetry.sendPostHogEvents({ + // event: PostHogEventTypes.IssueCert, + // distinctId: getTelemetryDistinctId(req), + // properties: { + // subscriberId: subscriber.id, + // commonName: subscriber.commonName, + // ...req.auditLogInfo + // } + // }); return { - certificate, - certificateChain, - issuingCaCertificate, - privateKey, - serialNumber + message: "Successfully placed order for certificate" }; } }); diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 1480e8b02..f78697e26 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -3,11 +3,12 @@ import * as x509 from "@peculiar/x509"; import acme from "acme-client"; import { KeyObject } from "crypto"; -import { TableName, TPkiSubscribers } from "@app/db/schemas"; +import { TableName } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; @@ -21,6 +22,7 @@ import { CertStatus } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -30,6 +32,7 @@ import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { TCertificateAuthority } from "../certificate-authority-types"; import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; import { AcmeDnsProvider } from "./acme-certificate-authority-enums"; +import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas"; import { TAcmeCertificateAuthority, TCreateAcmeCertificateAuthorityDTO, @@ -47,7 +50,11 @@ type TAcmeCertificateAuthorityFnsDeps = { certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; + pkiSubscriberDAL: Pick; projectDAL: Pick; }; @@ -59,7 +66,7 @@ type DBConfigurationColumn = { export const castDbEntryToAcmeCertificateAuthority = ( ca: Awaited> -): TAcmeCertificateAuthority => { +): TAcmeCertificateAuthority & { credentials: unknown } => { if (!ca.externalCa) { throw new BadRequestError({ message: "Malformed ACME certificate authority" }); } @@ -72,6 +79,7 @@ export const castDbEntryToAcmeCertificateAuthority = ( disableDirectIssuance: ca.disableDirectIssuance, name: ca.externalCa.name, projectId: ca.projectId, + credentials: ca.externalCa.credentials, configuration: { dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string, dnsProvider: dbConfigurationCol.dnsProvider as AcmeDnsProvider, @@ -147,7 +155,8 @@ export const AcmeCertificateAuthorityFns = ({ certificateBodyDAL, certificateSecretDAL, kmsService, - projectDAL + projectDAL, + pkiSubscriberDAL }: TAcmeCertificateAuthorityFnsDeps) => { const createCertificateAuthority = async ({ name, @@ -320,20 +329,66 @@ export const AcmeCertificateAuthorityFns = ({ return cas.map(castDbEntryToAcmeCertificateAuthority); }; - // SHEEN TODO: need to execute this from a job - const orderCertificate = async ( - subscriber: TPkiSubscribers, - ca: Awaited>, - actor: OrgServiceActor - ) => { + const orderCertificate = async (subscriberId: string) => { + const subscriber = await pkiSubscriberDAL.findById(subscriberId); + if (!subscriber.caId) { + throw new BadRequestError({ message: "Subscriber does not have a CA" }); + } + + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); + if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) { + throw new BadRequestError({ message: "CA is not an ACME CA" }); + } + const acmeCa = castDbEntryToAcmeCertificateAuthority(ca); - // SHEEN TODO: need to save this in credentials field and reuse - const privateRsaKey = await acme.crypto.createPrivateRsaKey(); + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + let accountKey: Buffer | undefined; + if (acmeCa.credentials) { + const decryptedCredentials = await kmsDecryptor({ + cipherTextBlob: acmeCa.credentials as Buffer + }); + + const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync( + JSON.parse(decryptedCredentials.toString("utf8")) + ); + + accountKey = Buffer.from(parsedCredentials.accountKey, "base64"); + } + if (!accountKey) { + accountKey = await acme.crypto.createPrivateRsaKey(); + const newCredentials = { + accountKey: accountKey.toString("base64") + }; + const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({ + plainText: Buffer.from(JSON.stringify(newCredentials)) + }); + await externalCertificateAuthorityDAL.update( + { + certificateAuthorityId: acmeCa.id + }, + { + credentials: encryptedNewCredentials + } + ); + } const acmeClient = new acme.Client({ directoryUrl: acmeCa.configuration.directoryUrl, - accountKey: privateRsaKey + accountKey }); const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); @@ -349,13 +404,8 @@ export const AcmeCertificateAuthorityFns = ({ skLeaf ); - // SHEEN TODO: need to update this to remove dependence on ACTOR const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId); - const connection = await appConnectionService.connectAppConnectionById( - appConnection.app as AppConnection, - acmeCa.configuration.dnsAppConnectionId, - actor - ); + const connection = await decryptAppConnection(appConnection, kmsService); const pem = await acmeClient.auto({ csr: certificateCsr, @@ -385,20 +435,9 @@ export const AcmeCertificateAuthorityFns = ({ } }); - console.log("PEM IS", pem); - const [leafCert, parentCert] = acme.crypto.splitPemChain(pem); const certObj = new x509.X509Certificate(leafCert); - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: ca.projectId, - projectDAL, - kmsService - }); - - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(certObj.rawData)) }); diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts index 755346600..802edca76 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts @@ -15,6 +15,10 @@ export const AcmeCertificateAuthorityConfigurationSchema = z.object({ accountEmail: z.string().trim() }); +export const AcmeCertificateAuthorityCredentialsSchema = z.object({ + accountKey: z.string() +}); + export const AcmeCertificateAuthoritySchema = BaseCertificateAuthoritySchema(CaType.ACME).extend({ type: z.literal(CaType.ACME), configuration: AcmeCertificateAuthorityConfigurationSchema diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 2126ead6e..1e81e0a05 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -58,6 +58,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"), db.ref("status").withSchema(TableName.ExternalCertificateAuthority).as("externalStatus"), db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"), + db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"), db .ref("dnsAppConnectionId") .withSchema(TableName.ExternalCertificateAuthority) @@ -99,7 +100,8 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { status: result.externalStatus, configuration: result.externalConfiguration, dnsAppConnectionId: result.externalDnsAppConnectionId, - appConnectionId: result.externalAppConnectionId + appConnectionId: result.externalAppConnectionId, + credentials: result.externalCredentials } : undefined }; @@ -195,6 +197,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { .ref("dnsAppConnectionId") .withSchema(TableName.ExternalCertificateAuthority) .as("externalDnsAppConnectionId"), + db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"), db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") ); @@ -243,7 +246,8 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { status: ca.externalStatus, configuration: ca.externalConfiguration, dnsAppConnectionId: ca.externalDnsAppConnectionId, - appConnectionId: ca.externalAppConnectionId + appConnectionId: ca.externalAppConnectionId, + credentials: ca.externalCredentials } : undefined })); diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index b99a20330..32246de73 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -1,6 +1,7 @@ import * as x509 from "@peculiar/x509"; import crypto from "crypto"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { NotFoundError } from "@app/lib/errors"; @@ -13,21 +14,42 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; +import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; +import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { CaType } from "./certificate-authority-enums"; import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; -import { TRotateCaCrlTriggerDTO } from "./internal/internal-certificate-authority-types"; +import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal"; +import { + TOrderCertificateForSubscriberDTO, + TRotateCaCrlTriggerDTO +} from "./internal/internal-certificate-authority-types"; type TCertificateAuthorityQueueFactoryDep = { - // TODO: Pick certificateAuthorityDAL: TCertificateAuthorityDALFactory; + appConnectionDAL: Pick; + appConnectionService: Pick; + externalCertificateAuthorityDAL: Pick; + keyStore: Pick; certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory; certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory; certificateDAL: TCertificateDALFactory; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey" + >; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; queueService: TQueueServiceFactory; + pkiSubscriberDAL: Pick; }; + export type TCertificateAuthorityQueueFactory = ReturnType; export const certificateAuthorityQueueFactory = ({ @@ -37,8 +59,28 @@ export const certificateAuthorityQueueFactory = ({ certificateDAL, projectDAL, kmsService, - queueService + queueService, + keyStore, + appConnectionDAL, + appConnectionService, + externalCertificateAuthorityDAL, + certificateBodyDAL, + certificateSecretDAL, + pkiSubscriberDAL }: TCertificateAuthorityQueueFactoryDep) => { + const acmeFns = AcmeCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL + }); + // TODO 1: auto-periodic rotation // TODO 2: manual rotation @@ -71,6 +113,51 @@ export const certificateAuthorityQueueFactory = ({ ); }; + const orderCertificateForSubscriber = async ({ subscriberId, caType }: TOrderCertificateForSubscriberDTO) => { + await queueService.queue( + QueueName.CaLifecycle, + QueueJobs.CaOrderCertificateForSubscriber, + { + subscriberId, + caType + }, + { + attempts: 1, + removeOnComplete: true, + removeOnFail: true + } + ); + }; + + queueService.start(QueueName.CaLifecycle, async (job) => { + if (job.name === QueueJobs.CaOrderCertificateForSubscriber) { + const { subscriberId, caType } = job.data; + let lock: Awaited>; + + try { + lock = await keyStore.acquireLock( + [KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)], + // scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked, + // but should always release below... + 5 * 60 * 1000 + ); + } catch (e) { + logger.info(`CaOrderCertificate Failed to acquire lock [subscriberId=${subscriberId}] [job=${job.name}]`); + return; + } + + try { + if (caType === CaType.ACME) { + await acmeFns.orderCertificate(subscriberId); + } + } catch (e) { + logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`); + } finally { + await lock.release(); + } + } + }); + queueService.start(QueueName.CaCrlRotation, async (job) => { const { caId } = job.data; logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`); @@ -144,6 +231,7 @@ export const certificateAuthorityQueueFactory = ({ }); return { - setCaCrlRotationInterval + setCaCrlRotationInterval, + orderCertificateForSubscriber }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index b355eb13c..8d0894ede 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -8,6 +8,11 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; +import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import { TCertificateDALFactory } from "../certificate/certificate-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; import { @@ -46,6 +51,14 @@ type TCertificateAuthorityServiceFactoryDep = { "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId" >; permissionService: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; + pkiSubscriberDAL: Pick; }; export type TCertificateAuthorityServiceFactory = ReturnType; @@ -57,13 +70,24 @@ export const certificateAuthorityServiceFactory = ({ internalCertificateAuthorityService, appConnectionDAL, appConnectionService, - externalCertificateAuthorityDAL + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL }: TCertificateAuthorityServiceFactoryDep) => { const acmeFns = AcmeCertificateAuthorityFns({ appConnectionDAL, appConnectionService, certificateAuthorityDAL, - externalCertificateAuthorityDAL + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL }); const createCertificateAuthority = async ( diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index f74c83406..206db0cd6 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -9,7 +9,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; -import { CaRenewalType, CaStatus, InternalCaType } from "../certificate-authority-enums"; +import { CaRenewalType, CaStatus, CaType, InternalCaType } from "../certificate-authority-enums"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { CreateInternalCertificateAuthoritySchema, @@ -212,3 +212,8 @@ export type TRotateCaCrlTriggerDTO = { caId: string; rotationIntervalDays: number; }; + +export type TOrderCertificateForSubscriberDTO = { + subscriberId: string; + caType: CaType; +}; diff --git a/backend/src/services/pki-alert/pki-alert-dal.ts b/backend/src/services/pki-alert/pki-alert-dal.ts index d4d4fa987..eddce1570 100644 --- a/backend/src/services/pki-alert/pki-alert-dal.ts +++ b/backend/src/services/pki-alert/pki-alert-dal.ts @@ -25,6 +25,7 @@ export const pkiAlertDALFactory = (db: TDbClient) => { recipientEmails: string; }; + // SHEEN TODO: FIX REGRESION HERE // gets CAs and certificates as part of PKI collection items const combinedQuery = db .replicaNode() diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts index 7ffeea3fa..c062b6406 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + import { PkiSubscribersSchema } from "@app/db/schemas"; export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ @@ -11,4 +13,6 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ ttl: true, keyUsages: true, extendedKeyUsages: true +}).extend({ + supportsImmediateCertIssuance: z.boolean().optional() }); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 6f84c65ae..bd1a14e50 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -38,11 +38,8 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; -import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; -import { AcmeCertificateAuthorityFns } from "../certificate-authority/acme/acme-certificate-authority-fns"; -import { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal"; +import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue"; import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns"; import { PkiSubscriberStatus, @@ -57,9 +54,6 @@ import { } from "./pki-subscriber-types"; type TPkiSubscriberServiceFactoryDep = { - appConnectionDAL: Pick; - appConnectionService: Pick; - externalCertificateAuthorityDAL: Pick; pkiSubscriberDAL: Pick< TPkiSubscriberDALFactory, "create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne" @@ -70,6 +64,7 @@ type TPkiSubscriberServiceFactoryDep = { >; certificateAuthorityCertDAL: Pick; certificateAuthoritySecretDAL: Pick; + certificateAuthorityQueue: Pick; certificateAuthorityCrlDAL: Pick; certificateDAL: Pick; certificateSecretDAL: Pick; @@ -93,9 +88,7 @@ export const pkiSubscriberServiceFactory = ({ projectDAL, kmsService, permissionService, - appConnectionDAL, - appConnectionService, - externalCertificateAuthorityDAL + certificateAuthorityQueue }: TPkiSubscriberServiceFactoryDep) => { const internalCaFns = InternalCertificateAuthorityFns({ certificateAuthorityDAL, @@ -109,18 +102,6 @@ export const pkiSubscriberServiceFactory = ({ kmsService }); - const acmeCaFns = AcmeCertificateAuthorityFns({ - appConnectionDAL, - appConnectionService, - certificateAuthorityDAL, - externalCertificateAuthorityDAL, - certificateDAL, - certificateBodyDAL, - certificateSecretDAL, - kmsService, - projectDAL - }); - const createSubscriber = async ({ name, commonName, @@ -198,7 +179,18 @@ export const pkiSubscriberServiceFactory = ({ }) ); - return subscriber; + let supportsImmediateCertIssuance = false; + if (subscriber.caId) { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); + if (ca.internalCa?.id) { + supportsImmediateCertIssuance = true; + } + } + + return { + ...subscriber, + supportsImmediateCertIssuance + }; }; const updateSubscriber = async ({ @@ -325,16 +317,16 @@ export const pkiSubscriberServiceFactory = ({ const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); if (ca.internalCa?.id) { - throw new BadRequestError({ message: "CA does not support ordering certificates" }); + throw new BadRequestError({ message: "CA does not support ordering of certificates" }); } if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) { - return acmeCaFns.orderCertificate(subscriber, ca, { - type: actor, - id: actorId, - authMethod: actorAuthMethod, - orgId: actorOrgId + await certificateAuthorityQueue.orderCertificateForSubscriber({ + subscriberId: subscriber.id, + caType: ca.externalCa.type }); + + return subscriber; } throw new BadRequestError({ message: "Unsupported CA type" }); @@ -380,15 +372,6 @@ export const pkiSubscriberServiceFactory = ({ return internalCaFns.issueCertificate(subscriber, ca); } - if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) { - return acmeCaFns.orderCertificate(subscriber, ca, { - type: actor, - id: actorId, - authMethod: actorAuthMethod, - orgId: actorOrgId - }); - } - throw new BadRequestError({ message: "CA does not support immediate issuance of certificates" }); }; diff --git a/frontend/src/hooks/api/pkiSubscriber/index.tsx b/frontend/src/hooks/api/pkiSubscriber/index.tsx index b086839df..40ab88df1 100644 --- a/frontend/src/hooks/api/pkiSubscriber/index.tsx +++ b/frontend/src/hooks/api/pkiSubscriber/index.tsx @@ -2,6 +2,7 @@ export { useCreatePkiSubscriber, useDeletePkiSubscriber, useIssuePkiSubscriberCert, + useOrderPkiSubscriberCert, useUpdatePkiSubscriber } from "./mutations"; export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries"; diff --git a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx index a7d0eef92..b30924f97 100644 --- a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx +++ b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx @@ -108,3 +108,17 @@ export const useIssuePkiSubscriberCert = () => { } }); }; + +export const useOrderPkiSubscriberCert = () => { + return useMutation<{ message: string }, object, TIssuePkiSubscriberCertDTO>({ + mutationFn: async ({ subscriberName, projectId }) => { + const { data } = await apiRequest.post( + `/api/v1/pki/subscribers/${subscriberName}/order-certificate`, + { + projectId + } + ); + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts index e6050dd13..6b477f9bd 100644 --- a/frontend/src/hooks/api/pkiSubscriber/types.ts +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -16,6 +16,7 @@ export type TPkiSubscriber = { subjectAlternativeNames: string[]; keyUsages: CertKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[]; + supportsImmediateCertIssuance?: boolean; }; export type TCreatePkiSubscriberDTO = { @@ -51,3 +52,8 @@ export type TIssuePkiSubscriberCertDTO = { subscriberName: string; projectId: string; }; + +export type TOrderPkiSubscriberCertDTO = { + subscriberName: string; + projectId: string; +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx index 5899f5004..cb403b12d 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -13,7 +13,11 @@ import { useWorkspace } from "@app/context"; import { useTimedReset } from "@app/hooks"; -import { useGetPkiSubscriber, useIssuePkiSubscriberCert } from "@app/hooks/api"; +import { + useGetPkiSubscriber, + useIssuePkiSubscriberCert, + useOrderPkiSubscriberCert +} from "@app/hooks/api"; import { pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -49,23 +53,34 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } = useIssuePkiSubscriberCert(); + const { mutateAsync: orderPkiSubscriberCert } = useOrderPkiSubscriberCert(); + const onIssuePkiSubscriberCert = async () => { try { - const response = await issuePkiSubscriberCert({ subscriberName, projectId }); + if (pkiSubscriber?.supportsImmediateCertIssuance) { + const response = await issuePkiSubscriberCert({ subscriberName, projectId }); - setCertificateDetails({ - serialNumber: response.serialNumber, - certificate: response.certificate, - certificateChain: response.certificateChain, - privateKey: response.privateKey - }); + setCertificateDetails({ + serialNumber: response.serialNumber, + certificate: response.certificate, + certificateChain: response.certificateChain, + privateKey: response.privateKey + }); - setIsModalOpen(true); + setIsModalOpen(true); - createNotification({ - text: "Successfully issued certificate", - type: "success" - }); + createNotification({ + text: "Successfully issued certificate", + type: "success" + }); + } else { + await orderPkiSubscriberCert({ subscriberName, projectId }); + + createNotification({ + text: "Successfully ordered certificate. It will be issued after CA processing.", + type: "success" + }); + } } catch (err) { console.error(err); createNotification({ @@ -158,7 +173,9 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: onIssuePkiSubscriberCert(); }} > - Issue Certificate + {pkiSubscriber?.supportsImmediateCertIssuance + ? "Issue Certificate" + : "Order Certificate"} )}