Address PR comments and improvements

This commit is contained in:
Carlos Monastyrski
2025-12-02 02:11:12 -03:00
parent 95a26eca9b
commit 443e4226fb
19 changed files with 948 additions and 1156 deletions
@@ -388,6 +388,9 @@ export enum EventType {
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config", UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-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_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2846,7 +2849,6 @@ interface OrderCertificateFromProfile {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE; type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
metadata: { metadata: {
certificateProfileId: string; certificateProfileId: string;
orderId: string;
profileName: 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 = export type Event =
| CreateSubOrganizationEvent | CreateSubOrganizationEvent
| UpdateSubOrganizationEvent | UpdateSubOrganizationEvent
@@ -4575,6 +4602,9 @@ export type Event =
| PamResourceDeleteEvent | PamResourceDeleteEvent
| UpdateCertificateRenewalConfigEvent | UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent | DisableCertificateRenewalConfigEvent
| CreateCertificateRequestEvent
| GetCertificateRequestEvent
| GetCertificateFromRequestEvent
| AutomatedRenewCertificate | AutomatedRenewCertificate
| AutomatedRenewCertificateFailed | AutomatedRenewCertificateFailed
| UserLoginEvent | UserLoginEvent
+2 -1
View File
@@ -2259,7 +2259,8 @@ export const registerRoutes = async (
kmsService, kmsService,
projectDAL, projectDAL,
certificateBodyDAL, certificateBodyDAL,
certificateIssuanceQueue certificateIssuanceQueue,
certificateRequestService
}); });
const certificateV3Queue = certificateV3QueueServiceFactory({ const certificateV3Queue = certificateV3QueueServiceFactory({
@@ -665,8 +665,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = const response = await server.services.internalCertificateAuthority.issueCertFromCa({
await server.services.internalCertificateAuthority.issueCertFromCa({
caId: req.params.caId, caId: req.params.caId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -675,6 +674,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
...req.body ...req.body
}); });
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: ca.projectId, projectId: ca.projectId,
@@ -299,7 +299,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
certificateAuthority: z certificateAuthority: z
.object({ .object({
id: z.string(), id: z.string(),
projectId: z.string().optional(), projectId: z.string(),
status: z.string(), status: z.string(),
name: z.string(), name: z.string(),
isExternal: z.boolean().optional(), isExternal: z.boolean().optional(),
+125 -335
View File
@@ -2,7 +2,7 @@
import RE2 from "re2"; import RE2 from "re2";
import { z } from "zod"; 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs"; import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
import { NotFoundError } from "@app/lib/errors"; 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 { AuthMode } from "@app/services/auth/auth-type";
import { import {
ACMESANType, ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm, CertKeyAlgorithm,
CertSignatureAlgorithm, CertSignatureAlgorithm,
CrlReason CrlReason
@@ -30,9 +29,21 @@ import { mapEnumsForValidation } from "@app/services/certificate-common/certific
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types";
import { booleanSchema } from "../sanitizedSchemas"; import { booleanSchema } from "../sanitizedSchemas";
type CertificateServiceResponse = TCertificateFromProfileResponse | Omit<TCertificateFromProfileResponse, "privateKey">;
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 { interface CertificateRequestForService {
commonName?: string; commonName?: string;
keyUsages?: CertKeyUsageType[]; keyUsages?: CertKeyUsageType[];
@@ -50,13 +61,32 @@ interface CertificateRequestForService {
keyAlgorithm?: string; 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 hasDateFields = data.notBefore || data.notAfter;
const hasTtl = data.ttl; const hasTtl = data.ttl;
return !(hasDateFields && hasTtl); 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) { if (data.notBefore && data.notAfter) {
const notBefore = new Date(data.notBefore); const notBefore = new Date(data.notBefore);
const notAfter = new Date(data.notAfter); const notAfter = new Date(data.notAfter);
@@ -65,20 +95,6 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
return true; 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) => { export const registerCertificateRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -92,7 +108,14 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
body: z body: z
.object({ .object({
profileId: z.string().uuid(), profileId: z.string().uuid(),
projectId: z.string().uuid(), csr: z
.string()
.trim()
.min(1, "CSR cannot be empty")
.max(4096, "CSR cannot exceed 4096 characters")
.optional(),
attributes: z
.object({
commonName: validateTemplateRegexField.optional(), commonName: validateTemplateRegexField.optional(),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
@@ -104,14 +127,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}) })
) )
.optional(), .optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(),
csr: z
.string()
.trim()
.min(1, "CSR cannot be empty")
.max(4096, "CSR cannot exceed 4096 characters")
.optional(),
subjectAlternativeNames: z subjectAlternativeNames: z
.array( .array(
z.object({ z.object({
@@ -130,7 +147,9 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
.min(1, "TTL cannot be empty") .min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"), .refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(), notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(), notAfter: validateCaDateField.optional()
})
.optional(),
removeRootsFromChain: booleanSchema.default(false).optional() removeRootsFromChain: booleanSchema.default(false).optional()
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
@@ -139,130 +158,27 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}) })
.refine(validateDateOrder, { .refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter" 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: { response: {
200: z.union([ 200: z.object({
z.object({ certificate: z
.object({
certificate: z.string().trim(), certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(), issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(), certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(), privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(), serialNumber: z.string().trim(),
certificateId: z.string(), certificateId: z.string()
})
.nullable()
.optional(),
certificateRequestId: 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()
})
])
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { csr, subjectAlternativeNames, ...requestBody } = req.body; const { csr, attributes, ...requestBody } = req.body;
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
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
})
});
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({ const profile = await server.services.certificateProfile.getProfileById({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -284,10 +200,10 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
useOrderFlow = caType !== CaType.INTERNAL; useOrderFlow = caType !== CaType.INTERNAL;
} }
if (subjectAlternativeNames?.length || useOrderFlow) { if (attributes?.subjectAlternativeNames?.length || useOrderFlow) {
let acmeAltNames = subjectAlternativeNames; let acmeAltNames: Array<{ type: ACMESANType; value: string }> | undefined = attributes?.subjectAlternativeNames;
if (useOrderFlow && !subjectAlternativeNames && requestBody.altNames?.length) { if (useOrderFlow && !attributes?.subjectAlternativeNames && attributes?.altNames?.length) {
acmeAltNames = requestBody.altNames.map((alt) => ({ acmeAltNames = attributes.altNames.map((alt: { type: CertSubjectAlternativeNameType; value: string }) => ({
type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME
? ACMESANType.DNS ? ACMESANType.DNS
: ACMESANType.IP) as ACMESANType, : ACMESANType.IP) as ACMESANType,
@@ -297,14 +213,15 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
const certificateOrderObject = { const certificateOrderObject = {
altNames: acmeAltNames || [], altNames: acmeAltNames || [],
validity: { ttl: requestBody.ttl }, validity: { ttl: attributes?.ttl || "" },
commonName: requestBody.commonName, commonName: attributes?.commonName,
keyUsages: requestBody.keyUsages, keyUsages: attributes?.keyUsages,
extendedKeyUsages: requestBody.extendedKeyUsages, extendedKeyUsages: attributes?.extendedKeyUsages,
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
signatureAlgorithm: requestBody.signatureAlgorithm, signatureAlgorithm: attributes?.signatureAlgorithm,
keyAlgorithm: requestBody.keyAlgorithm keyAlgorithm: attributes?.keyAlgorithm,
csr
}; };
const data = await server.services.certificateV3.orderCertificateFromProfile({ const data = await server.services.certificateV3.orderCertificateFromProfile({
@@ -314,14 +231,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
profileId: requestBody.profileId, profileId: requestBody.profileId,
certificateOrder: certificateOrderObject, certificateOrder: certificateOrderObject,
removeRootsFromChain: requestBody.removeRootsFromChain, removeRootsFromChain: requestBody.removeRootsFromChain
certificateRequestId: certificateRequest.id
});
await server.services.certificateRequest.updateCertificateRequestStatus({
certificateRequestId: certificateRequest.id,
status: CertificateRequestStatus.PENDING,
errorMessage: undefined
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -331,27 +241,63 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: { metadata: {
certificateProfileId: requestBody.profileId, certificateProfileId: requestBody.profileId,
orderId: data.orderId,
profileName: data.profileName profileName: data.profileName
} }
} }
}); });
return { return {
certificateRequestId: certificateRequest.id, certificate: null,
...data certificateRequestId: data.certificateRequestId
}; };
} }
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 = { const certificateRequestForService: CertificateRequestForService = {
commonName: requestBody.commonName, commonName: attributes?.commonName,
keyUsages: requestBody.keyUsages, keyUsages: attributes?.keyUsages,
extendedKeyUsages: requestBody.extendedKeyUsages, extendedKeyUsages: attributes?.extendedKeyUsages,
altNames: requestBody.altNames, altNames: attributes?.altNames,
validity: { ttl: requestBody.ttl }, validity: { ttl: attributes?.ttl || "" },
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined, notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined, notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
signatureAlgorithm: requestBody.signatureAlgorithm, signatureAlgorithm: attributes?.signatureAlgorithm,
keyAlgorithm: requestBody.keyAlgorithm keyAlgorithm: attributes?.keyAlgorithm
}; };
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService); const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
@@ -366,11 +312,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
removeRootsFromChain: requestBody.removeRootsFromChain removeRootsFromChain: requestBody.removeRootsFromChain
}); });
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -379,24 +320,15 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
certificateProfileId: requestBody.profileId, certificateProfileId: requestBody.profileId,
certificateId: data.certificateId, certificateId: data.certificateId,
commonName: requestBody.commonName || "", commonName: attributes?.commonName || "",
profileName: data.profileName profileName: data.profileName
} }
} }
}); });
return { return {
...data, certificate: extractCertificateData(data),
certificateRequestId: certificateRequest.id 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;
}
} }
}); });
server.route({ server.route({
@@ -437,7 +369,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
certificateRequestId: req.params.requestId certificateRequestId: req.params.requestId
}); });
if (data.certificate && data.serialNumber) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: (req.query as { projectId: string }).projectId, projectId: (req.query as { projectId: string }).projectId,
@@ -450,8 +381,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
} }
} }
}); });
}
return data; return data;
} }
}); });
@@ -539,28 +468,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
removeRootsFromChain: req.body.removeRootsFromChain 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -575,10 +482,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
} }
}); });
return { return data;
...data,
certificateRequestId: certificateRequest.id
};
} }
}); });
@@ -644,29 +548,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr); 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -681,10 +562,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
} }
}); });
return { return data;
...data,
certificateRequestId: certificateRequest.id
};
} }
}); });
@@ -735,34 +613,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 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(), certificate: z.string().optional(),
certificateRequestId: z.string() certificateRequestId: z.string()
}) })
@@ -794,23 +644,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
removeRootsFromChain: req.body.removeRootsFromChain 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -818,16 +651,12 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: { metadata: {
certificateProfileId: req.body.profileId, certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName profileName: data.profileName
} }
} }
}); });
return { return data;
...data,
certificateRequestId: certificateRequest.id
};
} }
}); });
@@ -862,9 +691,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
let certificateRequest: TCertificateRequests | undefined;
try {
const originalCertificate = await server.services.certificate.getCert({ const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -876,38 +702,15 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
throw new NotFoundError({ message: "Original certificate not found" }); 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({ const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
certificateId: req.params.id, certificateId: req.params.id,
removeRootsFromChain: req.body?.removeRootsFromChain, 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -922,20 +725,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
} }
}); });
return { return data;
...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;
}
} }
}); });
@@ -242,8 +242,7 @@ export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) =
} }
}, },
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = const response = await server.services.internalCertificateAuthority.issueCertFromCa({
await server.services.internalCertificateAuthority.issueCertFromCa({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -251,6 +250,8 @@ export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) =
...req.body ...req.body
}); });
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: ca.projectId, projectId: ca.projectId,
@@ -1,6 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { TCertificateRequests } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
@@ -8,12 +7,7 @@ import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { import { ACMESANType, CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types";
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
@@ -23,7 +17,6 @@ import {
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; 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 { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas"; import { booleanSchema } from "../sanitizedSchemas";
@@ -340,34 +333,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 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(), certificate: z.string().optional(),
certificateRequestId: z.string() certificateRequestId: z.string()
}) })
@@ -423,7 +388,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: { metadata: {
certificateProfileId: req.body.profileId, certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName profileName: data.profileName
} }
} }
@@ -467,9 +431,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
let certificateRequest: TCertificateRequests | undefined;
try {
const originalCertificate = await server.services.certificate.getCert({ const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -481,38 +442,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
throw new NotFoundError({ message: "Original certificate not found" }); 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({ const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId, certificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain, 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -527,20 +465,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
} }
}); });
return { return data;
...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;
}
} }
}); });
@@ -1,12 +1,11 @@
import acme from "acme-client"; import acme from "acme-client";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; 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 { logger } from "@app/lib/logger";
import { QueueJobs, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, TQueueServiceFactory } from "@app/queue";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; 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 { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; 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 { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { CertKeyAlgorithm } from "../certificate-common/certificate-constants";
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service"; import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
import { CertificateRequestStatus } from "../certificate-request/certificate-request-types"; import { CertificateRequestStatus } from "../certificate-request/certificate-request-types";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
@@ -41,6 +41,7 @@ export type TIssueCertificateFromProfileJobData = {
isRenewal?: boolean; isRenewal?: boolean;
originalCertificateId?: string; originalCertificateId?: string;
certificateRequestId?: string; certificateRequestId?: string;
csr?: string;
}; };
type TCertificateIssuanceQueueFactoryDep = { type TCertificateIssuanceQueueFactoryDep = {
@@ -86,40 +87,6 @@ export const certificateIssuanceQueueFactory = ({
certificateProfileDAL, certificateProfileDAL,
certificateRequestService certificateRequestService
}: TCertificateIssuanceQueueFactoryDep) => { }: 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({ const acmeFns = AcmeCertificateAuthorityFns({
appConnectionDAL, appConnectionDAL,
appConnectionService, appConnectionService,
@@ -168,7 +135,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages, extendedKeyUsages,
isRenewal, isRenewal,
originalCertificateId, originalCertificateId,
certificateRequestId certificateRequestId,
csr
}: TIssueCertificateFromProfileJobData) => { }: TIssueCertificateFromProfileJobData) => {
const jobData: TIssueCertificateFromProfileJobData = { const jobData: TIssueCertificateFromProfileJobData = {
certificateId, certificateId,
@@ -183,7 +151,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages, extendedKeyUsages,
isRenewal, isRenewal,
originalCertificateId, originalCertificateId,
certificateRequestId certificateRequestId,
csr
}; };
await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, { await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, {
@@ -210,7 +179,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages, extendedKeyUsages,
isRenewal, isRenewal,
originalCertificateId, originalCertificateId,
certificateRequestId certificateRequestId,
csr
} = data; } = data;
try { try {
@@ -225,32 +195,36 @@ export const certificateIssuanceQueueFactory = ({
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (ca.externalCa?.type === CaType.ACME) { if (ca.externalCa?.type === CaType.ACME) {
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm); let certificateCsr: string;
if (!validatedKeyAlgorithm) { let skLeaf: string = "";
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
} if (csr) {
const keyAlg = keyAlgorithmToAlgCfg(validatedKeyAlgorithm); certificateCsr = csr;
} else {
const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]); const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const [, certificateCsr] = await acme.crypto.createCsr( const [, generatedCsr] = await acme.crypto.createCsr(
{ {
altNames: altNames || [], altNames: altNames || [],
commonName: commonName || "" commonName: commonName || ""
}, },
skLeaf skLeaf
); );
certificateCsr = generatedCsr.toString();
}
const acmeResult = await acmeFns.orderCertificateFromProfile({ const acmeResult = await acmeFns.orderCertificateFromProfile({
caId, caId,
profileId, profileId,
commonName: commonName || "", commonName: commonName || "",
altNames: altNames || [], altNames: altNames || [],
csr: certificateCsr, csr: Buffer.from(certificateCsr),
csrPrivateKey: skLeaf, csrPrivateKey: skLeaf,
keyUsages: validateKeyUsages(keyUsages), keyUsages: keyUsages as CertKeyUsage[],
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages), extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
ttl, ttl,
signatureAlgorithm, signatureAlgorithm,
keyAlgorithm, keyAlgorithm,
@@ -306,24 +280,20 @@ export const certificateIssuanceQueueFactory = ({
} }
} }
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm);
if (!validatedKeyAlgorithm) {
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
}
const azureParams = { const azureParams = {
caId, caId,
profileId, profileId,
commonName: commonName || "", commonName: commonName || "",
altNames: altNames || [], altNames: altNames || [],
keyUsages: validateKeyUsages(keyUsages), keyUsages: keyUsages as CertKeyUsage[],
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages), extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
validity: { ttl }, validity: { ttl },
signatureAlgorithm, signatureAlgorithm,
keyAlgorithm: validatedKeyAlgorithm, keyAlgorithm: keyAlgorithm as CertKeyAlgorithm,
isRenewal, isRenewal,
originalCertificateId, originalCertificateId,
template template,
...(csr && { csr })
}; };
const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams); const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams);
@@ -383,8 +353,6 @@ export const certificateIssuanceQueueFactory = ({
}; };
const initializeCertificateIssuanceQueue = async () => { const initializeCertificateIssuanceQueue = async () => {
const appCfg = getConfig();
await queueService.startPg( await queueService.startPg(
QueueJobs.CaIssueCertificateFromProfile, QueueJobs.CaIssueCertificateFromProfile,
async ([job]) => { async ([job]) => {
@@ -392,8 +360,9 @@ export const certificateIssuanceQueueFactory = ({
await processCertificateIssuanceJobs(data); await processCertificateIssuanceJobs(data);
}, },
{ {
workerCount: appCfg.NODE_ENV === "production" ? 3 : 1, workerCount: 2,
batchSize: 1 batchSize: 1,
pollingIntervalSeconds: 1
} }
); );
@@ -67,6 +67,7 @@ import {
TGetCaDTO, TGetCaDTO,
TImportCertToCaDTO, TImportCertToCaDTO,
TIssueCertFromCaDTO, TIssueCertFromCaDTO,
TIssueCertFromCaResponse,
TRenewCaCertDTO, TRenewCaCertDTO,
TSignCertFromCaDTO, TSignCertFromCaDTO,
TSignIntermediateDTO, TSignIntermediateDTO,
@@ -1198,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({
isFromProfile, isFromProfile,
internal = false, internal = false,
tx tx
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO): Promise<TIssueCertFromCaResponse> => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined; let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@@ -1532,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({
return cert; return cert;
}; };
let cert;
if (tx) { if (tx) {
await executeIssueCertOperations(tx); cert = await executeIssueCertOperations(tx);
} else { } else {
await certificateDAL.transaction(executeIssueCertOperations); cert = await certificateDAL.transaction(executeIssueCertOperations);
} }
return { return {
@@ -1544,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({
issuingCaCertificate, issuingCaCertificate,
privateKey: skLeaf, privateKey: skLeaf,
serialNumber, serialNumber,
certificateId: cert.id,
commonName,
ca: expandInternalCa(ca) ca: expandInternalCa(ca)
}; };
}; };
@@ -1905,8 +1909,8 @@ export const internalCertificateAuthorityServiceFactory = ({
plainText: Buffer.from(certificateChainPem) plainText: Buffer.from(certificateChainPem)
}); });
await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create( const newCert = await certificateDAL.create(
{ {
caId: (ca as TCertificateAuthorities).id, caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id, caCertId: caCert.id,
@@ -1929,7 +1933,7 @@ export const internalCertificateAuthorityServiceFactory = ({
await certificateBodyDAL.create( await certificateBodyDAL.create(
{ {
certId: cert.id, certId: newCert.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain encryptedCertificateChain
}, },
@@ -1940,13 +1944,13 @@ export const internalCertificateAuthorityServiceFactory = ({
await pkiCollectionItemDAL.create( await pkiCollectionItemDAL.create(
{ {
pkiCollectionId: collectionId, pkiCollectionId: collectionId,
certId: cert.id certId: newCert.id
}, },
tx tx
); );
} }
return cert; return newCert;
}); });
return { return {
@@ -1954,6 +1958,7 @@ export const internalCertificateAuthorityServiceFactory = ({
certificateChain: certificateChainPem, certificateChain: certificateChainPem,
issuingCaCertificate, issuingCaCertificate,
serialNumber, serialNumber,
certificateId: cert.id,
ca: expandInternalCa(ca), ca: expandInternalCa(ca),
commonName: cn commonName: cn
}; };
@@ -248,3 +248,20 @@ export type TIssueCertWithTemplateDTO = {
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; 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;
};
@@ -56,7 +56,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
}; };
certificateAuthority?: { certificateAuthority?: {
id: string; id: string;
projectId?: string; projectId: string;
status: string; status: string;
name: string; name: string;
isExternal?: boolean; isExternal?: boolean;
@@ -1,36 +1,12 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; 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 { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } 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;
};
type TCertificateRequestWithCertificate = TCertificateRequests & { type TCertificateRequestWithCertificate = TCertificateRequests & {
certificate: TCertificateInfo | null; certificate: TCertificates | null;
}; };
export type TCertificateRequestDALFactory = ReturnType<typeof certificateRequestDALFactory>; export type TCertificateRequestDALFactory = ReturnType<typeof certificateRequestDALFactory>;
@@ -40,61 +16,24 @@ export const certificateRequestDALFactory = (db: TDbClient) => {
const findByIdWithCertificate = async (id: string): Promise<TCertificateRequestWithCertificate | null> => { const findByIdWithCertificate = async (id: string): Promise<TCertificateRequestWithCertificate | null> => {
try { try {
const certificateRequest = (await db(TableName.CertificateRequests) const certificateRequest = await certificateRequestOrm.findById(id);
.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;
if (!certificateRequest) return null; if (!certificateRequest) return null;
// Transform the flat result into nested structure if (!certificateRequest.certificateId) {
const { return {
certificateId, ...certificateRequest,
certificateSerialNumber, certificate: null
certificateFriendlyName, };
certificateCommonName, }
certificateAltNames,
certificateStatus, const certificate = await db(TableName.Certificate)
certificateNotBefore, .where("id", certificateRequest.certificateId)
certificateNotAfter, .select(selectAllTableCols(TableName.Certificate))
certificateKeyUsages, .first();
certificateExtendedKeyUsages,
...certificateRequestData
} = certificateRequest;
return { return {
...certificateRequestData, ...certificateRequest,
certificate: certificateId certificate: certificate || null
? {
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
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" }); 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<TCertificateRequests> => { const updateStatus = async (
id: string,
status: string,
errorMessage?: string,
tx?: Knex
): Promise<TCertificateRequests> => {
try { try {
const updateData: Partial<TCertificateRequests> = { status }; const updateData: Partial<TCertificateRequests> = { status };
if (errorMessage !== undefined) { if (errorMessage !== undefined) {
updateData.errorMessage = errorMessage; updateData.errorMessage = errorMessage;
} }
return await certificateRequestOrm.updateById(id, updateData); return await certificateRequestOrm.updateById(id, updateData, tx);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Update certificate request status" }); throw new DatabaseError({ error, name: "Update certificate request status" });
} }
}; };
const attachCertificate = async (id: string, certificateId: string): Promise<TCertificateRequests> => { const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise<TCertificateRequests> => {
try { try {
return await certificateRequestOrm.updateById(id, { return await certificateRequestOrm.updateById(
id,
{
certificateId, certificateId,
status: "issued" status: "issued"
}); },
tx
);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Attach certificate to request" }); throw new DatabaseError({ error, name: "Attach certificate to request" });
} }
@@ -13,7 +13,7 @@ import {
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } 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 { ActorType, AuthMethod } from "@app/services/auth/auth-type";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
@@ -202,7 +202,7 @@ describe("CertificateRequestService", () => {
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); (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" "550e8400-e29b-41d4-a716-446655440005"
); );
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
serialNumber: "123456", id: "550e8400-e29b-41d4-a716-446655440006",
actor: ActorType.USER, actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001", actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
}); });
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
serialNumber: "123456", id: "550e8400-e29b-41d4-a716-446655440006",
actor: ActorType.USER, actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001", actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
@@ -354,14 +354,14 @@ describe("CertificateRequestService", () => {
"550e8400-e29b-41d4-a716-446655440005" "550e8400-e29b-41d4-a716-446655440005"
); );
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
serialNumber: "123456", id: "550e8400-e29b-41d4-a716-446655440008",
actor: ActorType.USER, actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001", actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
}); });
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
serialNumber: "123456", id: "550e8400-e29b-41d4-a716-446655440008",
actor: ActorType.USER, actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001", actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
@@ -30,13 +31,12 @@ type TCertificateRequestServiceFactoryDep = {
export type TCertificateRequestServiceFactory = ReturnType<typeof certificateRequestServiceFactory>; export type TCertificateRequestServiceFactory = ReturnType<typeof certificateRequestServiceFactory>;
// Input validation schemas
const certificateRequestDataSchema = z const certificateRequestDataSchema = z
.object({ .object({
profileId: z.string().uuid().optional(), profileId: z.string().uuid().optional(),
caId: z.string().uuid().optional(), caId: z.string().uuid().optional(),
csr: z.string().min(1).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(), altNames: z.string().max(1000).optional(),
keyUsages: z.array(z.string()).max(20).optional(), keyUsages: z.array(z.string()).max(20).optional(),
extendedKeyUsages: z.array(z.string()).max(20).optional(), extendedKeyUsages: z.array(z.string()).max(20).optional(),
@@ -93,8 +93,9 @@ export const certificateRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
projectId, projectId,
tx,
...requestData ...requestData
}: TCreateCertificateRequestDTO) => { }: TCreateCertificateRequestDTO & { tx?: Knex }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -112,7 +113,16 @@ export const certificateRequestServiceFactory = ({
// Validate input data before creating the request // Validate input data before creating the request
const validatedData = validateCertificateRequestData(requestData); const validatedData = validateCertificateRequestData(requestData);
const certificateRequest = await certificateRequestDAL.create({ const certificateRequest = tx
? await certificateRequestDAL.create(
{
status: CertificateRequestStatus.PENDING,
projectId,
...validatedData
},
tx
)
: await certificateRequestDAL.create({
status: CertificateRequestStatus.PENDING, status: CertificateRequestStatus.PENDING,
projectId, projectId,
...validatedData ...validatedData
@@ -149,7 +159,7 @@ export const certificateRequestServiceFactory = ({
} }
if (certificateRequest.projectId !== projectId) { 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; return certificateRequest;
@@ -183,7 +193,7 @@ export const certificateRequestServiceFactory = ({
} }
if (certificateRequest.projectId !== projectId) { 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 // If no certificate is attached, return basic info
@@ -201,7 +211,7 @@ export const certificateRequestServiceFactory = ({
// Get certificate body (PEM data) // Get certificate body (PEM data)
const certBody = await certificateService.getCertBody({ const certBody = await certificateService.getCertBody({
serialNumber: certificateRequest.certificate.serialNumber, id: certificateRequest.certificate.id,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -212,7 +222,7 @@ export const certificateRequestServiceFactory = ({
let privateKey: string | null = null; let privateKey: string | null = null;
try { try {
const certPrivateKey = await certificateService.getCertPrivateKey({ const certPrivateKey = await certificateService.getCertPrivateKey({
serialNumber: certificateRequest.certificate.serialNumber, id: certificateRequest.certificate.id,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-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 { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
@@ -198,6 +198,9 @@ describe("CertificateV3Service", () => {
} as any, } as any,
certificateIssuanceQueue: { certificateIssuanceQueue: {
queueCertificateIssuance: vi.fn().mockResolvedValue(undefined) queueCertificateIssuance: vi.fn().mockResolvedValue(undefined)
},
certificateRequestService: {
createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" })
} }
}); });
}); });
@@ -287,6 +290,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca", issuingCaCertificate: "issuing-ca",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: { ca: {
id: "ca-123", id: "ca-123",
projectId: "project-123", projectId: "project-123",
@@ -346,8 +351,13 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
const result = await service.issueCertificateFromProfile({ const result = await service.issueCertificateFromProfile({
profileId, profileId,
certificateRequest: mockCertificateRequest, certificateRequest: mockCertificateRequest,
@@ -481,6 +491,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca", issuingCaCertificate: "issuing-ca",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: { ca: {
id: "ca-123", id: "ca-123",
projectId: "project-123", projectId: "project-123",
@@ -523,8 +535,34 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); 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.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
await service.issueCertificateFromProfile({ await service.issueCertificateFromProfile({
profileId, profileId,
certificateRequest: camelCaseRequest, certificateRequest: camelCaseRequest,
@@ -749,8 +787,13 @@ describe("CertificateV3Service", () => {
}); });
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any); vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
const result = await service.signCertificateFromProfile({ const result = await service.signCertificateFromProfile({
profileId, profileId,
csr: mockCSR, csr: mockCSR,
@@ -819,168 +862,6 @@ describe("CertificateV3Service", () => {
keyAlgorithm: "RSA_2048" 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 () => { it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
const profileId = "profile-123"; const profileId = "profile-123";
const mockProfile = { const mockProfile = {
@@ -1117,6 +998,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert", issuingCaCertificate: "ca-cert",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: rsaCa as any ca: rsaCa as any
}); });
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1163,6 +1046,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null, pkiSubscriberId: null,
profileId: 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<unknown>) => {
return callback(undefined as any);
});
// Should not throw - RSA CA is compatible with RSA signature algorithms // Should not throw - RSA CA is compatible with RSA signature algorithms
await expect( await expect(
@@ -1249,6 +1157,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert", issuingCaCertificate: "ca-cert",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: ecCa as any ca: ecCa as any
}); });
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1295,6 +1205,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null, pkiSubscriberId: null,
profileId: 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<unknown>) => {
return callback(undefined as any);
});
// Should not throw - EC CA is compatible with ECDSA signature algorithms // Should not throw - EC CA is compatible with ECDSA signature algorithms
await expect( await expect(
@@ -1381,6 +1316,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert", issuingCaCertificate: "ca-cert",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: rsa8192Ca as any ca: rsa8192Ca as any
}); });
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1427,6 +1364,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null, pkiSubscriberId: null,
profileId: 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<unknown>) => {
return callback(undefined as any);
});
// Should not throw - dynamic check supports new RSA key sizes // Should not throw - dynamic check supports new RSA key sizes
await expect( await expect(
@@ -1513,6 +1475,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert", issuingCaCertificate: "ca-cert",
privateKey: "key", privateKey: "key",
serialNumber: "123456", serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: newEcCa as any ca: newEcCa as any
}); });
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1559,6 +1523,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null, pkiSubscriberId: null,
profileId: 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<unknown>) => {
return callback(undefined as any);
});
// Should not throw - dynamic check supports new EC curves // Should not throw - dynamic check supports new EC curves
await expect( await expect(
@@ -1692,8 +1681,9 @@ describe("CertificateV3Service", () => {
}); });
it("should successfully renew eligible certificate", async () => { it("should successfully renew eligible certificate", async () => {
// Mock the initial findById call vi.mocked(mockCertificateDAL.findById)
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert); .mockResolvedValueOnce(mockOriginalCert)
.mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" });
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any); vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
@@ -1709,6 +1699,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca", issuingCaCertificate: "issuing-ca",
privateKey: "private-key", privateKey: "private-key",
serialNumber: "789012", serialNumber: "789012",
certificateId: "cert-456",
commonName: "test.example.com",
ca: mockCA ca: mockCA
}); });
@@ -2026,6 +2018,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca", issuingCaCertificate: "issuing-ca",
privateKey: "private-key", privateKey: "private-key",
serialNumber: "789012", serialNumber: "789012",
certificateId: "cert-456",
commonName: "test.example.com",
ca: mockCA ca: mockCA
}); });
@@ -20,7 +20,6 @@ import { TCertificateDALFactory } from "@app/services/certificate/certificate-da
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
CertificateOrderStatus,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyType, CertKeyType,
CertKeyUsage, CertKeyUsage,
@@ -40,11 +39,7 @@ import {
} from "@app/services/certificate-authority/certificate-authority-fns"; } from "@app/services/certificate-authority/certificate-authority-fns";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
EnrollmentType,
IssuerType,
TCertificateProfileWithConfigs
} from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -71,7 +66,9 @@ import {
normalizeDateForApi, normalizeDateForApi,
removeRootCaFromChain removeRootCaFromChain
} from "../certificate-common/certificate-utils"; } from "../certificate-common/certificate-utils";
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; 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 { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils"; import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
@@ -122,6 +119,7 @@ type TCertificateV3ServiceFactoryDep = {
import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory, import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory,
"queueCertificateIssuance" "queueCertificateIssuance"
>; >;
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
}; };
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>; export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
@@ -861,7 +859,8 @@ export const certificateV3ServiceFactory = ({
pkiSyncQueue, pkiSyncQueue,
kmsService, kmsService,
projectDAL, projectDAL,
certificateIssuanceQueue certificateIssuanceQueue,
certificateRequestService
}: TCertificateV3ServiceFactoryDep) => { }: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({ const issueCertificateFromProfile = async ({
profileId, profileId,
@@ -945,7 +944,7 @@ export const certificateV3ServiceFactory = ({
const result = await certificateDAL.transaction(async (tx) => { const result = await certificateDAL.transaction(async (tx) => {
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm); const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
return processSelfSignedCertificate({ const selfSignedResult = await processSelfSignedCertificate({
certificateRequest, certificateRequest,
template, template,
profile, profile,
@@ -957,9 +956,29 @@ export const certificateV3ServiceFactory = ({
projectDAL, projectDAL,
tx 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
}); });
const { selfSignedResult, certificateData } = result; return { ...selfSignedResult, certificateRequestId: certRequestResult.id };
});
const { selfSignedResult, certificateData, certificateRequestId } = result;
const subjectCommonName = const subjectCommonName =
(selfSignedResult.certificateSubject.common_name as string) || (selfSignedResult.certificateSubject.common_name as string) ||
@@ -985,6 +1004,7 @@ export const certificateV3ServiceFactory = ({
privateKey: selfSignedResult.privateKey.toString("utf8"), privateKey: selfSignedResult.privateKey.toString("utf8"),
serialNumber: selfSignedResult.serialNumber, serialNumber: selfSignedResult.serialNumber,
certificateId: certificateData.id, certificateId: certificateData.id,
certificateRequestId,
projectId: profile.projectId, projectId: profile.projectId,
profileName: profile.slug, profileName: profile.slug,
commonName: subjectCommonName commonName: subjectCommonName
@@ -1003,8 +1023,16 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "direct certificate issuance"); validateCaSupport(ca, "direct certificate issuance");
validateAlgorithmCompatibility(ca, template); validateAlgorithmCompatibility(ca, template);
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } = const {
await internalCaService.issueCertFromCa({ certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber,
cert,
certificateRequestId
} = await certificateDAL.transaction(async (tx) => {
const certResult = await internalCaService.issueCertFromCa({
caId: ca.id, caId: ca.id,
friendlyName: certificateSubject.common_name || "Certificate", friendlyName: certificateSubject.common_name || "Certificate",
commonName: certificateSubject.common_name || "", commonName: certificateSubject.common_name || "",
@@ -1023,22 +1051,43 @@ export const certificateV3ServiceFactory = ({
isFromProfile: true isFromProfile: true
}); });
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); const certificateRecord = await certificateDAL.findById(certResult.certificateId);
if (!cert) { if (!certificateRecord) {
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" }); throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
} }
const finalRenewBeforeDays = calculateFinalRenewBeforeDays( const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile, profile,
certificateRequest.validity.ttl, certificateRequest.validity.ttl,
new Date(cert.notAfter) new Date(certificateRecord.notAfter)
); );
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) { if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays; updateData.renewBeforeDays = finalRenewBeforeDays;
} }
await certificateDAL.updateById(cert.id, updateData); 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); let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) { if (removeRootsFromChain) {
@@ -1052,6 +1101,7 @@ export const certificateV3ServiceFactory = ({
privateKey: bufferToString(privateKey), privateKey: bufferToString(privateKey),
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
certificateRequestId,
projectId: profile.projectId, projectId: profile.projectId,
profileName: profile.slug, profileName: profile.slug,
commonName: cert.commonName || "" commonName: cert.commonName || ""
@@ -1135,8 +1185,9 @@ export const certificateV3ServiceFactory = ({
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm; const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
const effectiveKeyAlgorithm = extractedKeyAlgorithm; const effectiveKeyAlgorithm = extractedKeyAlgorithm;
const { certificate, certificateChain, issuingCaCertificate, serialNumber } = const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } =
await internalCaService.signCertFromCa({ await certificateDAL.transaction(async (tx) => {
const certResult = await internalCaService.signCertFromCa({
isInternal: true, isInternal: true,
caId: ca.id, caId: ca.id,
csr, csr,
@@ -1149,18 +1200,44 @@ export const certificateV3ServiceFactory = ({
isFromProfile: true isFromProfile: true
}); });
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); const signedCertRecord = await certificateDAL.findById(certResult.certificateId);
if (!cert) { if (!signedCertRecord) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); 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 finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile,
validity.ttl,
new Date(signedCertRecord.notAfter)
);
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId }; const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) { if (finalRenewBeforeDays !== undefined) {
updateData2.renewBeforeDays = finalRenewBeforeDays; updateData.renewBeforeDays = finalRenewBeforeDays;
} }
await certificateDAL.updateById(cert.id, updateData2); 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 certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer); const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer); let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
@@ -1174,6 +1251,7 @@ export const certificateV3ServiceFactory = ({
certificateChain: certificateChainString, certificateChain: certificateChainString,
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
certificateRequestId,
projectId: profile.projectId, projectId: profile.projectId,
profileName: profile.slug, profileName: profile.slug,
commonName: cert.commonName || "" commonName: cert.commonName || ""
@@ -1186,9 +1264,7 @@ export const certificateV3ServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
removeRootsFromChain,
certificateRequestId
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => { }: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -1202,7 +1278,22 @@ export const certificateV3ServiceFactory = ({
EnrollmentType.API EnrollmentType.API
); );
const certificateRequest = { 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, commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages, keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages, extendedKeyUsages: certificateOrder.extendedKeyUsages,
@@ -1231,8 +1322,15 @@ export const certificateV3ServiceFactory = ({
signatureAlgorithm: certificateOrder.signatureAlgorithm, signatureAlgorithm: certificateOrder.signatureAlgorithm,
keyAlgorithm: certificateOrder.keyAlgorithm keyAlgorithm: certificateOrder.keyAlgorithm
}; };
}
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest); const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
if (certificateOrder.csr) {
mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm;
mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
}
const validationResult = await certificateTemplateV2Service.validateCertificateRequest( const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId, profile.certificateTemplateId,
mappedCertificateRequest mappedCertificateRequest
@@ -1258,64 +1356,53 @@ export const certificateV3ServiceFactory = ({
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
if (caType === CaType.INTERNAL) { if (caType === CaType.INTERNAL) {
const certificateResult = await issueCertificateFromProfile({ throw new BadRequestError({
profileId, message: "Certificate ordering is not supported for the specified CA type"
certificateRequest,
actor,
actorId,
actorAuthMethod,
actorOrgId,
removeRootsFromChain
}); });
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) { if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) {
const orderId = randomUUID(); 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({ await certificateIssuanceQueue.queueCertificateIssuance({
certificateId: orderId, certificateId: orderId,
profileId: profile.id, profileId: profile.id,
caId: profile.caId || "", caId: profile.caId || "",
ttl: certificateOrder.validity?.ttl || "1y", ttl: certificateOrder.validity?.ttl || "1y",
signatureAlgorithm: certificateOrder.signatureAlgorithm || "", signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
keyAlgorithm: certificateOrder.keyAlgorithm || "", keyAlgorithm: certificateRequest.keyAlgorithm || "",
commonName: certificateOrder.commonName || "", commonName: certificateRequest.commonName || "",
altNames: certificateOrder.altNames?.map((san) => san.value) || [], altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [], keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [],
extendedKeyUsages: certificateOrder.extendedKeyUsages extendedKeyUsages: certificateRequest.extendedKeyUsages
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages) ? convertEnumsToStringArray(certificateRequest.extendedKeyUsages)
: [], : [],
certificateRequestId certificateRequestId: certRequest.id,
csr: certificateOrder.csr
}); });
return { return {
orderId, certificateRequestId: certRequest.id,
status: CertificateOrderStatus.PENDING, projectId: certRequest.projectId,
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,
profileName: profile.slug 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 ({ const renewCertificate = async ({
certificateId, certificateId,
actor, actor,
@@ -1356,10 +1419,11 @@ export const certificateV3ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
internal = false, internal = false,
removeRootsFromChain, removeRootsFromChain
certificateRequestId }: Omit<TRenewCertificateDTO, "certificateRequestId"> & {
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => { internal?: boolean;
const renewalResult: TRenewalTransactionResult = await certificateDAL.transaction(async (tx) => { }): Promise<TCertificateFromProfileResponse> => {
const renewalResult = await certificateDAL.transaction(async (tx) => {
const originalCert = await certificateDAL.findById(certificateId, tx); const originalCert = await certificateDAL.findById(certificateId, tx);
if (!originalCert) { if (!originalCert) {
throw new NotFoundError({ message: "Certificate not found" }); throw new NotFoundError({ message: "Certificate not found" });
@@ -1576,7 +1640,7 @@ export const certificateV3ServiceFactory = ({
issuingCaCertificate = caResult.issuingCaCertificate; issuingCaCertificate = caResult.issuingCaCertificate;
serialNumber = caResult.serialNumber; serialNumber = caResult.serialNumber;
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx); const foundCert = await certificateDAL.findById(caResult.certificateId, tx);
if (!foundCert) { if (!foundCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); 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); 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 { return {
certificate, certificate,
certificateChain, certificateChain,
@@ -1672,10 +1757,13 @@ export const certificateV3ServiceFactory = ({
serialNumber, serialNumber,
newCert, newCert,
originalCert, originalCert,
profile profile,
certRequestResult
}; };
}); });
let certificateRequestId: string = renewalResult.certRequestResult?.id || "";
// Handle external CA renewals separately // Handle external CA renewals separately
if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) { if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) {
const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult; const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult;
@@ -1685,6 +1773,27 @@ export const certificateV3ServiceFactory = ({
? originalCert.altNames.split(",").map((san: string) => san.trim()) ? 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({ await certificateIssuanceQueue.queueCertificateIssuance({
certificateId: renewalOrderId, certificateId: renewalOrderId,
profileId: profile?.id || "", profileId: profile?.id || "",
@@ -1698,7 +1807,7 @@ export const certificateV3ServiceFactory = ({
extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)), extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)),
isRenewal: true, isRenewal: true,
originalCertificateId: certificateId, originalCertificateId: certificateId,
certificateRequestId certificateRequestId: certificateRequest.id
}); });
return { return {
@@ -1707,6 +1816,7 @@ export const certificateV3ServiceFactory = ({
issuingCaCertificate: "", issuingCaCertificate: "",
serialNumber: "", serialNumber: "",
certificateId: renewalOrderId, certificateId: renewalOrderId,
certificateRequestId: certificateRequest.id,
projectId: originalCert.projectId, projectId: originalCert.projectId,
profileName: profile?.slug || "External CA Profile", profileName: profile?.slug || "External CA Profile",
commonName: originalCert.commonName || "" commonName: originalCert.commonName || ""
@@ -1734,6 +1844,7 @@ export const certificateV3ServiceFactory = ({
certificateChain: finalCertificateChain, certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber, serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id, certificateId: renewalResult.newCert.id,
certificateRequestId,
projectId: renewalResult.originalCert.projectId, projectId: renewalResult.originalCert.projectId,
profileName: renewalResult.profile?.slug || "Self-signed Certificate", profileName: renewalResult.profile?.slug || "Self-signed Certificate",
commonName: renewalResult.originalCert.commonName || "" commonName: renewalResult.originalCert.commonName || ""
@@ -1,6 +1,6 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; import { ACMESANType } from "../certificate/certificate-types";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CertKeyUsageType, CertKeyUsageType,
@@ -59,9 +59,9 @@ export type TOrderCertificateFromProfileDTO = {
signatureAlgorithm?: string; signatureAlgorithm?: string;
keyAlgorithm?: string; keyAlgorithm?: string;
template?: string; template?: string;
csr?: string;
}; };
removeRootsFromChain?: boolean; removeRootsFromChain?: boolean;
certificateRequestId?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCertificateFromProfileResponse = { export type TCertificateFromProfileResponse = {
@@ -71,34 +71,14 @@ export type TCertificateFromProfileResponse = {
privateKey?: string; privateKey?: string;
serialNumber: string; serialNumber: string;
certificateId: string; certificateId: string;
certificateRequestId: string;
projectId: string; projectId: string;
profileName: string; profileName: string;
commonName: string; commonName: string;
}; };
export type TCertificateOrderResponse = { export type TCertificateOrderResponse = {
orderId: string; certificateRequestId: 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;
certificate?: string; certificate?: string;
projectId: string; projectId: string;
profileName: string; profileName: string;
+20 -12
View File
@@ -83,32 +83,40 @@ export type TDownloadPkcs12DTO = {
export type TUnifiedCertificateIssuanceDTO = { export type TUnifiedCertificateIssuanceDTO = {
projectSlug: string; projectSlug: string;
profileId: string;
projectId: string; projectId: string;
profileId?: string;
caId?: string;
csr?: string; csr?: string;
attributes?: {
commonName?: string; commonName?: string;
altNames?: string;
keyUsages?: string[]; keyUsages?: string[];
extendedKeyUsages?: string[]; extendedKeyUsages?: string[];
notBefore?: Date; altNames?: Array<{
notAfter?: Date; type: string;
keyAlgorithm?: string; value: string;
signatureAlgorithm?: string; }>;
ttl?: string; signatureAlgorithm: string;
friendlyName?: string; keyAlgorithm: string;
pkiCollectionId?: string; subjectAlternativeNames?: Array<{
issuerType?: string; type: string;
value: string;
}>;
ttl: string;
notBefore?: string;
notAfter?: string;
};
removeRootsFromChain?: boolean;
}; };
export type TUnifiedCertificateResponse = { export type TUnifiedCertificateResponse = {
certificate: {
certificate: string; certificate: string;
issuingCaCertificate: string; issuingCaCertificate: string;
certificateChain: string; certificateChain: string;
privateKey?: string; privateKey?: string;
serialNumber: string; serialNumber: string;
certificateId: string; certificateId: string;
projectId: string; };
certificateRequestId: string;
}; };
export type TCertificateRequestResponse = { export type TCertificateRequestResponse = {
@@ -107,6 +107,7 @@ type TCertificateDetails = {
certificate?: string; certificate?: string;
certificateChain?: string; certificateChain?: string;
privateKey?: string; privateKey?: string;
issuingCaCertificate?: string;
}; };
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => { export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
@@ -280,41 +281,42 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
profileId: formProfileId, profileId: formProfileId,
projectSlug: currentProject.slug, projectSlug: currentProject.slug,
projectId: currentProject.id, projectId: currentProject.id,
attributes: {
ttl, ttl,
signatureAlgorithm: signatureAlgorithm || "",
keyAlgorithm: keyAlgorithm || "",
keyUsages: filterUsages(keyUsages) as CertKeyUsage[], keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
}
}; };
if (constraints.shouldShowSubjectSection && commonName) { if (constraints.shouldShowSubjectSection && commonName) {
request.commonName = commonName; request.attributes.commonName = commonName;
}
if (signatureAlgorithm) {
request.signatureAlgorithm = signatureAlgorithm;
}
if (keyAlgorithm) {
request.keyAlgorithm = keyAlgorithm;
} }
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames); const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) { if (formattedSans && formattedSans.length > 0) {
request.altNames = formattedSans; request.attributes.altNames = formattedSans;
request.attributes.subjectAlternativeNames = formattedSans;
} }
} }
const response = await issueCertificate(request); const response = await issueCertificate(request);
// Handle certificate issuance response // Handle certificate issuance response
if ("certificate" in response) {
// Immediate certificate issuance if ("certificate" in response && response.certificate) {
setCertificateDetails({ const certData = response.certificate;
serialNumber: response.serialNumber, const certificateDetailsToSet = {
certificate: response.certificate, serialNumber: certData.serialNumber || "",
certificateChain: response.certificateChain, certificate: certData.certificate || "",
privateKey: response.privateKey certificateChain: certData.certificateChain || "",
}); privateKey: certData.privateKey || "",
issuingCaCertificate: certData.issuingCaCertificate || ""
};
setCertificateDetails(certificateDetailsToSet);
createNotification({ createNotification({
text: "Successfully created certificate", text: "Successfully created certificate",