misc: moved cert issuance to job

This commit is contained in:
Sheen Capadngan
2025-05-16 20:38:08 +08:00
parent 6d10afc9d2
commit 04d553f052
17 changed files with 373 additions and 169 deletions

View File

@@ -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}`,

View File

@@ -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<typeof queueServiceFactory>;

View File

@@ -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({

View File

@@ -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"
};
}
});

View File

@@ -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<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
@@ -59,7 +66,7 @@ type DBConfigurationColumn = {
export const castDbEntryToAcmeCertificateAuthority = (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
): 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<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>,
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))
});

View File

@@ -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

View File

@@ -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
}));

View File

@@ -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<TAppConnectionDALFactory, "findById" | "update">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
certificateDAL: TCertificateDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey"
>;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
queueService: TQueueServiceFactory;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
};
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
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<ReturnType<typeof keyStore.acquireLock>>;
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
};
};

View File

@@ -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<TPermissionServiceFactory, "getProjectPermission">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
};
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
@@ -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 (

View File

@@ -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;
};

View File

@@ -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()

View File

@@ -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()
});

View File

@@ -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<TAppConnectionDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
pkiSubscriberDAL: Pick<
TPkiSubscriberDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
@@ -70,6 +64,7 @@ type TPkiSubscriberServiceFactoryDep = {
>;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityQueue: Pick<TCertificateAuthorityQueueFactory, "orderCertificateForSubscriber">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
@@ -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" });
};

View File

@@ -2,6 +2,7 @@ export {
useCreatePkiSubscriber,
useDeletePkiSubscriber,
useIssuePkiSubscriberCert,
useOrderPkiSubscriberCert,
useUpdatePkiSubscriber
} from "./mutations";
export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries";

View File

@@ -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;
}
});
};

View File

@@ -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;
};

View File

@@ -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"}
</Button>
)}
</div>