From 443e4226fbee7cbe3adf08735943fa0b75a4958c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 2 Dec 2025 02:11:12 -0300 Subject: [PATCH] Address PR comments and improvements --- .../ee/services/audit-log/audit-log-types.ts | 32 +- backend/src/server/routes/index.ts | 3 +- .../routes/v1/certificate-authority-router.ts | 19 +- .../routes/v1/certificate-profiles-router.ts | 2 +- .../server/routes/v1/certificate-router.ts | 700 ++++++------------ .../v1/deprecated-certificate-router.ts | 17 +- .../v3/deprecated-certificates-router.ts | 145 +--- .../certificate-issuance-queue.ts | 111 +-- .../internal-certificate-authority-service.ts | 21 +- .../internal-certificate-authority-types.ts | 17 + .../certificate-profile-types.ts | 2 +- .../certificate-request-dal.ts | 122 +-- .../certificate-request-service.test.ts | 12 +- .../certificate-request-service.ts | 34 +- .../certificate-v3-service.test.ts | 324 ++++---- .../certificate-v3/certificate-v3-service.ts | 421 +++++++---- .../certificate-v3/certificate-v3-types.ts | 28 +- frontend/src/hooks/api/certificates/types.ts | 50 +- .../components/CertificateIssuanceModal.tsx | 44 +- 19 files changed, 948 insertions(+), 1156 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index ac7aa6404..36b49a37c 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -388,6 +388,9 @@ export enum EventType { GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config", DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config", + CREATE_CERTIFICATE_REQUEST = "create-certificate-request", + GET_CERTIFICATE_REQUEST = "get-certificate-request", + GET_CERTIFICATE_FROM_REQUEST = "get-certificate-from-request", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", @@ -2846,7 +2849,6 @@ interface OrderCertificateFromProfile { type: EventType.ORDER_CERTIFICATE_FROM_PROFILE; metadata: { certificateProfileId: string; - orderId: string; profileName: string; }; } @@ -4196,6 +4198,31 @@ interface DisableCertificateRenewalConfigEvent { }; } +interface CreateCertificateRequestEvent { + type: EventType.CREATE_CERTIFICATE_REQUEST; + metadata: { + certificateRequestId: string; + profileId?: string; + caId?: string; + commonName?: string; + }; +} + +interface GetCertificateRequestEvent { + type: EventType.GET_CERTIFICATE_REQUEST; + metadata: { + certificateRequestId: string; + }; +} + +interface GetCertificateFromRequestEvent { + type: EventType.GET_CERTIFICATE_FROM_REQUEST; + metadata: { + certificateRequestId: string; + certificateId?: string; + }; +} + export type Event = | CreateSubOrganizationEvent | UpdateSubOrganizationEvent @@ -4575,6 +4602,9 @@ export type Event = | PamResourceDeleteEvent | UpdateCertificateRenewalConfigEvent | DisableCertificateRenewalConfigEvent + | CreateCertificateRequestEvent + | GetCertificateRequestEvent + | GetCertificateFromRequestEvent | AutomatedRenewCertificate | AutomatedRenewCertificateFailed | UserLoginEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2ff71bc3a..41e7e76f7 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2259,7 +2259,8 @@ export const registerRoutes = async ( kmsService, projectDAL, certificateBodyDAL, - certificateIssuanceQueue + certificateIssuanceQueue, + certificateRequestService }); const certificateV3Queue = certificateV3QueueServiceFactory({ diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 2917270ef..96e0db219 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -665,15 +665,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = - await server.services.internalCertificateAuthority.issueCertFromCa({ - caId: req.params.caId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); + const response = await server.services.internalCertificateAuthority.issueCertFromCa({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 685050e5c..ff770326d 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -299,7 +299,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid certificateAuthority: z .object({ id: z.string(), - projectId: z.string().optional(), + projectId: z.string(), status: z.string(), name: z.string(), isExternal: z.boolean().optional(), diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 25284a7ac..3823f791d 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -2,7 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; -import { CertificatesSchema, TCertificateRequests } from "@app/db/schemas"; +import { CertificatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs"; import { NotFoundError } from "@app/lib/errors"; @@ -13,7 +13,6 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ACMESANType, - CertificateOrderStatus, CertKeyAlgorithm, CertSignatureAlgorithm, CrlReason @@ -30,9 +29,21 @@ import { mapEnumsForValidation } from "@app/services/certificate-common/certific import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types"; import { booleanSchema } from "../sanitizedSchemas"; +type CertificateServiceResponse = TCertificateFromProfileResponse | Omit; + +const extractCertificateData = (data: CertificateServiceResponse) => ({ + certificate: data.certificate, + issuingCaCertificate: data.issuingCaCertificate, + certificateChain: data.certificateChain, + privateKey: "privateKey" in data ? data.privateKey : undefined, + serialNumber: data.serialNumber, + certificateId: data.certificateId +}); + interface CertificateRequestForService { commonName?: string; keyUsages?: CertKeyUsageType[]; @@ -50,13 +61,32 @@ interface CertificateRequestForService { keyAlgorithm?: string; } -const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => { +const validateTtlAndDateFields = (data: { + attributes?: { notBefore?: string; notAfter?: string; ttl?: string }; + notBefore?: string; + notAfter?: string; + ttl?: string; +}) => { + if (data.attributes) { + const hasDateFields = data.attributes.notBefore || data.attributes.notAfter; + const hasTtl = data.attributes.ttl; + return !(hasDateFields && hasTtl); + } const hasDateFields = data.notBefore || data.notAfter; const hasTtl = data.ttl; return !(hasDateFields && hasTtl); }; -const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => { +const validateDateOrder = (data: { + attributes?: { notBefore?: string; notAfter?: string }; + notBefore?: string; + notAfter?: string; +}) => { + if (data.attributes?.notBefore && data.attributes?.notAfter) { + const notBefore = new Date(data.attributes.notBefore); + const notAfter = new Date(data.attributes.notAfter); + return notBefore < notAfter; + } if (data.notBefore && data.notAfter) { const notBefore = new Date(data.notBefore); const notAfter = new Date(data.notAfter); @@ -65,20 +95,6 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => { return true; }; -const validateCertificateRequestFlow = (data: { - csr?: string; - subjectAlternativeNames?: Array<{ type: ACMESANType; value: string }>; - commonName?: string; - altNames?: Array<{ type: CertSubjectAlternativeNameType; value: string }>; -}) => { - const hasCSR = !!data.csr; - const hasSANs = !!data.subjectAlternativeNames?.length; - const hasStandardFields = !!(data.commonName || data.altNames?.length); - - const flowCount = Number(hasCSR) + Number(hasSANs) + Number(hasStandardFields); - return flowCount === 1; -}; - export const registerCertificateRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -92,45 +108,48 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { body: z .object({ profileId: z.string().uuid(), - projectId: z.string().uuid(), - commonName: validateTemplateRegexField.optional(), - keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), - extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), - altNames: z - .array( - z.object({ - type: z.nativeEnum(CertSubjectAlternativeNameType), - value: z.string().min(1, "SAN value cannot be empty") - }) - ) - .optional(), - signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), - keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), csr: z .string() .trim() .min(1, "CSR cannot be empty") .max(4096, "CSR cannot exceed 4096 characters") .optional(), - subjectAlternativeNames: z - .array( - z.object({ - type: z.nativeEnum(ACMESANType), - value: z - .string() - .trim() - .min(1, "SAN value cannot be empty") - .max(255, "SAN value must be less than 255 characters") - }) - ) + attributes: z + .object({ + commonName: validateTemplateRegexField.optional(), + keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), + altNames: z + .array( + z.object({ + type: z.nativeEnum(CertSubjectAlternativeNameType), + value: z.string().min(1, "SAN value cannot be empty") + }) + ) + .optional(), + signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(), + subjectAlternativeNames: z + .array( + z.object({ + type: z.nativeEnum(ACMESANType), + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") + }) + ) + .optional(), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), + notBefore: validateCaDateField.optional(), + notAfter: validateCaDateField.optional() + }) .optional(), - ttl: z - .string() - .trim() - .min(1, "TTL cannot be empty") - .refine((val) => ms(val) > 0, "TTL must be a positive number"), - notBefore: validateCaDateField.optional(), - notAfter: validateCaDateField.optional(), removeRootsFromChain: booleanSchema.default(false).optional() }) .refine(validateTtlAndDateFields, { @@ -139,264 +158,177 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { }) .refine(validateDateOrder, { message: "notBefore must be earlier than notAfter" - }) - .refine(validateCertificateRequestFlow, { - message: - "Must specify exactly one of: csr (for signing), subjectAlternativeNames (for ACME), or commonName/altNames (for issuance)" }), response: { - 200: z.union([ - z.object({ - certificate: z.string().trim(), - issuingCaCertificate: z.string().trim(), - certificateChain: z.string().trim(), - privateKey: z.string().trim().optional(), - serialNumber: z.string().trim(), - certificateId: z.string(), - certificateRequestId: z.string() - }), - z.object({ - certificateRequestId: z.string(), - status: z.string(), - orderId: z.string().optional(), - subjectAlternativeNames: z - .array( - z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string(), - status: z.nativeEnum(CertificateOrderStatus) - }) - ) - .optional(), - authorizations: z - .array( - z.object({ - identifier: z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string() - }), - status: z.nativeEnum(CertificateOrderStatus), - expires: z.string().optional(), - challenges: z.array( - z.object({ - type: z.string(), - status: z.nativeEnum(CertificateOrderStatus), - url: z.string(), - token: z.string() - }) - ) - }) - ) - .optional(), - finalize: z.string().optional() - }) - ]) + 200: z.object({ + certificate: z + .object({ + certificate: z.string().trim(), + issuingCaCertificate: z.string().trim(), + certificateChain: z.string().trim(), + privateKey: z.string().trim().optional(), + serialNumber: z.string().trim(), + certificateId: z.string() + }) + .nullable() + .optional(), + certificateRequestId: z.string() + }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { csr, subjectAlternativeNames, ...requestBody } = req.body; - - const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ + const { csr, attributes, ...requestBody } = req.body; + const profile = await server.services.certificateProfile.getProfileById({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.projectId, - profileId: requestBody.profileId, - csr, - commonName: requestBody.commonName, - altNames: requestBody.altNames ? JSON.stringify(requestBody.altNames) : undefined, - keyUsages: requestBody.keyUsages, - extendedKeyUsages: requestBody.extendedKeyUsages, - notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, - notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, - keyAlgorithm: requestBody.keyAlgorithm, - signatureAlgorithm: requestBody.signatureAlgorithm, - metadata: JSON.stringify({ - ttl: requestBody.ttl, - removeRootsFromChain: requestBody.removeRootsFromChain, - subjectAlternativeNames - }) + profileId: requestBody.profileId }); - try { - if (csr) { - const extractedCsrData = extractCertificateRequestFromCSR(csr); - - const data = await server.services.certificateV3.signCertificateFromProfile({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - profileId: requestBody.profileId, - csr, - validity: { ttl: requestBody.ttl }, - notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, - notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, - enrollmentType: EnrollmentType.API, - removeRootsFromChain: requestBody.removeRootsFromChain - }); - - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: data.projectId, - event: { - type: EventType.SIGN_CERTIFICATE_FROM_PROFILE, - metadata: { - certificateProfileId: requestBody.profileId, - certificateId: data.certificateId, - profileName: data.profileName, - commonName: extractedCsrData.commonName || "" - } - } - }); - - return { - ...data, - certificateRequestId: certificateRequest.id - }; - } - - const profile = await server.services.certificateProfile.getProfileById({ + let useOrderFlow = false; + if (profile?.caId) { + const ca = await server.services.certificateAuthority.getCaById({ + caId: profile.caId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - profileId: requestBody.profileId + actorOrgId: req.permission.orgId }); + const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; + useOrderFlow = caType !== CaType.INTERNAL; + } - let useOrderFlow = false; - if (profile?.caId) { - const ca = await server.services.certificateAuthority.getCaById({ - caId: profile.caId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId - }); - const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; - useOrderFlow = caType !== CaType.INTERNAL; + if (attributes?.subjectAlternativeNames?.length || useOrderFlow) { + let acmeAltNames: Array<{ type: ACMESANType; value: string }> | undefined = attributes?.subjectAlternativeNames; + if (useOrderFlow && !attributes?.subjectAlternativeNames && attributes?.altNames?.length) { + acmeAltNames = attributes.altNames.map((alt: { type: CertSubjectAlternativeNameType; value: string }) => ({ + type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME + ? ACMESANType.DNS + : ACMESANType.IP) as ACMESANType, + value: alt.value + })); } - if (subjectAlternativeNames?.length || useOrderFlow) { - let acmeAltNames = subjectAlternativeNames; - if (useOrderFlow && !subjectAlternativeNames && requestBody.altNames?.length) { - acmeAltNames = requestBody.altNames.map((alt) => ({ - type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME - ? ACMESANType.DNS - : ACMESANType.IP) as ACMESANType, - value: alt.value - })); - } - - const certificateOrderObject = { - altNames: acmeAltNames || [], - validity: { ttl: requestBody.ttl }, - commonName: requestBody.commonName, - keyUsages: requestBody.keyUsages, - extendedKeyUsages: requestBody.extendedKeyUsages, - notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, - notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, - signatureAlgorithm: requestBody.signatureAlgorithm, - keyAlgorithm: requestBody.keyAlgorithm - }; - - const data = await server.services.certificateV3.orderCertificateFromProfile({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - profileId: requestBody.profileId, - certificateOrder: certificateOrderObject, - removeRootsFromChain: requestBody.removeRootsFromChain, - certificateRequestId: certificateRequest.id - }); - - await server.services.certificateRequest.updateCertificateRequestStatus({ - certificateRequestId: certificateRequest.id, - status: CertificateRequestStatus.PENDING, - errorMessage: undefined - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: data.projectId, - event: { - type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, - metadata: { - certificateProfileId: requestBody.profileId, - orderId: data.orderId, - profileName: data.profileName - } - } - }); - - return { - certificateRequestId: certificateRequest.id, - ...data - }; - } - const certificateRequestForService: CertificateRequestForService = { - commonName: requestBody.commonName, - keyUsages: requestBody.keyUsages, - extendedKeyUsages: requestBody.extendedKeyUsages, - altNames: requestBody.altNames, - validity: { ttl: requestBody.ttl }, - notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, - notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, - signatureAlgorithm: requestBody.signatureAlgorithm, - keyAlgorithm: requestBody.keyAlgorithm + const certificateOrderObject = { + altNames: acmeAltNames || [], + validity: { ttl: attributes?.ttl || "" }, + commonName: attributes?.commonName, + keyUsages: attributes?.keyUsages, + extendedKeyUsages: attributes?.extendedKeyUsages, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + signatureAlgorithm: attributes?.signatureAlgorithm, + keyAlgorithm: attributes?.keyAlgorithm, + csr }; - const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService); - - const data = await server.services.certificateV3.issueCertificateFromProfile({ + const data = await server.services.certificateV3.orderCertificateFromProfile({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, profileId: requestBody.profileId, - certificateRequest: mappedCertificateRequest, + certificateOrder: certificateOrderObject, removeRootsFromChain: requestBody.removeRootsFromChain }); - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, event: { - type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE, + type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, metadata: { certificateProfileId: requestBody.profileId, - certificateId: data.certificateId, - commonName: requestBody.commonName || "", profileName: data.profileName } } }); return { - ...data, - certificateRequestId: certificateRequest.id + certificate: null, + certificateRequestId: data.certificateRequestId }; - } catch (error) { - await server.services.certificateRequest.updateCertificateRequestStatus({ - certificateRequestId: certificateRequest.id, - status: CertificateRequestStatus.FAILED, - errorMessage: error instanceof Error ? error.message : "Unknown error" - }); - throw error; } + + if (csr) { + const extractedCsrData = extractCertificateRequestFromCSR(csr); + + const data = await server.services.certificateV3.signCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: requestBody.profileId, + csr, + validity: { ttl: attributes?.ttl || "" }, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + enrollmentType: EnrollmentType.API, + removeRootsFromChain: requestBody.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.SIGN_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: requestBody.profileId, + certificateId: data.certificateId, + profileName: data.profileName, + commonName: extractedCsrData.commonName || "" + } + } + }); + return { + certificate: extractCertificateData(data), + certificateRequestId: data.certificateRequestId + }; + } + + const certificateRequestForService: CertificateRequestForService = { + commonName: attributes?.commonName, + keyUsages: attributes?.keyUsages, + extendedKeyUsages: attributes?.extendedKeyUsages, + altNames: attributes?.altNames, + validity: { ttl: attributes?.ttl || "" }, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + signatureAlgorithm: attributes?.signatureAlgorithm, + keyAlgorithm: attributes?.keyAlgorithm + }; + + const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService); + + const data = await server.services.certificateV3.issueCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: requestBody.profileId, + certificateRequest: mappedCertificateRequest, + removeRootsFromChain: requestBody.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: requestBody.profileId, + certificateId: data.certificateId, + commonName: attributes?.commonName || "", + profileName: data.profileName + } + } + }); + return { + certificate: extractCertificateData(data), + certificateRequestId: data.certificateRequestId + }; } }); server.route({ @@ -437,21 +369,18 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { certificateRequestId: req.params.requestId }); - if (data.certificate && data.serialNumber) { - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: (req.query as { projectId: string }).projectId, - event: { - type: EventType.GET_CERT, - metadata: { - certId: req.params.requestId, - cn: "", - serialNumber: data.serialNumber || "" - } + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: (req.query as { projectId: string }).projectId, + event: { + type: EventType.GET_CERT, + metadata: { + certId: req.params.requestId, + cn: "", + serialNumber: data.serialNumber || "" } - }); - } - + } + }); return data; } }); @@ -539,28 +468,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { removeRootsFromChain: req.body.removeRootsFromChain }); - const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: data.projectId, - profileId: req.body.profileId, - commonName: req.body.commonName, - altNames: req.body.altNames?.map((altName) => `${altName.type}:${altName.value}`).join(","), - keyUsages: req.body.keyUsages, - extendedKeyUsages: req.body.extendedKeyUsages, - notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, - notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, - keyAlgorithm: req.body.keyAlgorithm, - signatureAlgorithm: req.body.signatureAlgorithm - }); - - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -575,10 +482,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { } }); - return { - ...data, - certificateRequestId: certificateRequest.id - }; + return data; } }); @@ -644,29 +548,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr); - const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: data.projectId, - profileId: req.body.profileId, - csr: req.body.csr, - commonName: certificateRequestData.commonName, - altNames: certificateRequestData.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","), - keyUsages: certificateRequestData.keyUsages, - extendedKeyUsages: certificateRequestData.extendedKeyUsages, - notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, - notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, - keyAlgorithm: certificateRequestData.keyAlgorithm, - signatureAlgorithm: certificateRequestData.signatureAlgorithm - }); - - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -681,10 +562,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { } }); - return { - ...data, - certificateRequestId: certificateRequest.id - }; + return data; } }); @@ -735,34 +613,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - orderId: z.string(), - status: z.nativeEnum(CertificateOrderStatus), - subjectAlternativeNames: z.array( - z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string(), - status: z.nativeEnum(CertificateOrderStatus) - }) - ), - authorizations: z.array( - z.object({ - identifier: z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string() - }), - status: z.nativeEnum(CertificateOrderStatus), - expires: z.string().optional(), - challenges: z.array( - z.object({ - type: z.string(), - status: z.nativeEnum(CertificateOrderStatus), - url: z.string(), - token: z.string() - }) - ) - }) - ), - finalize: z.string(), certificate: z.string().optional(), certificateRequestId: z.string() }) @@ -794,23 +644,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { removeRootsFromChain: req.body.removeRootsFromChain }); - const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: data.projectId, - profileId: req.body.profileId, - commonName: req.body.commonName, - altNames: req.body.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","), - keyUsages: req.body.keyUsages, - extendedKeyUsages: req.body.extendedKeyUsages, - notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, - notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, - signatureAlgorithm: req.body.signatureAlgorithm, - keyAlgorithm: req.body.keyAlgorithm - }); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -818,16 +651,12 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, metadata: { certificateProfileId: req.body.profileId, - orderId: data.orderId, profileName: data.profileName } } }); - return { - ...data, - certificateRequestId: certificateRequest.id - }; + return data; } }); @@ -862,80 +691,41 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - let certificateRequest: TCertificateRequests | undefined; - - try { - const originalCertificate = await server.services.certificate.getCert({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.params.id - }); - if (!originalCertificate) { - throw new NotFoundError({ message: "Original certificate not found" }); - } - - certificateRequest = await server.services.certificateRequest.createCertificateRequest({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: originalCertificate.cert.projectId, - profileId: originalCertificate.cert.profileId || undefined, - caId: originalCertificate.cert.caId ?? undefined, - metadata: JSON.stringify({ - operation: "renewal", - originalCertificateId: req.params.id, - removeRootsFromChain: req.body?.removeRootsFromChain - }) - }); - - const data = await server.services.certificateV3.renewCertificate({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - certificateId: req.params.id, - removeRootsFromChain: req.body?.removeRootsFromChain, - certificateRequestId: certificateRequest.id - }); - - if (data.certificate && data.certificate.trim() !== "") { - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - } - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: data.projectId, - event: { - type: EventType.RENEW_CERTIFICATE, - metadata: { - originalCertificateId: req.params.id, - newCertificateId: data.certificateId, - profileName: data.profileName, - commonName: data.commonName - } - } - }); - - return { - ...data, - certificateRequestId: certificateRequest.id - }; - } catch (error) { - if (certificateRequest) { - await server.services.certificateRequest.updateCertificateRequestStatus({ - certificateRequestId: certificateRequest.id, - status: CertificateRequestStatus.FAILED, - errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal" - }); - } - throw error; + const originalCertificate = await server.services.certificate.getCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + if (!originalCertificate) { + throw new NotFoundError({ message: "Original certificate not found" }); } + + const data = await server.services.certificateV3.renewCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + certificateId: req.params.id, + removeRootsFromChain: req.body?.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.RENEW_CERTIFICATE, + metadata: { + originalCertificateId: req.params.id, + newCertificateId: data.certificateId, + profileName: data.profileName, + commonName: data.commonName + } + } + }); + + return data; } }); diff --git a/backend/src/server/routes/v1/deprecated-certificate-router.ts b/backend/src/server/routes/v1/deprecated-certificate-router.ts index 955407e4c..73072ea31 100644 --- a/backend/src/server/routes/v1/deprecated-certificate-router.ts +++ b/backend/src/server/routes/v1/deprecated-certificate-router.ts @@ -242,14 +242,15 @@ export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) = } }, handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = - await server.services.internalCertificateAuthority.issueCertFromCa({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); + const response = await server.services.internalCertificateAuthority.issueCertFromCa({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v3/deprecated-certificates-router.ts b/backend/src/server/routes/v3/deprecated-certificates-router.ts index 74ea22604..beaffd5f6 100644 --- a/backend/src/server/routes/v3/deprecated-certificates-router.ts +++ b/backend/src/server/routes/v3/deprecated-certificates-router.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { TCertificateRequests } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { NotFoundError } from "@app/lib/errors"; @@ -8,12 +7,7 @@ import { ms } from "@app/lib/ms"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { - ACMESANType, - CertificateOrderStatus, - CertKeyAlgorithm, - CertSignatureAlgorithm -} from "@app/services/certificate/certificate-types"; +import { ACMESANType, CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { CertExtendedKeyUsageType, @@ -23,7 +17,6 @@ import { import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; -import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { booleanSchema } from "../sanitizedSchemas"; @@ -340,34 +333,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - orderId: z.string(), - status: z.nativeEnum(CertificateOrderStatus), - subjectAlternativeNames: z.array( - z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string(), - status: z.nativeEnum(CertificateOrderStatus) - }) - ), - authorizations: z.array( - z.object({ - identifier: z.object({ - type: z.nativeEnum(ACMESANType), - value: z.string() - }), - status: z.nativeEnum(CertificateOrderStatus), - expires: z.string().optional(), - challenges: z.array( - z.object({ - type: z.string(), - status: z.nativeEnum(CertificateOrderStatus), - url: z.string(), - token: z.string() - }) - ) - }) - ), - finalize: z.string(), certificate: z.string().optional(), certificateRequestId: z.string() }) @@ -423,7 +388,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, metadata: { certificateProfileId: req.body.profileId, - orderId: data.orderId, profileName: data.profileName } } @@ -467,80 +431,41 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - let certificateRequest: TCertificateRequests | undefined; - - try { - const originalCertificate = await server.services.certificate.getCert({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.params.certificateId - }); - if (!originalCertificate) { - throw new NotFoundError({ message: "Original certificate not found" }); - } - - certificateRequest = await server.services.certificateRequest.createCertificateRequest({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: originalCertificate.cert.projectId, - profileId: originalCertificate.cert.profileId || undefined, - caId: originalCertificate.cert.caId ?? undefined, - metadata: JSON.stringify({ - operation: "renewal", - originalCertificateId: req.params.certificateId, - removeRootsFromChain: req.body?.removeRootsFromChain - }) - }); - - const data = await server.services.certificateV3.renewCertificate({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - certificateId: req.params.certificateId, - removeRootsFromChain: req.body?.removeRootsFromChain, - certificateRequestId: certificateRequest.id - }); - - if (data.certificate && data.certificate.trim() !== "") { - await server.services.certificateRequest.attachCertificateToRequest({ - certificateRequestId: certificateRequest.id, - certificateId: data.certificateId - }); - } - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: data.projectId, - event: { - type: EventType.RENEW_CERTIFICATE, - metadata: { - originalCertificateId: req.params.certificateId, - newCertificateId: data.certificateId, - profileName: data.profileName, - commonName: data.commonName - } - } - }); - - return { - ...data, - certificateRequestId: certificateRequest.id - }; - } catch (error) { - if (certificateRequest) { - await server.services.certificateRequest.updateCertificateRequestStatus({ - certificateRequestId: certificateRequest.id, - status: CertificateRequestStatus.FAILED, - errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal" - }); - } - throw error; + const originalCertificate = await server.services.certificate.getCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.certificateId + }); + if (!originalCertificate) { + throw new NotFoundError({ message: "Original certificate not found" }); } + + const data = await server.services.certificateV3.renewCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + certificateId: req.params.certificateId, + removeRootsFromChain: req.body?.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.RENEW_CERTIFICATE, + metadata: { + originalCertificateId: req.params.certificateId, + newCertificateId: data.certificateId, + profileName: data.profileName, + commonName: data.commonName + } + } + }); + + return data; } }); diff --git a/backend/src/services/certificate-authority/certificate-issuance-queue.ts b/backend/src/services/certificate-authority/certificate-issuance-queue.ts index 846d0c4bc..13fd68804 100644 --- a/backend/src/services/certificate-authority/certificate-issuance-queue.ts +++ b/backend/src/services/certificate-authority/certificate-issuance-queue.ts @@ -1,12 +1,11 @@ import acme from "acme-client"; -import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, TQueueServiceFactory } from "@app/queue"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; -import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -15,6 +14,7 @@ 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 { CertKeyAlgorithm } from "../certificate-common/certificate-constants"; import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service"; import { CertificateRequestStatus } from "../certificate-request/certificate-request-types"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; @@ -41,6 +41,7 @@ export type TIssueCertificateFromProfileJobData = { isRenewal?: boolean; originalCertificateId?: string; certificateRequestId?: string; + csr?: string; }; type TCertificateIssuanceQueueFactoryDep = { @@ -86,40 +87,6 @@ export const certificateIssuanceQueueFactory = ({ certificateProfileDAL, certificateRequestService }: TCertificateIssuanceQueueFactoryDep) => { - const validateKeyUsages = (keyUsages: unknown): CertKeyUsage[] => { - if (!keyUsages) return []; - const validKeyUsages = Object.values(CertKeyUsage); - - if (Array.isArray(keyUsages)) { - return keyUsages.filter( - (usage): usage is CertKeyUsage => typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsage) - ); - } - - return []; - }; - - const validateExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => { - if (!extendedKeyUsages) return []; - const validExtendedKeyUsages = Object.values(CertExtendedKeyUsage); - - if (Array.isArray(extendedKeyUsages)) { - return extendedKeyUsages.filter( - (usage): usage is CertExtendedKeyUsage => - typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsage) - ); - } - - return []; - }; - - const validateKeyAlgorithm = (keyAlgorithm: unknown): CertKeyAlgorithm | undefined => { - if (typeof keyAlgorithm !== "string") return undefined; - const validKeyAlgorithms = Object.values(CertKeyAlgorithm); - return validKeyAlgorithms.includes(keyAlgorithm as CertKeyAlgorithm) - ? (keyAlgorithm as CertKeyAlgorithm) - : undefined; - }; const acmeFns = AcmeCertificateAuthorityFns({ appConnectionDAL, appConnectionService, @@ -168,7 +135,8 @@ export const certificateIssuanceQueueFactory = ({ extendedKeyUsages, isRenewal, originalCertificateId, - certificateRequestId + certificateRequestId, + csr }: TIssueCertificateFromProfileJobData) => { const jobData: TIssueCertificateFromProfileJobData = { certificateId, @@ -183,7 +151,8 @@ export const certificateIssuanceQueueFactory = ({ extendedKeyUsages, isRenewal, originalCertificateId, - certificateRequestId + certificateRequestId, + csr }; await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, { @@ -210,7 +179,8 @@ export const certificateIssuanceQueueFactory = ({ extendedKeyUsages, isRenewal, originalCertificateId, - certificateRequestId + certificateRequestId, + csr } = data; try { @@ -225,32 +195,36 @@ export const certificateIssuanceQueueFactory = ({ const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (ca.externalCa?.type === CaType.ACME) { - const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm); - if (!validatedKeyAlgorithm) { - throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` }); - } - const keyAlg = keyAlgorithmToAlgCfg(validatedKeyAlgorithm); - const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]); - const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); - const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + let certificateCsr: string; + let skLeaf: string = ""; - const [, certificateCsr] = await acme.crypto.createCsr( - { - altNames: altNames || [], - commonName: commonName || "" - }, - skLeaf - ); + if (csr) { + certificateCsr = csr; + } else { + const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm); + const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]); + const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); + skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const [, generatedCsr] = await acme.crypto.createCsr( + { + altNames: altNames || [], + commonName: commonName || "" + }, + skLeaf + ); + certificateCsr = generatedCsr.toString(); + } const acmeResult = await acmeFns.orderCertificateFromProfile({ caId, profileId, commonName: commonName || "", altNames: altNames || [], - csr: certificateCsr, + csr: Buffer.from(certificateCsr), csrPrivateKey: skLeaf, - keyUsages: validateKeyUsages(keyUsages), - extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages), + keyUsages: keyUsages as CertKeyUsage[], + extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[], ttl, signatureAlgorithm, keyAlgorithm, @@ -306,24 +280,20 @@ export const certificateIssuanceQueueFactory = ({ } } - const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm); - if (!validatedKeyAlgorithm) { - throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` }); - } - const azureParams = { caId, profileId, commonName: commonName || "", altNames: altNames || [], - keyUsages: validateKeyUsages(keyUsages), - extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages), + keyUsages: keyUsages as CertKeyUsage[], + extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[], validity: { ttl }, signatureAlgorithm, - keyAlgorithm: validatedKeyAlgorithm, + keyAlgorithm: keyAlgorithm as CertKeyAlgorithm, isRenewal, originalCertificateId, - template + template, + ...(csr && { csr }) }; const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams); @@ -383,8 +353,6 @@ export const certificateIssuanceQueueFactory = ({ }; const initializeCertificateIssuanceQueue = async () => { - const appCfg = getConfig(); - await queueService.startPg( QueueJobs.CaIssueCertificateFromProfile, async ([job]) => { @@ -392,8 +360,9 @@ export const certificateIssuanceQueueFactory = ({ await processCertificateIssuanceJobs(data); }, { - workerCount: appCfg.NODE_ENV === "production" ? 3 : 1, - batchSize: 1 + workerCount: 2, + batchSize: 1, + pollingIntervalSeconds: 1 } ); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 35c596cd3..f3c56e322 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -67,6 +67,7 @@ import { TGetCaDTO, TImportCertToCaDTO, TIssueCertFromCaDTO, + TIssueCertFromCaResponse, TRenewCaCertDTO, TSignCertFromCaDTO, TSignIntermediateDTO, @@ -1198,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({ isFromProfile, internal = false, tx - }: TIssueCertFromCaDTO) => { + }: TIssueCertFromCaDTO): Promise => { let ca: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; let collectionId = pkiCollectionId; @@ -1532,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({ return cert; }; + let cert; if (tx) { - await executeIssueCertOperations(tx); + cert = await executeIssueCertOperations(tx); } else { - await certificateDAL.transaction(executeIssueCertOperations); + cert = await certificateDAL.transaction(executeIssueCertOperations); } return { @@ -1544,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({ issuingCaCertificate, privateKey: skLeaf, serialNumber, + certificateId: cert.id, + commonName, ca: expandInternalCa(ca) }; }; @@ -1905,8 +1909,8 @@ export const internalCertificateAuthorityServiceFactory = ({ plainText: Buffer.from(certificateChainPem) }); - await certificateDAL.transaction(async (tx) => { - const cert = await certificateDAL.create( + const cert = await certificateDAL.transaction(async (tx) => { + const newCert = await certificateDAL.create( { caId: (ca as TCertificateAuthorities).id, caCertId: caCert.id, @@ -1929,7 +1933,7 @@ export const internalCertificateAuthorityServiceFactory = ({ await certificateBodyDAL.create( { - certId: cert.id, + certId: newCert.id, encryptedCertificate, encryptedCertificateChain }, @@ -1940,13 +1944,13 @@ export const internalCertificateAuthorityServiceFactory = ({ await pkiCollectionItemDAL.create( { pkiCollectionId: collectionId, - certId: cert.id + certId: newCert.id }, tx ); } - return cert; + return newCert; }); return { @@ -1954,6 +1958,7 @@ export const internalCertificateAuthorityServiceFactory = ({ certificateChain: certificateChainPem, issuingCaCertificate, serialNumber, + certificateId: cert.id, ca: expandInternalCa(ca), commonName: cn }; 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 8f4e9c0d6..60b9c65c5 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 @@ -248,3 +248,20 @@ export type TIssueCertWithTemplateDTO = { keyUsages?: CertKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[]; }; + +type TCaReference = { + id: string; + projectId: string; + dn: string; +}; + +export type TIssueCertFromCaResponse = { + certificate: string; + certificateChain: string; + issuingCaCertificate: string; + privateKey: string; + serialNumber: string; + certificateId: string; + ca: TCaReference; + commonName: string; +}; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 41c1b4746..3eca249cd 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -56,7 +56,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & { }; certificateAuthority?: { id: string; - projectId?: string; + projectId: string; status: string; name: string; isExternal?: boolean; diff --git a/backend/src/services/certificate-request/certificate-request-dal.ts b/backend/src/services/certificate-request/certificate-request-dal.ts index 93d13f0ba..df2a4b0c4 100644 --- a/backend/src/services/certificate-request/certificate-request-dal.ts +++ b/backend/src/services/certificate-request/certificate-request-dal.ts @@ -1,36 +1,12 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName, TCertificateRequests } from "@app/db/schemas"; +import { TableName, TCertificateRequests, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; - -type TCertificateRequestWithCertificateFlat = TCertificateRequests & { - certificateId?: string | null; - certificateSerialNumber?: string | null; - certificateFriendlyName?: string | null; - certificateCommonName?: string | null; - certificateAltNames?: string | null; - certificateStatus?: string | null; - certificateNotBefore?: Date | null; - certificateNotAfter?: Date | null; - certificateKeyUsages?: string[] | null; - certificateExtendedKeyUsages?: string[] | null; -}; - -type TCertificateInfo = { - id: string; - serialNumber: string; - friendlyName: string | null; - commonName: string; - altNames: string | null; - status: string; - notBefore: Date; - notAfter: Date; - keyUsages: string[] | null; - extendedKeyUsages: string[] | null; -}; +import { ormify, selectAllTableCols } from "@app/lib/knex"; type TCertificateRequestWithCertificate = TCertificateRequests & { - certificate: TCertificateInfo | null; + certificate: TCertificates | null; }; export type TCertificateRequestDALFactory = ReturnType; @@ -40,61 +16,24 @@ export const certificateRequestDALFactory = (db: TDbClient) => { const findByIdWithCertificate = async (id: string): Promise => { try { - const certificateRequest = (await db(TableName.CertificateRequests) - .leftJoin( - TableName.Certificate, - `${TableName.CertificateRequests}.certificateId`, - `${TableName.Certificate}.id` - ) - .where(`${TableName.CertificateRequests}.id`, id) - .select( - `${TableName.CertificateRequests}.*`, - `${TableName.Certificate}.id as certificateId`, - `${TableName.Certificate}.serialNumber as certificateSerialNumber`, - `${TableName.Certificate}.friendlyName as certificateFriendlyName`, - `${TableName.Certificate}.commonName as certificateCommonName`, - `${TableName.Certificate}.altNames as certificateAltNames`, - `${TableName.Certificate}.status as certificateStatus`, - `${TableName.Certificate}.notBefore as certificateNotBefore`, - `${TableName.Certificate}.notAfter as certificateNotAfter`, - `${TableName.Certificate}.keyUsages as certificateKeyUsages`, - `${TableName.Certificate}.extendedKeyUsages as certificateExtendedKeyUsages` - ) - .first()) as TCertificateRequestWithCertificateFlat | undefined; - + const certificateRequest = await certificateRequestOrm.findById(id); if (!certificateRequest) return null; - // Transform the flat result into nested structure - const { - certificateId, - certificateSerialNumber, - certificateFriendlyName, - certificateCommonName, - certificateAltNames, - certificateStatus, - certificateNotBefore, - certificateNotAfter, - certificateKeyUsages, - certificateExtendedKeyUsages, - ...certificateRequestData - } = certificateRequest; + if (!certificateRequest.certificateId) { + return { + ...certificateRequest, + certificate: null + }; + } + + const certificate = await db(TableName.Certificate) + .where("id", certificateRequest.certificateId) + .select(selectAllTableCols(TableName.Certificate)) + .first(); return { - ...certificateRequestData, - certificate: certificateId - ? { - id: certificateId, - serialNumber: certificateSerialNumber as string, - friendlyName: certificateFriendlyName || null, - commonName: certificateCommonName as string, - altNames: certificateAltNames || null, - status: certificateStatus as string, - notBefore: certificateNotBefore as Date, - notAfter: certificateNotAfter as Date, - keyUsages: certificateKeyUsages || null, - extendedKeyUsages: certificateExtendedKeyUsages || null - } - : null + ...certificateRequest, + certificate: certificate || null }; } catch (error) { throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" }); @@ -111,24 +50,33 @@ export const certificateRequestDALFactory = (db: TDbClient) => { } }; - const updateStatus = async (id: string, status: string, errorMessage?: string): Promise => { + const updateStatus = async ( + id: string, + status: string, + errorMessage?: string, + tx?: Knex + ): Promise => { try { const updateData: Partial = { status }; if (errorMessage !== undefined) { updateData.errorMessage = errorMessage; } - return await certificateRequestOrm.updateById(id, updateData); + return await certificateRequestOrm.updateById(id, updateData, tx); } catch (error) { throw new DatabaseError({ error, name: "Update certificate request status" }); } }; - const attachCertificate = async (id: string, certificateId: string): Promise => { + const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise => { try { - return await certificateRequestOrm.updateById(id, { - certificateId, - status: "issued" - }); + return await certificateRequestOrm.updateById( + id, + { + certificateId, + status: "issued" + }, + tx + ); } catch (error) { throw new DatabaseError({ error, name: "Attach certificate to request" }); } diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index fec4ce9ae..2df3a2354 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -13,7 +13,7 @@ import { ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { NotFoundError } from "@app/lib/errors"; import { ActorType, AuthMethod } from "@app/services/auth/auth-type"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; @@ -202,7 +202,7 @@ describe("CertificateRequestService", () => { (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); - await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(BadRequestError); + await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError); }); }); @@ -257,14 +257,14 @@ describe("CertificateRequestService", () => { "550e8400-e29b-41d4-a716-446655440005" ); expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ - serialNumber: "123456", + id: "550e8400-e29b-41d4-a716-446655440006", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ - serialNumber: "123456", + id: "550e8400-e29b-41d4-a716-446655440006", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, @@ -354,14 +354,14 @@ describe("CertificateRequestService", () => { "550e8400-e29b-41d4-a716-446655440005" ); expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ - serialNumber: "123456", + id: "550e8400-e29b-41d4-a716-446655440008", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ - serialNumber: "123456", + id: "550e8400-e29b-41d4-a716-446655440008", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index b27783822..67cd6d77c 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; import { z } from "zod"; import { ActionProjectType } from "@app/db/schemas"; @@ -30,13 +31,12 @@ type TCertificateRequestServiceFactoryDep = { export type TCertificateRequestServiceFactory = ReturnType; -// Input validation schemas const certificateRequestDataSchema = z .object({ profileId: z.string().uuid().optional(), caId: z.string().uuid().optional(), csr: z.string().min(1).optional(), - commonName: z.string().min(1).max(255).optional(), + commonName: z.string().max(255).optional(), altNames: z.string().max(1000).optional(), keyUsages: z.array(z.string()).max(20).optional(), extendedKeyUsages: z.array(z.string()).max(20).optional(), @@ -93,8 +93,9 @@ export const certificateRequestServiceFactory = ({ actorAuthMethod, actorOrgId, projectId, + tx, ...requestData - }: TCreateCertificateRequestDTO) => { + }: TCreateCertificateRequestDTO & { tx?: Knex }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -112,11 +113,20 @@ export const certificateRequestServiceFactory = ({ // Validate input data before creating the request const validatedData = validateCertificateRequestData(requestData); - const certificateRequest = await certificateRequestDAL.create({ - status: CertificateRequestStatus.PENDING, - projectId, - ...validatedData - }); + const certificateRequest = tx + ? await certificateRequestDAL.create( + { + status: CertificateRequestStatus.PENDING, + projectId, + ...validatedData + }, + tx + ) + : await certificateRequestDAL.create({ + status: CertificateRequestStatus.PENDING, + projectId, + ...validatedData + }); return certificateRequest; }; @@ -149,7 +159,7 @@ export const certificateRequestServiceFactory = ({ } if (certificateRequest.projectId !== projectId) { - throw new BadRequestError({ message: "Certificate request does not belong to this project" }); + throw new NotFoundError({ message: "Certificate request not found" }); } return certificateRequest; @@ -183,7 +193,7 @@ export const certificateRequestServiceFactory = ({ } if (certificateRequest.projectId !== projectId) { - throw new BadRequestError({ message: "Certificate request does not belong to this project" }); + throw new NotFoundError({ message: "Certificate request not found" }); } // If no certificate is attached, return basic info @@ -201,7 +211,7 @@ export const certificateRequestServiceFactory = ({ // Get certificate body (PEM data) const certBody = await certificateService.getCertBody({ - serialNumber: certificateRequest.certificate.serialNumber, + id: certificateRequest.certificate.id, actor, actorId, actorAuthMethod, @@ -212,7 +222,7 @@ export const certificateRequestServiceFactory = ({ let privateKey: string | null = null; try { const certPrivateKey = await certificateService.getCertPrivateKey({ - serialNumber: certificateRequest.certificate.serialNumber, + id: certificateRequest.certificate.id, actor, actorId, actorAuthMethod, diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index bab58385f..b3db6f736 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; -import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types"; +import { ACMESANType, CertStatus } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; @@ -198,6 +198,9 @@ describe("CertificateV3Service", () => { } as any, certificateIssuanceQueue: { queueCertificateIssuance: vi.fn().mockResolvedValue(undefined) + }, + certificateRequestService: { + createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" }) } }); }); @@ -287,6 +290,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: { id: "ca-123", projectId: "project-123", @@ -346,8 +351,13 @@ describe("CertificateV3Service", () => { vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + const result = await service.issueCertificateFromProfile({ profileId, certificateRequest: mockCertificateRequest, @@ -481,6 +491,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: { id: "ca-123", projectId: "project-123", @@ -523,8 +535,34 @@ describe("CertificateV3Service", () => { vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + await service.issueCertificateFromProfile({ profileId, certificateRequest: camelCaseRequest, @@ -749,8 +787,13 @@ describe("CertificateV3Service", () => { }); vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + const result = await service.signCertificateFromProfile({ profileId, csr: mockCSR, @@ -819,168 +862,6 @@ describe("CertificateV3Service", () => { keyAlgorithm: "RSA_2048" }; - it("should create order successfully for API enrollment profile", async () => { - const profileId = "profile-123"; - const mockProfile = { - id: profileId, - projectId: "project-123", - enrollmentType: EnrollmentType.API, - issuerType: IssuerType.CA, - caId: "ca-123", - certificateTemplateId: "template-123", - createdAt: new Date(), - updatedAt: new Date(), - slug: "test-profile-order", - description: "Test order profile", - estConfigId: null, - apiConfigId: null - }; - - const mockCA = { - id: "ca-123", - projectId: "project-123", - externalCa: undefined, - internalCa: { - id: "internal-ca-123", - parentCaId: null, - type: "ROOT", - friendlyName: "Test CA", - organization: "Test Org", - ou: "Test OU", - country: "US", - province: "CA", - locality: "SF", - commonName: "Test CA", - dn: "CN=Test CA", - serialNumber: "123", - maxPathLength: null, - keyAlgorithm: "RSA_2048", - notBefore: undefined, - notAfter: undefined, - activeCaCertId: "cert-123", - caId: "ca-123" - }, - name: "Test CA", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - enableDirectIssuance: true - }; - - const mockTemplate = { - id: "template-123", - name: "Test Order Template", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-123", - description: "Test template for ordering certificates", - signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, - keyAlgorithm: { defaultKeyType: "RSA_2048" }, - attributes: [ - { - type: CertSubjectAttributeType.COMMON_NAME, - include: CertIncludeType.OPTIONAL, - value: ["example.com"] - } - ], - subject: undefined, - sans: undefined, - keyUsages: undefined, - extendedKeyUsages: undefined, - algorithms: undefined, - validity: undefined - }; - - const mockCertificateResult = { - certificate: "cert", - certificateChain: "chain", - issuingCaCertificate: "issuing-ca", - privateKey: "key", - serialNumber: "123456", - ca: { - id: "ca-123", - projectId: "project-123", - name: "Test CA", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - enableDirectIssuance: true, - externalCa: undefined, - internalCa: { - id: "internal-ca-123", - parentCaId: null, - type: "ROOT", - friendlyName: "Test CA", - organization: "Test Org", - ou: "Test OU", - country: "US", - province: "CA", - locality: "SF", - commonName: "Test CA", - dn: "CN=Test CA", - serialNumber: "123", - maxPathLength: null, - keyAlgorithm: "RSA_2048", - notBefore: null, - notAfter: null, - activeCaCertId: "cert-123", - caId: "ca-123" - } - } - }; - - const mockCertRecord = { - id: "cert-123", - serialNumber: "123456", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-123", - commonName: "example.com", - friendlyName: "Test Order Cert", - notBefore: new Date(), - notAfter: new Date(), - caId: "ca-123", - certificateTemplateId: "template-123", - revokedAt: null, - altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]), - caCertId: null, - keyUsages: ["DIGITAL_SIGNATURE"], - extendedKeyUsages: ["SERVER_AUTH"], - revocationReason: null, - pkiSubscriberId: null, - profileId: null - }; - - vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); - vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ - isValid: true, - errors: [], - warnings: [] - }); - vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); - vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); - vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); - vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); - vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); - - const result = await service.orderCertificateFromProfile({ - profileId, - certificateOrder: mockCertificateOrder, - ...mockActor - }); - - expect(result).toHaveProperty("orderId"); - expect(result).toHaveProperty("status", "valid"); - expect(result).toHaveProperty("certificate"); - expect(result.subjectAlternativeNames).toHaveLength(1); - expect(result.subjectAlternativeNames[0]).toEqual({ - type: ACMESANType.DNS, - value: "example.com", - status: CertificateOrderStatus.VALID - }); - }); - it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => { const profileId = "profile-123"; const mockProfile = { @@ -1117,6 +998,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: rsaCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1163,6 +1046,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - RSA CA is compatible with RSA signature algorithms await expect( @@ -1249,6 +1157,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: ecCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1295,6 +1205,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - EC CA is compatible with ECDSA signature algorithms await expect( @@ -1381,6 +1316,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: rsa8192Ca as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1427,6 +1364,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - dynamic check supports new RSA key sizes await expect( @@ -1513,6 +1475,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: newEcCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1559,6 +1523,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - dynamic check supports new EC curves await expect( @@ -1692,8 +1681,9 @@ describe("CertificateV3Service", () => { }); it("should successfully renew eligible certificate", async () => { - // Mock the initial findById call - vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert); + vi.mocked(mockCertificateDAL.findById) + .mockResolvedValueOnce(mockOriginalCert) + .mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" }); vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); @@ -1709,6 +1699,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "private-key", serialNumber: "789012", + certificateId: "cert-456", + commonName: "test.example.com", ca: mockCA }); @@ -2026,6 +2018,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "private-key", serialNumber: "789012", + certificateId: "cert-456", + commonName: "test.example.com", ca: mockCA }); diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 21a24a207..21e315a19 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -20,7 +20,6 @@ import { TCertificateDALFactory } from "@app/services/certificate/certificate-da import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, - CertificateOrderStatus, CertKeyAlgorithm, CertKeyType, CertKeyUsage, @@ -40,11 +39,7 @@ import { } from "@app/services/certificate-authority/certificate-authority-fns"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { - EnrollmentType, - IssuerType, - TCertificateProfileWithConfigs -} from "@app/services/certificate-profile/certificate-profile-types"; +import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -71,7 +66,9 @@ import { normalizeDateForApi, removeRootCaFromChain } from "../certificate-common/certificate-utils"; +import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; +import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types"; import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils"; @@ -122,6 +119,7 @@ type TCertificateV3ServiceFactoryDep = { import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory, "queueCertificateIssuance" >; + certificateRequestService: Pick; }; export type TCertificateV3ServiceFactory = ReturnType; @@ -861,7 +859,8 @@ export const certificateV3ServiceFactory = ({ pkiSyncQueue, kmsService, projectDAL, - certificateIssuanceQueue + certificateIssuanceQueue, + certificateRequestService }: TCertificateV3ServiceFactoryDep) => { const issueCertificateFromProfile = async ({ profileId, @@ -945,7 +944,7 @@ export const certificateV3ServiceFactory = ({ const result = await certificateDAL.transaction(async (tx) => { const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm); - return processSelfSignedCertificate({ + const selfSignedResult = await processSelfSignedCertificate({ certificateRequest, template, profile, @@ -957,9 +956,29 @@ export const certificateV3ServiceFactory = ({ projectDAL, tx }); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + profileId: profile.id, + commonName: certificateRequest.commonName, + altNames: certificateRequest.altNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages), + notBefore: certificateRequest.notBefore, + notAfter: certificateRequest.notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm + }); + + return { ...selfSignedResult, certificateRequestId: certRequestResult.id }; }); - const { selfSignedResult, certificateData } = result; + const { selfSignedResult, certificateData, certificateRequestId } = result; const subjectCommonName = (selfSignedResult.certificateSubject.common_name as string) || @@ -985,6 +1004,7 @@ export const certificateV3ServiceFactory = ({ privateKey: selfSignedResult.privateKey.toString("utf8"), serialNumber: selfSignedResult.serialNumber, certificateId: certificateData.id, + certificateRequestId, projectId: profile.projectId, profileName: profile.slug, commonName: subjectCommonName @@ -1003,8 +1023,16 @@ export const certificateV3ServiceFactory = ({ validateCaSupport(ca, "direct certificate issuance"); validateAlgorithmCompatibility(ca, template); - const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } = - await internalCaService.issueCertFromCa({ + const { + certificate, + certificateChain, + issuingCaCertificate, + privateKey, + serialNumber, + cert, + certificateRequestId + } = await certificateDAL.transaction(async (tx) => { + const certResult = await internalCaService.issueCertFromCa({ caId: ca.id, friendlyName: certificateSubject.common_name || "Certificate", commonName: certificateSubject.common_name || "", @@ -1023,22 +1051,43 @@ export const certificateV3ServiceFactory = ({ isFromProfile: true }); - const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); - if (!cert) { - throw new NotFoundError({ message: "Certificate was issued but could not be found in database" }); - } + const certificateRecord = await certificateDAL.findById(certResult.certificateId); + if (!certificateRecord) { + throw new NotFoundError({ message: "Certificate was issued but could not be found in database" }); + } - const finalRenewBeforeDays = calculateFinalRenewBeforeDays( - profile, - certificateRequest.validity.ttl, - new Date(cert.notAfter) - ); + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile, + certificateRequest.validity.ttl, + new Date(certificateRecord.notAfter) + ); - const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; - if (finalRenewBeforeDays !== undefined) { - updateData.renewBeforeDays = finalRenewBeforeDays; - } - await certificateDAL.updateById(cert.id, updateData); + const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; + if (finalRenewBeforeDays !== undefined) { + updateData.renewBeforeDays = finalRenewBeforeDays; + } + await certificateDAL.updateById(certificateRecord.id, updateData); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + profileId: profile.id, + commonName: certificateRequest.commonName, + altNames: certificateRequest.altNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages), + notBefore: certificateRequest.notBefore, + notAfter: certificateRequest.notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm + }); + + return { ...certResult, cert: certificateRecord, certificateRequestId: certRequestResult.id }; + }); let finalCertificateChain = bufferToString(certificateChain); if (removeRootsFromChain) { @@ -1052,6 +1101,7 @@ export const certificateV3ServiceFactory = ({ privateKey: bufferToString(privateKey), serialNumber, certificateId: cert.id, + certificateRequestId, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" @@ -1135,33 +1185,60 @@ export const certificateV3ServiceFactory = ({ const effectiveSignatureAlgorithm = extractedSignatureAlgorithm; const effectiveKeyAlgorithm = extractedKeyAlgorithm; - const { certificate, certificateChain, issuingCaCertificate, serialNumber } = - await internalCaService.signCertFromCa({ - isInternal: true, - caId: ca.id, - csr, - ttl: validity.ttl, - altNames: undefined, - notBefore: normalizeDateForApi(notBefore), - notAfter: normalizeDateForApi(notAfter), - signatureAlgorithm: effectiveSignatureAlgorithm, - keyAlgorithm: effectiveKeyAlgorithm, - isFromProfile: true + const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } = + await certificateDAL.transaction(async (tx) => { + const certResult = await internalCaService.signCertFromCa({ + isInternal: true, + caId: ca.id, + csr, + ttl: validity.ttl, + altNames: undefined, + notBefore: normalizeDateForApi(notBefore), + notAfter: normalizeDateForApi(notAfter), + signatureAlgorithm: effectiveSignatureAlgorithm, + keyAlgorithm: effectiveKeyAlgorithm, + isFromProfile: true + }); + + const signedCertRecord = await certificateDAL.findById(certResult.certificateId); + if (!signedCertRecord) { + throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); + } + + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile, + validity.ttl, + new Date(signedCertRecord.notAfter) + ); + + const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; + if (finalRenewBeforeDays !== undefined) { + updateData.renewBeforeDays = finalRenewBeforeDays; + } + await certificateDAL.updateById(signedCertRecord.id, updateData); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + profileId: profile.id, + csr, + commonName: mappedCertificateRequest.commonName, + altNames: mappedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(mappedCertificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(mappedCertificateRequest.extendedKeyUsages), + notBefore, + notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm + }); + + return { ...certResult, cert: signedCertRecord, certificateRequestId: certRequestResult.id }; }); - const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); - if (!cert) { - throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); - } - - const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter)); - - const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId }; - if (finalRenewBeforeDays !== undefined) { - updateData2.renewBeforeDays = finalRenewBeforeDays; - } - await certificateDAL.updateById(cert.id, updateData2); - const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer); let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer); if (removeRootsFromChain) { @@ -1174,6 +1251,7 @@ export const certificateV3ServiceFactory = ({ certificateChain: certificateChainString, serialNumber, certificateId: cert.id, + certificateRequestId, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" @@ -1186,9 +1264,7 @@ export const certificateV3ServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId, - removeRootsFromChain, - certificateRequestId + actorOrgId }: TOrderCertificateFromProfileDTO): Promise => { const profile = await validateProfileAndPermissions( profileId, @@ -1202,37 +1278,59 @@ export const certificateV3ServiceFactory = ({ EnrollmentType.API ); - const certificateRequest = { - commonName: certificateOrder.commonName, - keyUsages: certificateOrder.keyUsages, - extendedKeyUsages: certificateOrder.extendedKeyUsages, - subjectAlternativeNames: certificateOrder.altNames?.map((san) => { - let certType: CertSubjectAlternativeNameType; - switch (san.type) { - case "dns": - certType = CertSubjectAlternativeNameType.DNS_NAME; - break; - case "ip": - certType = CertSubjectAlternativeNameType.IP_ADDRESS; - break; - default: - throw new BadRequestError({ - message: `Unsupported Subject Alternative Name type: ${san.type as string}` - }); - } - return { - type: certType, - value: san.value - }; - }), - validity: certificateOrder.validity, - notBefore: certificateOrder.notBefore, - notAfter: certificateOrder.notAfter, - signatureAlgorithm: certificateOrder.signatureAlgorithm, - keyAlgorithm: certificateOrder.keyAlgorithm - }; + let certificateRequest: TCertificateRequest; + let extractedKeyAlgorithm: string | undefined; + let extractedSignatureAlgorithm: string | undefined; + + if (certificateOrder.csr) { + certificateRequest = extractCertificateRequestFromCSR(certificateOrder.csr); + const algorithms = extractAlgorithmsFromCSR(certificateOrder.csr); + extractedKeyAlgorithm = algorithms.keyAlgorithm; + extractedSignatureAlgorithm = algorithms.signatureAlgorithm; + certificateRequest.validity = certificateOrder.validity; + if (certificateOrder.notBefore && certificateOrder.notAfter) { + certificateRequest.notBefore = certificateOrder.notBefore; + certificateRequest.notAfter = certificateOrder.notAfter; + } + } else { + certificateRequest = { + commonName: certificateOrder.commonName, + keyUsages: certificateOrder.keyUsages, + extendedKeyUsages: certificateOrder.extendedKeyUsages, + subjectAlternativeNames: certificateOrder.altNames?.map((san) => { + let certType: CertSubjectAlternativeNameType; + switch (san.type) { + case "dns": + certType = CertSubjectAlternativeNameType.DNS_NAME; + break; + case "ip": + certType = CertSubjectAlternativeNameType.IP_ADDRESS; + break; + default: + throw new BadRequestError({ + message: `Unsupported Subject Alternative Name type: ${san.type as string}` + }); + } + return { + type: certType, + value: san.value + }; + }), + validity: certificateOrder.validity, + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter, + signatureAlgorithm: certificateOrder.signatureAlgorithm, + keyAlgorithm: certificateOrder.keyAlgorithm + }; + } const mappedCertificateRequest = mapEnumsForValidation(certificateRequest); + + if (certificateOrder.csr) { + mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm; + mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm; + } + const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, mappedCertificateRequest @@ -1258,64 +1356,53 @@ export const certificateV3ServiceFactory = ({ const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; if (caType === CaType.INTERNAL) { - const certificateResult = await issueCertificateFromProfile({ - profileId, - certificateRequest, - actor, - actorId, - actorAuthMethod, - actorOrgId, - removeRootsFromChain + throw new BadRequestError({ + message: "Certificate ordering is not supported for the specified CA type" }); - - const orderId = randomUUID(); - - return { - orderId, - status: CertificateOrderStatus.VALID, - subjectAlternativeNames: certificateOrder.altNames.map((san) => ({ - type: san.type, - value: san.value, - status: CertificateOrderStatus.VALID - })), - authorizations: [], - finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`, - certificate: certificateResult.certificate, - projectId: certificateResult.projectId, - profileName: certificateResult.profileName - }; } if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) { const orderId = randomUUID(); + const certRequest = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + profileId: profile.id, + commonName: certificateOrder.commonName || "", + keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [], + extendedKeyUsages: certificateOrder.extendedKeyUsages + ? convertEnumsToStringArray(certificateOrder.extendedKeyUsages) + : [], + keyAlgorithm: certificateOrder.keyAlgorithm || "", + signatureAlgorithm: certificateOrder.signatureAlgorithm || "", + altNames: certificateOrder.altNames?.map((san) => san.value).join(",") || "", + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter + }); + await certificateIssuanceQueue.queueCertificateIssuance({ certificateId: orderId, profileId: profile.id, caId: profile.caId || "", ttl: certificateOrder.validity?.ttl || "1y", signatureAlgorithm: certificateOrder.signatureAlgorithm || "", - keyAlgorithm: certificateOrder.keyAlgorithm || "", - commonName: certificateOrder.commonName || "", - altNames: certificateOrder.altNames?.map((san) => san.value) || [], - keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [], - extendedKeyUsages: certificateOrder.extendedKeyUsages - ? convertEnumsToStringArray(certificateOrder.extendedKeyUsages) + keyAlgorithm: certificateRequest.keyAlgorithm || "", + commonName: certificateRequest.commonName || "", + altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [], + keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [], + extendedKeyUsages: certificateRequest.extendedKeyUsages + ? convertEnumsToStringArray(certificateRequest.extendedKeyUsages) : [], - certificateRequestId + certificateRequestId: certRequest.id, + csr: certificateOrder.csr }); return { - orderId, - status: CertificateOrderStatus.PENDING, - subjectAlternativeNames: certificateOrder.altNames.map((san) => ({ - type: san.type, - value: san.value, - status: CertificateOrderStatus.PENDING - })), - authorizations: [], - finalize: `/api/v3/pki/certificates/orders/${orderId}/finalize`, - projectId: profile.projectId, + certificateRequestId: certRequest.id, + projectId: certRequest.projectId, profileName: profile.slug }; } @@ -1325,30 +1412,6 @@ export const certificateV3ServiceFactory = ({ }); }; - // Type for internal CA renewal result - type TInternalRenewalData = { - certificate: string; - certificateChain: string; - issuingCaCertificate: string; - serialNumber: string; - newCert: TCertificates; - originalCert: TCertificates; - profile: TCertificateProfileWithConfigs | null; - }; - - // Type for external CA renewal result - type TExternalRenewalData = { - isExternalCA: true; - ca: TCertificateAuthorityWithAssociatedCa; - profile: TCertificateProfileWithConfigs | null; - originalCert: TCertificates; - originalSignatureAlgorithm: CertSignatureAlgorithm; - originalKeyAlgorithm: CertKeyAlgorithm; - ttl: string; - }; - - type TRenewalTransactionResult = TInternalRenewalData | TExternalRenewalData; - const renewCertificate = async ({ certificateId, actor, @@ -1356,10 +1419,11 @@ export const certificateV3ServiceFactory = ({ actorAuthMethod, actorOrgId, internal = false, - removeRootsFromChain, - certificateRequestId - }: TRenewCertificateDTO & { internal?: boolean }): Promise => { - const renewalResult: TRenewalTransactionResult = await certificateDAL.transaction(async (tx) => { + removeRootsFromChain + }: Omit & { + internal?: boolean; + }): Promise => { + const renewalResult = await certificateDAL.transaction(async (tx) => { const originalCert = await certificateDAL.findById(certificateId, tx); if (!originalCert) { throw new NotFoundError({ message: "Certificate not found" }); @@ -1576,7 +1640,7 @@ export const certificateV3ServiceFactory = ({ issuingCaCertificate = caResult.issuingCaCertificate; serialNumber = caResult.serialNumber; - const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx); + const foundCert = await certificateDAL.findById(caResult.certificateId, tx); if (!foundCert) { throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); } @@ -1665,6 +1729,27 @@ export const certificateV3ServiceFactory = ({ await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: originalCert.projectId, + tx, + profileId: originalCert.profileId || undefined, + commonName: originalCert.commonName || undefined, + altNames: originalCert.altNames || undefined, + keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)), + extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy( + parseExtendedKeyUsages(originalCert.extendedKeyUsages) + ), + notBefore: new Date(newCert.notBefore), + notAfter: new Date(newCert.notAfter), + keyAlgorithm: originalKeyAlgorithm, + signatureAlgorithm: originalSignatureAlgorithm, + metadata: `Renewed from certificate ID: ${originalCert.id}` + }); + return { certificate, certificateChain, @@ -1672,10 +1757,13 @@ export const certificateV3ServiceFactory = ({ serialNumber, newCert, originalCert, - profile + profile, + certRequestResult }; }); + let certificateRequestId: string = renewalResult.certRequestResult?.id || ""; + // Handle external CA renewals separately if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) { const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult; @@ -1685,6 +1773,27 @@ export const certificateV3ServiceFactory = ({ ? originalCert.altNames.split(",").map((san: string) => san.trim()) : []; + const certificateRequest = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: originalCert.projectId, + profileId: profile?.id, + caId: ca.id, + commonName: originalCert.commonName || undefined, + altNames: originalCert.altNames || undefined, + keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)), + extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy( + parseExtendedKeyUsages(originalCert.extendedKeyUsages) + ), + keyAlgorithm: originalKeyAlgorithm, + signatureAlgorithm: originalSignatureAlgorithm, + metadata: `Renewed from certificate ID: ${originalCert.id}` + }); + + certificateRequestId = certificateRequest.id; + await certificateIssuanceQueue.queueCertificateIssuance({ certificateId: renewalOrderId, profileId: profile?.id || "", @@ -1698,7 +1807,7 @@ export const certificateV3ServiceFactory = ({ extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)), isRenewal: true, originalCertificateId: certificateId, - certificateRequestId + certificateRequestId: certificateRequest.id }); return { @@ -1707,6 +1816,7 @@ export const certificateV3ServiceFactory = ({ issuingCaCertificate: "", serialNumber: "", certificateId: renewalOrderId, + certificateRequestId: certificateRequest.id, projectId: originalCert.projectId, profileName: profile?.slug || "External CA Profile", commonName: originalCert.commonName || "" @@ -1734,6 +1844,7 @@ export const certificateV3ServiceFactory = ({ certificateChain: finalCertificateChain, serialNumber: renewalResult.serialNumber, certificateId: renewalResult.newCert.id, + certificateRequestId, projectId: renewalResult.originalCert.projectId, profileName: renewalResult.profile?.slug || "Self-signed Certificate", commonName: renewalResult.originalCert.commonName || "" diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index d448cb770..6ea57c6a0 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -1,6 +1,6 @@ import { TProjectPermission } from "@app/lib/types"; -import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; +import { ACMESANType } from "../certificate/certificate-types"; import { CertExtendedKeyUsageType, CertKeyUsageType, @@ -59,9 +59,9 @@ export type TOrderCertificateFromProfileDTO = { signatureAlgorithm?: string; keyAlgorithm?: string; template?: string; + csr?: string; }; removeRootsFromChain?: boolean; - certificateRequestId?: string; } & Omit; export type TCertificateFromProfileResponse = { @@ -71,34 +71,14 @@ export type TCertificateFromProfileResponse = { privateKey?: string; serialNumber: string; certificateId: string; + certificateRequestId: string; projectId: string; profileName: string; commonName: string; }; export type TCertificateOrderResponse = { - orderId: string; - status: CertificateOrderStatus; - subjectAlternativeNames: Array<{ - type: ACMESANType; - value: string; - status: CertificateOrderStatus; - }>; - authorizations: Array<{ - identifier: { - type: ACMESANType; - value: string; - }; - status: CertificateOrderStatus; - expires?: string; - challenges: Array<{ - type: string; - status: CertificateOrderStatus; - url: string; - token: string; - }>; - }>; - finalize: string; + certificateRequestId: string; certificate?: string; projectId: string; profileName: string; diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 2d32a007b..4d34822b3 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -83,32 +83,40 @@ export type TDownloadPkcs12DTO = { export type TUnifiedCertificateIssuanceDTO = { projectSlug: string; + profileId: string; projectId: string; - profileId?: string; - caId?: string; csr?: string; - commonName?: string; - altNames?: string; - keyUsages?: string[]; - extendedKeyUsages?: string[]; - notBefore?: Date; - notAfter?: Date; - keyAlgorithm?: string; - signatureAlgorithm?: string; - ttl?: string; - friendlyName?: string; - pkiCollectionId?: string; - issuerType?: string; + attributes?: { + commonName?: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + altNames?: Array<{ + type: string; + value: string; + }>; + signatureAlgorithm: string; + keyAlgorithm: string; + subjectAlternativeNames?: Array<{ + type: string; + value: string; + }>; + ttl: string; + notBefore?: string; + notAfter?: string; + }; + removeRootsFromChain?: boolean; }; export type TUnifiedCertificateResponse = { - certificate: string; - issuingCaCertificate: string; - certificateChain: string; - privateKey?: string; - serialNumber: string; - certificateId: string; - projectId: string; + certificate: { + certificate: string; + issuingCaCertificate: string; + certificateChain: string; + privateKey?: string; + serialNumber: string; + certificateId: string; + }; + certificateRequestId: string; }; export type TCertificateRequestResponse = { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index c544e2a66..b5531dacf 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -107,6 +107,7 @@ type TCertificateDetails = { certificate?: string; certificateChain?: string; privateKey?: string; + issuingCaCertificate?: string; }; export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => { @@ -280,41 +281,42 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } profileId: formProfileId, projectSlug: currentProject.slug, projectId: currentProject.id, - ttl, - keyUsages: filterUsages(keyUsages) as CertKeyUsage[], - extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] + attributes: { + ttl, + signatureAlgorithm: signatureAlgorithm || "", + keyAlgorithm: keyAlgorithm || "", + keyUsages: filterUsages(keyUsages) as CertKeyUsage[], + extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] + } }; if (constraints.shouldShowSubjectSection && commonName) { - request.commonName = commonName; - } - - if (signatureAlgorithm) { - request.signatureAlgorithm = signatureAlgorithm; - } - - if (keyAlgorithm) { - request.keyAlgorithm = keyAlgorithm; + request.attributes.commonName = commonName; } if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { const formattedSans = formatSubjectAltNames(subjectAltNames); if (formattedSans && formattedSans.length > 0) { - request.altNames = formattedSans; + request.attributes.altNames = formattedSans; + request.attributes.subjectAlternativeNames = formattedSans; } } const response = await issueCertificate(request); // Handle certificate issuance response - if ("certificate" in response) { - // Immediate certificate issuance - setCertificateDetails({ - serialNumber: response.serialNumber, - certificate: response.certificate, - certificateChain: response.certificateChain, - privateKey: response.privateKey - }); + + if ("certificate" in response && response.certificate) { + const certData = response.certificate; + const certificateDetailsToSet = { + serialNumber: certData.serialNumber || "", + certificate: certData.certificate || "", + certificateChain: certData.certificateChain || "", + privateKey: certData.privateKey || "", + issuingCaCertificate: certData.issuingCaCertificate || "" + }; + + setCertificateDetails(certificateDetailsToSet); createNotification({ text: "Successfully created certificate",