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

View File

@@ -388,6 +388,9 @@ export enum EventType {
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
CREATE_CERTIFICATE_REQUEST = "create-certificate-request",
GET_CERTIFICATE_REQUEST = "get-certificate-request",
GET_CERTIFICATE_FROM_REQUEST = "get-certificate-from-request",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2846,7 +2849,6 @@ interface OrderCertificateFromProfile {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
orderId: string;
profileName: string;
};
}
@@ -4196,6 +4198,31 @@ interface DisableCertificateRenewalConfigEvent {
};
}
interface CreateCertificateRequestEvent {
type: EventType.CREATE_CERTIFICATE_REQUEST;
metadata: {
certificateRequestId: string;
profileId?: string;
caId?: string;
commonName?: string;
};
}
interface GetCertificateRequestEvent {
type: EventType.GET_CERTIFICATE_REQUEST;
metadata: {
certificateRequestId: string;
};
}
interface GetCertificateFromRequestEvent {
type: EventType.GET_CERTIFICATE_FROM_REQUEST;
metadata: {
certificateRequestId: string;
certificateId?: string;
};
}
export type Event =
| CreateSubOrganizationEvent
| UpdateSubOrganizationEvent
@@ -4575,6 +4602,9 @@ export type Event =
| PamResourceDeleteEvent
| UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent
| CreateCertificateRequestEvent
| GetCertificateRequestEvent
| GetCertificateFromRequestEvent
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed
| UserLoginEvent

View File

@@ -2259,7 +2259,8 @@ export const registerRoutes = async (
kmsService,
projectDAL,
certificateBodyDAL,
certificateIssuanceQueue
certificateIssuanceQueue,
certificateRequestService
});
const certificateV3Queue = certificateV3QueueServiceFactory({

View File

@@ -665,15 +665,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.internalCertificateAuthority.issueCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
const response = await server.services.internalCertificateAuthority.issueCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -299,7 +299,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
certificateAuthority: z
.object({
id: z.string(),
projectId: z.string().optional(),
projectId: z.string(),
status: z.string(),
name: z.string(),
isExternal: z.boolean().optional(),

View File

@@ -2,7 +2,7 @@
import RE2 from "re2";
import { z } from "zod";
import { CertificatesSchema, TCertificateRequests } from "@app/db/schemas";
import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
import { NotFoundError } from "@app/lib/errors";
@@ -13,7 +13,6 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm,
CrlReason
@@ -30,9 +29,21 @@ import { mapEnumsForValidation } from "@app/services/certificate-common/certific
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types";
import { booleanSchema } from "../sanitizedSchemas";
type CertificateServiceResponse = TCertificateFromProfileResponse | Omit<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 {
commonName?: string;
keyUsages?: CertKeyUsageType[];
@@ -50,13 +61,32 @@ interface CertificateRequestForService {
keyAlgorithm?: string;
}
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
const validateTtlAndDateFields = (data: {
attributes?: { notBefore?: string; notAfter?: string; ttl?: string };
notBefore?: string;
notAfter?: string;
ttl?: string;
}) => {
if (data.attributes) {
const hasDateFields = data.attributes.notBefore || data.attributes.notAfter;
const hasTtl = data.attributes.ttl;
return !(hasDateFields && hasTtl);
}
const hasDateFields = data.notBefore || data.notAfter;
const hasTtl = data.ttl;
return !(hasDateFields && hasTtl);
};
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
const validateDateOrder = (data: {
attributes?: { notBefore?: string; notAfter?: string };
notBefore?: string;
notAfter?: string;
}) => {
if (data.attributes?.notBefore && data.attributes?.notAfter) {
const notBefore = new Date(data.attributes.notBefore);
const notAfter = new Date(data.attributes.notAfter);
return notBefore < notAfter;
}
if (data.notBefore && data.notAfter) {
const notBefore = new Date(data.notBefore);
const notAfter = new Date(data.notAfter);
@@ -65,20 +95,6 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
return true;
};
const validateCertificateRequestFlow = (data: {
csr?: string;
subjectAlternativeNames?: Array<{ type: ACMESANType; value: string }>;
commonName?: string;
altNames?: Array<{ type: CertSubjectAlternativeNameType; value: string }>;
}) => {
const hasCSR = !!data.csr;
const hasSANs = !!data.subjectAlternativeNames?.length;
const hasStandardFields = !!(data.commonName || data.altNames?.length);
const flowCount = Number(hasCSR) + Number(hasSANs) + Number(hasStandardFields);
return flowCount === 1;
};
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -92,45 +108,48 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
body: z
.object({
profileId: z.string().uuid(),
projectId: z.string().uuid(),
commonName: validateTemplateRegexField.optional(),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
altNames: z
.array(
z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
value: z.string().min(1, "SAN value cannot be empty")
})
)
.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
csr: z
.string()
.trim()
.min(1, "CSR cannot be empty")
.max(4096, "CSR cannot exceed 4096 characters")
.optional(),
subjectAlternativeNames: z
.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z
.string()
.trim()
.min(1, "SAN value cannot be empty")
.max(255, "SAN value must be less than 255 characters")
})
)
attributes: z
.object({
commonName: validateTemplateRegexField.optional(),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
altNames: z
.array(
z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
value: z.string().min(1, "SAN value cannot be empty")
})
)
.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(),
subjectAlternativeNames: z
.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z
.string()
.trim()
.min(1, "SAN value cannot be empty")
.max(255, "SAN value must be less than 255 characters")
})
)
.optional(),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional()
})
.optional(),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
@@ -139,264 +158,177 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
})
.refine(validateCertificateRequestFlow, {
message:
"Must specify exactly one of: csr (for signing), subjectAlternativeNames (for ACME), or commonName/altNames (for issuance)"
}),
response: {
200: z.union([
z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string(),
certificateRequestId: z.string()
}),
z.object({
certificateRequestId: z.string(),
status: z.string(),
orderId: z.string().optional(),
subjectAlternativeNames: z
.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
)
.optional(),
authorizations: z
.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
)
.optional(),
finalize: z.string().optional()
})
])
200: z.object({
certificate: z
.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
.nullable()
.optional(),
certificateRequestId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { csr, subjectAlternativeNames, ...requestBody } = req.body;
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
const { csr, attributes, ...requestBody } = req.body;
const profile = await server.services.certificateProfile.getProfileById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.projectId,
profileId: requestBody.profileId,
csr,
commonName: requestBody.commonName,
altNames: requestBody.altNames ? JSON.stringify(requestBody.altNames) : undefined,
keyUsages: requestBody.keyUsages,
extendedKeyUsages: requestBody.extendedKeyUsages,
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
keyAlgorithm: requestBody.keyAlgorithm,
signatureAlgorithm: requestBody.signatureAlgorithm,
metadata: JSON.stringify({
ttl: requestBody.ttl,
removeRootsFromChain: requestBody.removeRootsFromChain,
subjectAlternativeNames
})
profileId: requestBody.profileId
});
try {
if (csr) {
const extractedCsrData = extractCertificateRequestFromCSR(csr);
const data = await server.services.certificateV3.signCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId,
csr,
validity: { ttl: requestBody.ttl },
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
enrollmentType: EnrollmentType.API,
removeRootsFromChain: requestBody.removeRootsFromChain
});
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: requestBody.profileId,
certificateId: data.certificateId,
profileName: data.profileName,
commonName: extractedCsrData.commonName || ""
}
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
}
const profile = await server.services.certificateProfile.getProfileById({
let useOrderFlow = false;
if (profile?.caId) {
const ca = await server.services.certificateAuthority.getCaById({
caId: profile.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId
actorOrgId: req.permission.orgId
});
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
useOrderFlow = caType !== CaType.INTERNAL;
}
let useOrderFlow = false;
if (profile?.caId) {
const ca = await server.services.certificateAuthority.getCaById({
caId: profile.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
useOrderFlow = caType !== CaType.INTERNAL;
if (attributes?.subjectAlternativeNames?.length || useOrderFlow) {
let acmeAltNames: Array<{ type: ACMESANType; value: string }> | undefined = attributes?.subjectAlternativeNames;
if (useOrderFlow && !attributes?.subjectAlternativeNames && attributes?.altNames?.length) {
acmeAltNames = attributes.altNames.map((alt: { type: CertSubjectAlternativeNameType; value: string }) => ({
type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME
? ACMESANType.DNS
: ACMESANType.IP) as ACMESANType,
value: alt.value
}));
}
if (subjectAlternativeNames?.length || useOrderFlow) {
let acmeAltNames = subjectAlternativeNames;
if (useOrderFlow && !subjectAlternativeNames && requestBody.altNames?.length) {
acmeAltNames = requestBody.altNames.map((alt) => ({
type: (alt.type === CertSubjectAlternativeNameType.DNS_NAME
? ACMESANType.DNS
: ACMESANType.IP) as ACMESANType,
value: alt.value
}));
}
const certificateOrderObject = {
altNames: acmeAltNames || [],
validity: { ttl: requestBody.ttl },
commonName: requestBody.commonName,
keyUsages: requestBody.keyUsages,
extendedKeyUsages: requestBody.extendedKeyUsages,
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
signatureAlgorithm: requestBody.signatureAlgorithm,
keyAlgorithm: requestBody.keyAlgorithm
};
const data = await server.services.certificateV3.orderCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId,
certificateOrder: certificateOrderObject,
removeRootsFromChain: requestBody.removeRootsFromChain,
certificateRequestId: certificateRequest.id
});
await server.services.certificateRequest.updateCertificateRequestStatus({
certificateRequestId: certificateRequest.id,
status: CertificateRequestStatus.PENDING,
errorMessage: undefined
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: requestBody.profileId,
orderId: data.orderId,
profileName: data.profileName
}
}
});
return {
certificateRequestId: certificateRequest.id,
...data
};
}
const certificateRequestForService: CertificateRequestForService = {
commonName: requestBody.commonName,
keyUsages: requestBody.keyUsages,
extendedKeyUsages: requestBody.extendedKeyUsages,
altNames: requestBody.altNames,
validity: { ttl: requestBody.ttl },
notBefore: requestBody.notBefore ? new Date(requestBody.notBefore) : undefined,
notAfter: requestBody.notAfter ? new Date(requestBody.notAfter) : undefined,
signatureAlgorithm: requestBody.signatureAlgorithm,
keyAlgorithm: requestBody.keyAlgorithm
const certificateOrderObject = {
altNames: acmeAltNames || [],
validity: { ttl: attributes?.ttl || "" },
commonName: attributes?.commonName,
keyUsages: attributes?.keyUsages,
extendedKeyUsages: attributes?.extendedKeyUsages,
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
signatureAlgorithm: attributes?.signatureAlgorithm,
keyAlgorithm: attributes?.keyAlgorithm,
csr
};
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
const data = await server.services.certificateV3.issueCertificateFromProfile({
const data = await server.services.certificateV3.orderCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId,
certificateRequest: mappedCertificateRequest,
certificateOrder: certificateOrderObject,
removeRootsFromChain: requestBody.removeRootsFromChain
});
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: requestBody.profileId,
certificateId: data.certificateId,
commonName: requestBody.commonName || "",
profileName: data.profileName
}
}
});
return {
...data,
certificateRequestId: certificateRequest.id
certificate: null,
certificateRequestId: data.certificateRequestId
};
} catch (error) {
await server.services.certificateRequest.updateCertificateRequestStatus({
certificateRequestId: certificateRequest.id,
status: CertificateRequestStatus.FAILED,
errorMessage: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
if (csr) {
const extractedCsrData = extractCertificateRequestFromCSR(csr);
const data = await server.services.certificateV3.signCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId,
csr,
validity: { ttl: attributes?.ttl || "" },
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
enrollmentType: EnrollmentType.API,
removeRootsFromChain: requestBody.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: requestBody.profileId,
certificateId: data.certificateId,
profileName: data.profileName,
commonName: extractedCsrData.commonName || ""
}
}
});
return {
certificate: extractCertificateData(data),
certificateRequestId: data.certificateRequestId
};
}
const certificateRequestForService: CertificateRequestForService = {
commonName: attributes?.commonName,
keyUsages: attributes?.keyUsages,
extendedKeyUsages: attributes?.extendedKeyUsages,
altNames: attributes?.altNames,
validity: { ttl: attributes?.ttl || "" },
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
signatureAlgorithm: attributes?.signatureAlgorithm,
keyAlgorithm: attributes?.keyAlgorithm
};
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
const data = await server.services.certificateV3.issueCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: requestBody.profileId,
certificateRequest: mappedCertificateRequest,
removeRootsFromChain: requestBody.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: requestBody.profileId,
certificateId: data.certificateId,
commonName: attributes?.commonName || "",
profileName: data.profileName
}
}
});
return {
certificate: extractCertificateData(data),
certificateRequestId: data.certificateRequestId
};
}
});
server.route({
@@ -437,21 +369,18 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
certificateRequestId: req.params.requestId
});
if (data.certificate && data.serialNumber) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: (req.query as { projectId: string }).projectId,
event: {
type: EventType.GET_CERT,
metadata: {
certId: req.params.requestId,
cn: "",
serialNumber: data.serialNumber || ""
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: (req.query as { projectId: string }).projectId,
event: {
type: EventType.GET_CERT,
metadata: {
certId: req.params.requestId,
cn: "",
serialNumber: data.serialNumber || ""
}
});
}
}
});
return data;
}
});
@@ -539,28 +468,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
removeRootsFromChain: req.body.removeRootsFromChain
});
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: data.projectId,
profileId: req.body.profileId,
commonName: req.body.commonName,
altNames: req.body.altNames?.map((altName) => `${altName.type}:${altName.value}`).join(","),
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
keyAlgorithm: req.body.keyAlgorithm,
signatureAlgorithm: req.body.signatureAlgorithm
});
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
@@ -575,10 +482,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
return data;
}
});
@@ -644,29 +548,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr);
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: data.projectId,
profileId: req.body.profileId,
csr: req.body.csr,
commonName: certificateRequestData.commonName,
altNames: certificateRequestData.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
keyUsages: certificateRequestData.keyUsages,
extendedKeyUsages: certificateRequestData.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
keyAlgorithm: certificateRequestData.keyAlgorithm,
signatureAlgorithm: certificateRequestData.signatureAlgorithm
});
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
@@ -681,10 +562,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
return data;
}
});
@@ -735,34 +613,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
orderId: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
subjectAlternativeNames: z.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
),
authorizations: z.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
),
finalize: z.string(),
certificate: z.string().optional(),
certificateRequestId: z.string()
})
@@ -794,23 +644,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
removeRootsFromChain: req.body.removeRootsFromChain
});
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: data.projectId,
profileId: req.body.profileId,
commonName: req.body.commonName,
altNames: req.body.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
@@ -818,16 +651,12 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName
}
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
return data;
}
});
@@ -862,80 +691,41 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
let certificateRequest: TCertificateRequests | undefined;
try {
const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
if (!originalCertificate) {
throw new NotFoundError({ message: "Original certificate not found" });
}
certificateRequest = await server.services.certificateRequest.createCertificateRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: originalCertificate.cert.projectId,
profileId: originalCertificate.cert.profileId || undefined,
caId: originalCertificate.cert.caId ?? undefined,
metadata: JSON.stringify({
operation: "renewal",
originalCertificateId: req.params.id,
removeRootsFromChain: req.body?.removeRootsFromChain
})
});
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.id,
removeRootsFromChain: req.body?.removeRootsFromChain,
certificateRequestId: certificateRequest.id
});
if (data.certificate && data.certificate.trim() !== "") {
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.id,
newCertificateId: data.certificateId,
profileName: data.profileName,
commonName: data.commonName
}
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
} catch (error) {
if (certificateRequest) {
await server.services.certificateRequest.updateCertificateRequestStatus({
certificateRequestId: certificateRequest.id,
status: CertificateRequestStatus.FAILED,
errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal"
});
}
throw error;
const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
if (!originalCertificate) {
throw new NotFoundError({ message: "Original certificate not found" });
}
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.id,
removeRootsFromChain: req.body?.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.id,
newCertificateId: data.certificateId,
profileName: data.profileName,
commonName: data.commonName
}
}
});
return data;
}
});

View File

@@ -242,14 +242,15 @@ export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) =
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.internalCertificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
const response = await server.services.internalCertificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = response;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -1,6 +1,5 @@
import { z } from "zod";
import { TCertificateRequests } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { NotFoundError } from "@app/lib/errors";
@@ -8,12 +7,7 @@ import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { ACMESANType, CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import {
CertExtendedKeyUsageType,
@@ -23,7 +17,6 @@ import {
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas";
@@ -340,34 +333,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
orderId: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
subjectAlternativeNames: z.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
),
authorizations: z.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
),
finalize: z.string(),
certificate: z.string().optional(),
certificateRequestId: z.string()
})
@@ -423,7 +388,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName
}
}
@@ -467,80 +431,41 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
let certificateRequest: TCertificateRequests | undefined;
try {
const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.certificateId
});
if (!originalCertificate) {
throw new NotFoundError({ message: "Original certificate not found" });
}
certificateRequest = await server.services.certificateRequest.createCertificateRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: originalCertificate.cert.projectId,
profileId: originalCertificate.cert.profileId || undefined,
caId: originalCertificate.cert.caId ?? undefined,
metadata: JSON.stringify({
operation: "renewal",
originalCertificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain
})
});
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain,
certificateRequestId: certificateRequest.id
});
if (data.certificate && data.certificate.trim() !== "") {
await server.services.certificateRequest.attachCertificateToRequest({
certificateRequestId: certificateRequest.id,
certificateId: data.certificateId
});
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.certificateId,
newCertificateId: data.certificateId,
profileName: data.profileName,
commonName: data.commonName
}
}
});
return {
...data,
certificateRequestId: certificateRequest.id
};
} catch (error) {
if (certificateRequest) {
await server.services.certificateRequest.updateCertificateRequestStatus({
certificateRequestId: certificateRequest.id,
status: CertificateRequestStatus.FAILED,
errorMessage: error instanceof Error ? error.message : "Unknown error during certificate renewal"
});
}
throw error;
const originalCertificate = await server.services.certificate.getCert({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.certificateId
});
if (!originalCertificate) {
throw new NotFoundError({ message: "Original certificate not found" });
}
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.certificateId,
newCertificateId: data.certificateId,
profileName: data.profileName,
commonName: data.commonName
}
}
});
return data;
}
});

View File

@@ -1,12 +1,11 @@
import acme from "acme-client";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -15,6 +14,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { CertKeyAlgorithm } from "../certificate-common/certificate-constants";
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
import { CertificateRequestStatus } from "../certificate-request/certificate-request-types";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
@@ -41,6 +41,7 @@ export type TIssueCertificateFromProfileJobData = {
isRenewal?: boolean;
originalCertificateId?: string;
certificateRequestId?: string;
csr?: string;
};
type TCertificateIssuanceQueueFactoryDep = {
@@ -86,40 +87,6 @@ export const certificateIssuanceQueueFactory = ({
certificateProfileDAL,
certificateRequestService
}: TCertificateIssuanceQueueFactoryDep) => {
const validateKeyUsages = (keyUsages: unknown): CertKeyUsage[] => {
if (!keyUsages) return [];
const validKeyUsages = Object.values(CertKeyUsage);
if (Array.isArray(keyUsages)) {
return keyUsages.filter(
(usage): usage is CertKeyUsage => typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsage)
);
}
return [];
};
const validateExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => {
if (!extendedKeyUsages) return [];
const validExtendedKeyUsages = Object.values(CertExtendedKeyUsage);
if (Array.isArray(extendedKeyUsages)) {
return extendedKeyUsages.filter(
(usage): usage is CertExtendedKeyUsage =>
typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsage)
);
}
return [];
};
const validateKeyAlgorithm = (keyAlgorithm: unknown): CertKeyAlgorithm | undefined => {
if (typeof keyAlgorithm !== "string") return undefined;
const validKeyAlgorithms = Object.values(CertKeyAlgorithm);
return validKeyAlgorithms.includes(keyAlgorithm as CertKeyAlgorithm)
? (keyAlgorithm as CertKeyAlgorithm)
: undefined;
};
const acmeFns = AcmeCertificateAuthorityFns({
appConnectionDAL,
appConnectionService,
@@ -168,7 +135,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages,
isRenewal,
originalCertificateId,
certificateRequestId
certificateRequestId,
csr
}: TIssueCertificateFromProfileJobData) => {
const jobData: TIssueCertificateFromProfileJobData = {
certificateId,
@@ -183,7 +151,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages,
isRenewal,
originalCertificateId,
certificateRequestId
certificateRequestId,
csr
};
await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, {
@@ -210,7 +179,8 @@ export const certificateIssuanceQueueFactory = ({
extendedKeyUsages,
isRenewal,
originalCertificateId,
certificateRequestId
certificateRequestId,
csr
} = data;
try {
@@ -225,32 +195,36 @@ export const certificateIssuanceQueueFactory = ({
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (ca.externalCa?.type === CaType.ACME) {
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm);
if (!validatedKeyAlgorithm) {
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
}
const keyAlg = keyAlgorithmToAlgCfg(validatedKeyAlgorithm);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
let certificateCsr: string;
let skLeaf: string = "";
const [, certificateCsr] = await acme.crypto.createCsr(
{
altNames: altNames || [],
commonName: commonName || ""
},
skLeaf
);
if (csr) {
certificateCsr = csr;
} else {
const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const [, generatedCsr] = await acme.crypto.createCsr(
{
altNames: altNames || [],
commonName: commonName || ""
},
skLeaf
);
certificateCsr = generatedCsr.toString();
}
const acmeResult = await acmeFns.orderCertificateFromProfile({
caId,
profileId,
commonName: commonName || "",
altNames: altNames || [],
csr: certificateCsr,
csr: Buffer.from(certificateCsr),
csrPrivateKey: skLeaf,
keyUsages: validateKeyUsages(keyUsages),
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages),
keyUsages: keyUsages as CertKeyUsage[],
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
ttl,
signatureAlgorithm,
keyAlgorithm,
@@ -306,24 +280,20 @@ export const certificateIssuanceQueueFactory = ({
}
}
const validatedKeyAlgorithm = validateKeyAlgorithm(keyAlgorithm);
if (!validatedKeyAlgorithm) {
throw new BadRequestError({ message: `Invalid key algorithm: ${keyAlgorithm}` });
}
const azureParams = {
caId,
profileId,
commonName: commonName || "",
altNames: altNames || [],
keyUsages: validateKeyUsages(keyUsages),
extendedKeyUsages: validateExtendedKeyUsages(extendedKeyUsages),
keyUsages: keyUsages as CertKeyUsage[],
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
validity: { ttl },
signatureAlgorithm,
keyAlgorithm: validatedKeyAlgorithm,
keyAlgorithm: keyAlgorithm as CertKeyAlgorithm,
isRenewal,
originalCertificateId,
template
template,
...(csr && { csr })
};
const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams);
@@ -383,8 +353,6 @@ export const certificateIssuanceQueueFactory = ({
};
const initializeCertificateIssuanceQueue = async () => {
const appCfg = getConfig();
await queueService.startPg(
QueueJobs.CaIssueCertificateFromProfile,
async ([job]) => {
@@ -392,8 +360,9 @@ export const certificateIssuanceQueueFactory = ({
await processCertificateIssuanceJobs(data);
},
{
workerCount: appCfg.NODE_ENV === "production" ? 3 : 1,
batchSize: 1
workerCount: 2,
batchSize: 1,
pollingIntervalSeconds: 1
}
);

View File

@@ -67,6 +67,7 @@ import {
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TIssueCertFromCaResponse,
TRenewCaCertDTO,
TSignCertFromCaDTO,
TSignIntermediateDTO,
@@ -1198,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({
isFromProfile,
internal = false,
tx
}: TIssueCertFromCaDTO) => {
}: TIssueCertFromCaDTO): Promise<TIssueCertFromCaResponse> => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
let collectionId = pkiCollectionId;
@@ -1532,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({
return cert;
};
let cert;
if (tx) {
await executeIssueCertOperations(tx);
cert = await executeIssueCertOperations(tx);
} else {
await certificateDAL.transaction(executeIssueCertOperations);
cert = await certificateDAL.transaction(executeIssueCertOperations);
}
return {
@@ -1544,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
certificateId: cert.id,
commonName,
ca: expandInternalCa(ca)
};
};
@@ -1905,8 +1909,8 @@ export const internalCertificateAuthorityServiceFactory = ({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
const cert = await certificateDAL.transaction(async (tx) => {
const newCert = await certificateDAL.create(
{
caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id,
@@ -1929,7 +1933,7 @@ export const internalCertificateAuthorityServiceFactory = ({
await certificateBodyDAL.create(
{
certId: cert.id,
certId: newCert.id,
encryptedCertificate,
encryptedCertificateChain
},
@@ -1940,13 +1944,13 @@ export const internalCertificateAuthorityServiceFactory = ({
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
certId: newCert.id
},
tx
);
}
return cert;
return newCert;
});
return {
@@ -1954,6 +1958,7 @@ export const internalCertificateAuthorityServiceFactory = ({
certificateChain: certificateChainPem,
issuingCaCertificate,
serialNumber,
certificateId: cert.id,
ca: expandInternalCa(ca),
commonName: cn
};

View File

@@ -248,3 +248,20 @@ export type TIssueCertWithTemplateDTO = {
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};
type TCaReference = {
id: string;
projectId: string;
dn: string;
};
export type TIssueCertFromCaResponse = {
certificate: string;
certificateChain: string;
issuingCaCertificate: string;
privateKey: string;
serialNumber: string;
certificateId: string;
ca: TCaReference;
commonName: string;
};

View File

@@ -56,7 +56,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
};
certificateAuthority?: {
id: string;
projectId?: string;
projectId: string;
status: string;
name: string;
isExternal?: boolean;

View File

@@ -1,36 +1,12 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TCertificateRequests } from "@app/db/schemas";
import { TableName, TCertificateRequests, TCertificates } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
type TCertificateRequestWithCertificateFlat = TCertificateRequests & {
certificateId?: string | null;
certificateSerialNumber?: string | null;
certificateFriendlyName?: string | null;
certificateCommonName?: string | null;
certificateAltNames?: string | null;
certificateStatus?: string | null;
certificateNotBefore?: Date | null;
certificateNotAfter?: Date | null;
certificateKeyUsages?: string[] | null;
certificateExtendedKeyUsages?: string[] | null;
};
type TCertificateInfo = {
id: string;
serialNumber: string;
friendlyName: string | null;
commonName: string;
altNames: string | null;
status: string;
notBefore: Date;
notAfter: Date;
keyUsages: string[] | null;
extendedKeyUsages: string[] | null;
};
import { ormify, selectAllTableCols } from "@app/lib/knex";
type TCertificateRequestWithCertificate = TCertificateRequests & {
certificate: TCertificateInfo | null;
certificate: TCertificates | null;
};
export type TCertificateRequestDALFactory = ReturnType<typeof certificateRequestDALFactory>;
@@ -40,61 +16,24 @@ export const certificateRequestDALFactory = (db: TDbClient) => {
const findByIdWithCertificate = async (id: string): Promise<TCertificateRequestWithCertificate | null> => {
try {
const certificateRequest = (await db(TableName.CertificateRequests)
.leftJoin(
TableName.Certificate,
`${TableName.CertificateRequests}.certificateId`,
`${TableName.Certificate}.id`
)
.where(`${TableName.CertificateRequests}.id`, id)
.select(
`${TableName.CertificateRequests}.*`,
`${TableName.Certificate}.id as certificateId`,
`${TableName.Certificate}.serialNumber as certificateSerialNumber`,
`${TableName.Certificate}.friendlyName as certificateFriendlyName`,
`${TableName.Certificate}.commonName as certificateCommonName`,
`${TableName.Certificate}.altNames as certificateAltNames`,
`${TableName.Certificate}.status as certificateStatus`,
`${TableName.Certificate}.notBefore as certificateNotBefore`,
`${TableName.Certificate}.notAfter as certificateNotAfter`,
`${TableName.Certificate}.keyUsages as certificateKeyUsages`,
`${TableName.Certificate}.extendedKeyUsages as certificateExtendedKeyUsages`
)
.first()) as TCertificateRequestWithCertificateFlat | undefined;
const certificateRequest = await certificateRequestOrm.findById(id);
if (!certificateRequest) return null;
// Transform the flat result into nested structure
const {
certificateId,
certificateSerialNumber,
certificateFriendlyName,
certificateCommonName,
certificateAltNames,
certificateStatus,
certificateNotBefore,
certificateNotAfter,
certificateKeyUsages,
certificateExtendedKeyUsages,
...certificateRequestData
} = certificateRequest;
if (!certificateRequest.certificateId) {
return {
...certificateRequest,
certificate: null
};
}
const certificate = await db(TableName.Certificate)
.where("id", certificateRequest.certificateId)
.select(selectAllTableCols(TableName.Certificate))
.first();
return {
...certificateRequestData,
certificate: certificateId
? {
id: certificateId,
serialNumber: certificateSerialNumber as string,
friendlyName: certificateFriendlyName || null,
commonName: certificateCommonName as string,
altNames: certificateAltNames || null,
status: certificateStatus as string,
notBefore: certificateNotBefore as Date,
notAfter: certificateNotAfter as Date,
keyUsages: certificateKeyUsages || null,
extendedKeyUsages: certificateExtendedKeyUsages || null
}
: null
...certificateRequest,
certificate: certificate || null
};
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" });
@@ -111,24 +50,33 @@ export const certificateRequestDALFactory = (db: TDbClient) => {
}
};
const updateStatus = async (id: string, status: string, errorMessage?: string): Promise<TCertificateRequests> => {
const updateStatus = async (
id: string,
status: string,
errorMessage?: string,
tx?: Knex
): Promise<TCertificateRequests> => {
try {
const updateData: Partial<TCertificateRequests> = { status };
if (errorMessage !== undefined) {
updateData.errorMessage = errorMessage;
}
return await certificateRequestOrm.updateById(id, updateData);
return await certificateRequestOrm.updateById(id, updateData, tx);
} catch (error) {
throw new DatabaseError({ error, name: "Update certificate request status" });
}
};
const attachCertificate = async (id: string, certificateId: string): Promise<TCertificateRequests> => {
const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise<TCertificateRequests> => {
try {
return await certificateRequestOrm.updateById(id, {
certificateId,
status: "issued"
});
return await certificateRequestOrm.updateById(
id,
{
certificateId,
status: "issued"
},
tx
);
} catch (error) {
throw new DatabaseError({ error, name: "Attach certificate to request" });
}

View File

@@ -13,7 +13,7 @@ import {
ProjectPermissionSet,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { NotFoundError } from "@app/lib/errors";
import { ActorType, AuthMethod } from "@app/services/auth/auth-type";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
@@ -202,7 +202,7 @@ describe("CertificateRequestService", () => {
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(BadRequestError);
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError);
});
});
@@ -257,14 +257,14 @@ describe("CertificateRequestService", () => {
"550e8400-e29b-41d4-a716-446655440005"
);
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
serialNumber: "123456",
id: "550e8400-e29b-41d4-a716-446655440006",
actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
});
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
serialNumber: "123456",
id: "550e8400-e29b-41d4-a716-446655440006",
actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL,
@@ -354,14 +354,14 @@ describe("CertificateRequestService", () => {
"550e8400-e29b-41d4-a716-446655440005"
);
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
serialNumber: "123456",
id: "550e8400-e29b-41d4-a716-446655440008",
actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
});
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
serialNumber: "123456",
id: "550e8400-e29b-41d4-a716-446655440008",
actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL,

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
@@ -30,13 +31,12 @@ type TCertificateRequestServiceFactoryDep = {
export type TCertificateRequestServiceFactory = ReturnType<typeof certificateRequestServiceFactory>;
// Input validation schemas
const certificateRequestDataSchema = z
.object({
profileId: z.string().uuid().optional(),
caId: z.string().uuid().optional(),
csr: z.string().min(1).optional(),
commonName: z.string().min(1).max(255).optional(),
commonName: z.string().max(255).optional(),
altNames: z.string().max(1000).optional(),
keyUsages: z.array(z.string()).max(20).optional(),
extendedKeyUsages: z.array(z.string()).max(20).optional(),
@@ -93,8 +93,9 @@ export const certificateRequestServiceFactory = ({
actorAuthMethod,
actorOrgId,
projectId,
tx,
...requestData
}: TCreateCertificateRequestDTO) => {
}: TCreateCertificateRequestDTO & { tx?: Knex }) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -112,11 +113,20 @@ export const certificateRequestServiceFactory = ({
// Validate input data before creating the request
const validatedData = validateCertificateRequestData(requestData);
const certificateRequest = await certificateRequestDAL.create({
status: CertificateRequestStatus.PENDING,
projectId,
...validatedData
});
const certificateRequest = tx
? await certificateRequestDAL.create(
{
status: CertificateRequestStatus.PENDING,
projectId,
...validatedData
},
tx
)
: await certificateRequestDAL.create({
status: CertificateRequestStatus.PENDING,
projectId,
...validatedData
});
return certificateRequest;
};
@@ -149,7 +159,7 @@ export const certificateRequestServiceFactory = ({
}
if (certificateRequest.projectId !== projectId) {
throw new BadRequestError({ message: "Certificate request does not belong to this project" });
throw new NotFoundError({ message: "Certificate request not found" });
}
return certificateRequest;
@@ -183,7 +193,7 @@ export const certificateRequestServiceFactory = ({
}
if (certificateRequest.projectId !== projectId) {
throw new BadRequestError({ message: "Certificate request does not belong to this project" });
throw new NotFoundError({ message: "Certificate request not found" });
}
// If no certificate is attached, return basic info
@@ -201,7 +211,7 @@ export const certificateRequestServiceFactory = ({
// Get certificate body (PEM data)
const certBody = await certificateService.getCertBody({
serialNumber: certificateRequest.certificate.serialNumber,
id: certificateRequest.certificate.id,
actor,
actorId,
actorAuthMethod,
@@ -212,7 +222,7 @@ export const certificateRequestServiceFactory = ({
let privateKey: string | null = null;
try {
const certPrivateKey = await certificateService.getCertPrivateKey({
serialNumber: certificateRequest.certificate.serialNumber,
id: certificateRequest.certificate.id,
actor,
actorId,
actorAuthMethod,

View File

@@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
import { ACMESANType, CertStatus } from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
@@ -198,6 +198,9 @@ describe("CertificateV3Service", () => {
} as any,
certificateIssuanceQueue: {
queueCertificateIssuance: vi.fn().mockResolvedValue(undefined)
},
certificateRequestService: {
createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" })
}
});
});
@@ -287,6 +290,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: {
id: "ca-123",
projectId: "project-123",
@@ -346,8 +351,13 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
const result = await service.issueCertificateFromProfile({
profileId,
certificateRequest: mockCertificateRequest,
@@ -481,6 +491,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: {
id: "ca-123",
projectId: "project-123",
@@ -523,8 +535,34 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
id: "cert-1",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-1",
commonName: "test.example.com",
friendlyName: "Test Algorithm Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-1",
certificateTemplateId: "template-1",
revokedAt: null,
altNames: null,
caCertId: null,
keyUsages: null,
extendedKeyUsages: null,
revocationReason: null,
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
await service.issueCertificateFromProfile({
profileId,
certificateRequest: camelCaseRequest,
@@ -749,8 +787,13 @@ describe("CertificateV3Service", () => {
});
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
const result = await service.signCertificateFromProfile({
profileId,
csr: mockCSR,
@@ -819,168 +862,6 @@ describe("CertificateV3Service", () => {
keyAlgorithm: "RSA_2048"
};
it("should create order successfully for API enrollment profile", async () => {
const profileId = "profile-123";
const mockProfile = {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
updatedAt: new Date(),
slug: "test-profile-order",
description: "Test order profile",
estConfigId: null,
apiConfigId: null
};
const mockCA = {
id: "ca-123",
projectId: "project-123",
externalCa: undefined,
internalCa: {
id: "internal-ca-123",
parentCaId: null,
type: "ROOT",
friendlyName: "Test CA",
organization: "Test Org",
ou: "Test OU",
country: "US",
province: "CA",
locality: "SF",
commonName: "Test CA",
dn: "CN=Test CA",
serialNumber: "123",
maxPathLength: null,
keyAlgorithm: "RSA_2048",
notBefore: undefined,
notAfter: undefined,
activeCaCertId: "cert-123",
caId: "ca-123"
},
name: "Test CA",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
enableDirectIssuance: true
};
const mockTemplate = {
id: "template-123",
name: "Test Order Template",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-123",
description: "Test template for ordering certificates",
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
keyAlgorithm: { defaultKeyType: "RSA_2048" },
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertIncludeType.OPTIONAL,
value: ["example.com"]
}
],
subject: undefined,
sans: undefined,
keyUsages: undefined,
extendedKeyUsages: undefined,
algorithms: undefined,
validity: undefined
};
const mockCertificateResult = {
certificate: "cert",
certificateChain: "chain",
issuingCaCertificate: "issuing-ca",
privateKey: "key",
serialNumber: "123456",
ca: {
id: "ca-123",
projectId: "project-123",
name: "Test CA",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
enableDirectIssuance: true,
externalCa: undefined,
internalCa: {
id: "internal-ca-123",
parentCaId: null,
type: "ROOT",
friendlyName: "Test CA",
organization: "Test Org",
ou: "Test OU",
country: "US",
province: "CA",
locality: "SF",
commonName: "Test CA",
dn: "CN=Test CA",
serialNumber: "123",
maxPathLength: null,
keyAlgorithm: "RSA_2048",
notBefore: null,
notAfter: null,
activeCaCertId: "cert-123",
caId: "ca-123"
}
}
};
const mockCertRecord = {
id: "cert-123",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-123",
commonName: "example.com",
friendlyName: "Test Order Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-123",
certificateTemplateId: "template-123",
revokedAt: null,
altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]),
caCertId: null,
keyUsages: ["DIGITAL_SIGNATURE"],
extendedKeyUsages: ["SERVER_AUTH"],
revocationReason: null,
pkiSubscriberId: null,
profileId: null
};
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
const result = await service.orderCertificateFromProfile({
profileId,
certificateOrder: mockCertificateOrder,
...mockActor
});
expect(result).toHaveProperty("orderId");
expect(result).toHaveProperty("status", "valid");
expect(result).toHaveProperty("certificate");
expect(result.subjectAlternativeNames).toHaveLength(1);
expect(result.subjectAlternativeNames[0]).toEqual({
type: ACMESANType.DNS,
value: "example.com",
status: CertificateOrderStatus.VALID
});
});
it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
const profileId = "profile-123";
const mockProfile = {
@@ -1117,6 +998,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: rsaCa as any
});
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1163,6 +1046,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
id: "cert-1",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-1",
commonName: "test.example.com",
friendlyName: "Test Algorithm Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-1",
certificateTemplateId: "template-1",
revokedAt: null,
altNames: null,
caCertId: null,
keyUsages: null,
extendedKeyUsages: null,
revocationReason: null,
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
// Should not throw - RSA CA is compatible with RSA signature algorithms
await expect(
@@ -1249,6 +1157,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: ecCa as any
});
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1295,6 +1205,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
id: "cert-1",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-1",
commonName: "test.example.com",
friendlyName: "Test Algorithm Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-1",
certificateTemplateId: "template-1",
revokedAt: null,
altNames: null,
caCertId: null,
keyUsages: null,
extendedKeyUsages: null,
revocationReason: null,
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
// Should not throw - EC CA is compatible with ECDSA signature algorithms
await expect(
@@ -1381,6 +1316,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: rsa8192Ca as any
});
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1427,6 +1364,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
id: "cert-1",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-1",
commonName: "test.example.com",
friendlyName: "Test Algorithm Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-1",
certificateTemplateId: "template-1",
revokedAt: null,
altNames: null,
caCertId: null,
keyUsages: null,
extendedKeyUsages: null,
revocationReason: null,
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
// Should not throw - dynamic check supports new RSA key sizes
await expect(
@@ -1513,6 +1475,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "ca-cert",
privateKey: "key",
serialNumber: "123456",
certificateId: "cert-1",
commonName: "test.example.com",
ca: newEcCa as any
});
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
@@ -1559,6 +1523,31 @@ describe("CertificateV3Service", () => {
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
id: "cert-1",
serialNumber: "123456",
status: "ACTIVE",
createdAt: new Date(),
updatedAt: new Date(),
projectId: "project-1",
commonName: "test.example.com",
friendlyName: "Test Algorithm Cert",
notBefore: new Date(),
notAfter: new Date(),
caId: "ca-1",
certificateTemplateId: "template-1",
revokedAt: null,
altNames: null,
caCertId: null,
keyUsages: null,
extendedKeyUsages: null,
revocationReason: null,
pkiSubscriberId: null,
profileId: null
});
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
return callback(undefined as any);
});
// Should not throw - dynamic check supports new EC curves
await expect(
@@ -1692,8 +1681,9 @@ describe("CertificateV3Service", () => {
});
it("should successfully renew eligible certificate", async () => {
// Mock the initial findById call
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateDAL.findById)
.mockResolvedValueOnce(mockOriginalCert)
.mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" });
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
@@ -1709,6 +1699,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca",
privateKey: "private-key",
serialNumber: "789012",
certificateId: "cert-456",
commonName: "test.example.com",
ca: mockCA
});
@@ -2026,6 +2018,8 @@ describe("CertificateV3Service", () => {
issuingCaCertificate: "issuing-ca",
privateKey: "private-key",
serialNumber: "789012",
certificateId: "cert-456",
commonName: "test.example.com",
ca: mockCA
});

View File

@@ -20,7 +20,6 @@ import { TCertificateDALFactory } from "@app/services/certificate/certificate-da
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertificateOrderStatus,
CertKeyAlgorithm,
CertKeyType,
CertKeyUsage,
@@ -40,11 +39,7 @@ import {
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import {
EnrollmentType,
IssuerType,
TCertificateProfileWithConfigs
} from "@app/services/certificate-profile/certificate-profile-types";
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -71,7 +66,9 @@ import {
normalizeDateForApi,
removeRootCaFromChain
} from "../certificate-common/certificate-utils";
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
@@ -122,6 +119,7 @@ type TCertificateV3ServiceFactoryDep = {
import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory,
"queueCertificateIssuance"
>;
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
};
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
@@ -861,7 +859,8 @@ export const certificateV3ServiceFactory = ({
pkiSyncQueue,
kmsService,
projectDAL,
certificateIssuanceQueue
certificateIssuanceQueue,
certificateRequestService
}: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({
profileId,
@@ -945,7 +944,7 @@ export const certificateV3ServiceFactory = ({
const result = await certificateDAL.transaction(async (tx) => {
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
return processSelfSignedCertificate({
const selfSignedResult = await processSelfSignedCertificate({
certificateRequest,
template,
profile,
@@ -957,9 +956,29 @@ export const certificateV3ServiceFactory = ({
projectDAL,
tx
});
const certRequestResult = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: profile.projectId,
tx,
profileId: profile.id,
commonName: certificateRequest.commonName,
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
notBefore: certificateRequest.notBefore,
notAfter: certificateRequest.notAfter,
keyAlgorithm: effectiveKeyAlgorithm,
signatureAlgorithm: effectiveSignatureAlgorithm
});
return { ...selfSignedResult, certificateRequestId: certRequestResult.id };
});
const { selfSignedResult, certificateData } = result;
const { selfSignedResult, certificateData, certificateRequestId } = result;
const subjectCommonName =
(selfSignedResult.certificateSubject.common_name as string) ||
@@ -985,6 +1004,7 @@ export const certificateV3ServiceFactory = ({
privateKey: selfSignedResult.privateKey.toString("utf8"),
serialNumber: selfSignedResult.serialNumber,
certificateId: certificateData.id,
certificateRequestId,
projectId: profile.projectId,
profileName: profile.slug,
commonName: subjectCommonName
@@ -1003,8 +1023,16 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "direct certificate issuance");
validateAlgorithmCompatibility(ca, template);
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
await internalCaService.issueCertFromCa({
const {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber,
cert,
certificateRequestId
} = await certificateDAL.transaction(async (tx) => {
const certResult = await internalCaService.issueCertFromCa({
caId: ca.id,
friendlyName: certificateSubject.common_name || "Certificate",
commonName: certificateSubject.common_name || "",
@@ -1023,22 +1051,43 @@ export const certificateV3ServiceFactory = ({
isFromProfile: true
});
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!cert) {
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
}
const certificateRecord = await certificateDAL.findById(certResult.certificateId);
if (!certificateRecord) {
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
}
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile,
certificateRequest.validity.ttl,
new Date(cert.notAfter)
);
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile,
certificateRequest.validity.ttl,
new Date(certificateRecord.notAfter)
);
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(cert.id, updateData);
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(certificateRecord.id, updateData);
const certRequestResult = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: profile.projectId,
tx,
profileId: profile.id,
commonName: certificateRequest.commonName,
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
notBefore: certificateRequest.notBefore,
notAfter: certificateRequest.notAfter,
keyAlgorithm: effectiveKeyAlgorithm,
signatureAlgorithm: effectiveSignatureAlgorithm
});
return { ...certResult, cert: certificateRecord, certificateRequestId: certRequestResult.id };
});
let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) {
@@ -1052,6 +1101,7 @@ export const certificateV3ServiceFactory = ({
privateKey: bufferToString(privateKey),
serialNumber,
certificateId: cert.id,
certificateRequestId,
projectId: profile.projectId,
profileName: profile.slug,
commonName: cert.commonName || ""
@@ -1135,33 +1185,60 @@ export const certificateV3ServiceFactory = ({
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
await internalCaService.signCertFromCa({
isInternal: true,
caId: ca.id,
csr,
ttl: validity.ttl,
altNames: undefined,
notBefore: normalizeDateForApi(notBefore),
notAfter: normalizeDateForApi(notAfter),
signatureAlgorithm: effectiveSignatureAlgorithm,
keyAlgorithm: effectiveKeyAlgorithm,
isFromProfile: true
const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } =
await certificateDAL.transaction(async (tx) => {
const certResult = await internalCaService.signCertFromCa({
isInternal: true,
caId: ca.id,
csr,
ttl: validity.ttl,
altNames: undefined,
notBefore: normalizeDateForApi(notBefore),
notAfter: normalizeDateForApi(notAfter),
signatureAlgorithm: effectiveSignatureAlgorithm,
keyAlgorithm: effectiveKeyAlgorithm,
isFromProfile: true
});
const signedCertRecord = await certificateDAL.findById(certResult.certificateId);
if (!signedCertRecord) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile,
validity.ttl,
new Date(signedCertRecord.notAfter)
);
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(signedCertRecord.id, updateData);
const certRequestResult = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: profile.projectId,
tx,
profileId: profile.id,
csr,
commonName: mappedCertificateRequest.commonName,
altNames: mappedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","),
keyUsages: convertKeyUsageArrayToLegacy(mappedCertificateRequest.keyUsages),
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(mappedCertificateRequest.extendedKeyUsages),
notBefore,
notAfter,
keyAlgorithm: effectiveKeyAlgorithm,
signatureAlgorithm: effectiveSignatureAlgorithm
});
return { ...certResult, cert: signedCertRecord, certificateRequestId: certRequestResult.id };
});
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!cert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData2.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(cert.id, updateData2);
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
if (removeRootsFromChain) {
@@ -1174,6 +1251,7 @@ export const certificateV3ServiceFactory = ({
certificateChain: certificateChainString,
serialNumber,
certificateId: cert.id,
certificateRequestId,
projectId: profile.projectId,
profileName: profile.slug,
commonName: cert.commonName || ""
@@ -1186,9 +1264,7 @@ export const certificateV3ServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
removeRootsFromChain,
certificateRequestId
actorOrgId
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
const profile = await validateProfileAndPermissions(
profileId,
@@ -1202,37 +1278,59 @@ export const certificateV3ServiceFactory = ({
EnrollmentType.API
);
const certificateRequest = {
commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.altNames?.map((san) => {
let certType: CertSubjectAlternativeNameType;
switch (san.type) {
case "dns":
certType = CertSubjectAlternativeNameType.DNS_NAME;
break;
case "ip":
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
break;
default:
throw new BadRequestError({
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
});
}
return {
type: certType,
value: san.value
};
}),
validity: certificateOrder.validity,
notBefore: certificateOrder.notBefore,
notAfter: certificateOrder.notAfter,
signatureAlgorithm: certificateOrder.signatureAlgorithm,
keyAlgorithm: certificateOrder.keyAlgorithm
};
let certificateRequest: TCertificateRequest;
let extractedKeyAlgorithm: string | undefined;
let extractedSignatureAlgorithm: string | undefined;
if (certificateOrder.csr) {
certificateRequest = extractCertificateRequestFromCSR(certificateOrder.csr);
const algorithms = extractAlgorithmsFromCSR(certificateOrder.csr);
extractedKeyAlgorithm = algorithms.keyAlgorithm;
extractedSignatureAlgorithm = algorithms.signatureAlgorithm;
certificateRequest.validity = certificateOrder.validity;
if (certificateOrder.notBefore && certificateOrder.notAfter) {
certificateRequest.notBefore = certificateOrder.notBefore;
certificateRequest.notAfter = certificateOrder.notAfter;
}
} else {
certificateRequest = {
commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.altNames?.map((san) => {
let certType: CertSubjectAlternativeNameType;
switch (san.type) {
case "dns":
certType = CertSubjectAlternativeNameType.DNS_NAME;
break;
case "ip":
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
break;
default:
throw new BadRequestError({
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
});
}
return {
type: certType,
value: san.value
};
}),
validity: certificateOrder.validity,
notBefore: certificateOrder.notBefore,
notAfter: certificateOrder.notAfter,
signatureAlgorithm: certificateOrder.signatureAlgorithm,
keyAlgorithm: certificateOrder.keyAlgorithm
};
}
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
if (certificateOrder.csr) {
mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm;
mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
}
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
@@ -1258,64 +1356,53 @@ export const certificateV3ServiceFactory = ({
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
if (caType === CaType.INTERNAL) {
const certificateResult = await issueCertificateFromProfile({
profileId,
certificateRequest,
actor,
actorId,
actorAuthMethod,
actorOrgId,
removeRootsFromChain
throw new BadRequestError({
message: "Certificate ordering is not supported for the specified CA type"
});
const orderId = randomUUID();
return {
orderId,
status: CertificateOrderStatus.VALID,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type,
value: san.value,
status: CertificateOrderStatus.VALID
})),
authorizations: [],
finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`,
certificate: certificateResult.certificate,
projectId: certificateResult.projectId,
profileName: certificateResult.profileName
};
}
if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) {
const orderId = randomUUID();
const certRequest = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: profile.projectId,
profileId: profile.id,
commonName: certificateOrder.commonName || "",
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [],
extendedKeyUsages: certificateOrder.extendedKeyUsages
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages)
: [],
keyAlgorithm: certificateOrder.keyAlgorithm || "",
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
altNames: certificateOrder.altNames?.map((san) => san.value).join(",") || "",
notBefore: certificateOrder.notBefore,
notAfter: certificateOrder.notAfter
});
await certificateIssuanceQueue.queueCertificateIssuance({
certificateId: orderId,
profileId: profile.id,
caId: profile.caId || "",
ttl: certificateOrder.validity?.ttl || "1y",
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
keyAlgorithm: certificateOrder.keyAlgorithm || "",
commonName: certificateOrder.commonName || "",
altNames: certificateOrder.altNames?.map((san) => san.value) || [],
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [],
extendedKeyUsages: certificateOrder.extendedKeyUsages
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages)
keyAlgorithm: certificateRequest.keyAlgorithm || "",
commonName: certificateRequest.commonName || "",
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [],
extendedKeyUsages: certificateRequest.extendedKeyUsages
? convertEnumsToStringArray(certificateRequest.extendedKeyUsages)
: [],
certificateRequestId
certificateRequestId: certRequest.id,
csr: certificateOrder.csr
});
return {
orderId,
status: CertificateOrderStatus.PENDING,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type,
value: san.value,
status: CertificateOrderStatus.PENDING
})),
authorizations: [],
finalize: `/api/v3/pki/certificates/orders/${orderId}/finalize`,
projectId: profile.projectId,
certificateRequestId: certRequest.id,
projectId: certRequest.projectId,
profileName: profile.slug
};
}
@@ -1325,30 +1412,6 @@ export const certificateV3ServiceFactory = ({
});
};
// Type for internal CA renewal result
type TInternalRenewalData = {
certificate: string;
certificateChain: string;
issuingCaCertificate: string;
serialNumber: string;
newCert: TCertificates;
originalCert: TCertificates;
profile: TCertificateProfileWithConfigs | null;
};
// Type for external CA renewal result
type TExternalRenewalData = {
isExternalCA: true;
ca: TCertificateAuthorityWithAssociatedCa;
profile: TCertificateProfileWithConfigs | null;
originalCert: TCertificates;
originalSignatureAlgorithm: CertSignatureAlgorithm;
originalKeyAlgorithm: CertKeyAlgorithm;
ttl: string;
};
type TRenewalTransactionResult = TInternalRenewalData | TExternalRenewalData;
const renewCertificate = async ({
certificateId,
actor,
@@ -1356,10 +1419,11 @@ export const certificateV3ServiceFactory = ({
actorAuthMethod,
actorOrgId,
internal = false,
removeRootsFromChain,
certificateRequestId
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const renewalResult: TRenewalTransactionResult = await certificateDAL.transaction(async (tx) => {
removeRootsFromChain
}: Omit<TRenewCertificateDTO, "certificateRequestId"> & {
internal?: boolean;
}): Promise<TCertificateFromProfileResponse> => {
const renewalResult = await certificateDAL.transaction(async (tx) => {
const originalCert = await certificateDAL.findById(certificateId, tx);
if (!originalCert) {
throw new NotFoundError({ message: "Certificate not found" });
@@ -1576,7 +1640,7 @@ export const certificateV3ServiceFactory = ({
issuingCaCertificate = caResult.issuingCaCertificate;
serialNumber = caResult.serialNumber;
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
const foundCert = await certificateDAL.findById(caResult.certificateId, tx);
if (!foundCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
@@ -1665,6 +1729,27 @@ export const certificateV3ServiceFactory = ({
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
const certRequestResult = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: originalCert.projectId,
tx,
profileId: originalCert.profileId || undefined,
commonName: originalCert.commonName || undefined,
altNames: originalCert.altNames || undefined,
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
),
notBefore: new Date(newCert.notBefore),
notAfter: new Date(newCert.notAfter),
keyAlgorithm: originalKeyAlgorithm,
signatureAlgorithm: originalSignatureAlgorithm,
metadata: `Renewed from certificate ID: ${originalCert.id}`
});
return {
certificate,
certificateChain,
@@ -1672,10 +1757,13 @@ export const certificateV3ServiceFactory = ({
serialNumber,
newCert,
originalCert,
profile
profile,
certRequestResult
};
});
let certificateRequestId: string = renewalResult.certRequestResult?.id || "";
// Handle external CA renewals separately
if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) {
const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult;
@@ -1685,6 +1773,27 @@ export const certificateV3ServiceFactory = ({
? originalCert.altNames.split(",").map((san: string) => san.trim())
: [];
const certificateRequest = await certificateRequestService.createCertificateRequest({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId: originalCert.projectId,
profileId: profile?.id,
caId: ca.id,
commonName: originalCert.commonName || undefined,
altNames: originalCert.altNames || undefined,
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
),
keyAlgorithm: originalKeyAlgorithm,
signatureAlgorithm: originalSignatureAlgorithm,
metadata: `Renewed from certificate ID: ${originalCert.id}`
});
certificateRequestId = certificateRequest.id;
await certificateIssuanceQueue.queueCertificateIssuance({
certificateId: renewalOrderId,
profileId: profile?.id || "",
@@ -1698,7 +1807,7 @@ export const certificateV3ServiceFactory = ({
extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)),
isRenewal: true,
originalCertificateId: certificateId,
certificateRequestId
certificateRequestId: certificateRequest.id
});
return {
@@ -1707,6 +1816,7 @@ export const certificateV3ServiceFactory = ({
issuingCaCertificate: "",
serialNumber: "",
certificateId: renewalOrderId,
certificateRequestId: certificateRequest.id,
projectId: originalCert.projectId,
profileName: profile?.slug || "External CA Profile",
commonName: originalCert.commonName || ""
@@ -1734,6 +1844,7 @@ export const certificateV3ServiceFactory = ({
certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id,
certificateRequestId,
projectId: renewalResult.originalCert.projectId,
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
commonName: renewalResult.originalCert.commonName || ""

View File

@@ -1,6 +1,6 @@
import { TProjectPermission } from "@app/lib/types";
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
import { ACMESANType } from "../certificate/certificate-types";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
@@ -59,9 +59,9 @@ export type TOrderCertificateFromProfileDTO = {
signatureAlgorithm?: string;
keyAlgorithm?: string;
template?: string;
csr?: string;
};
removeRootsFromChain?: boolean;
certificateRequestId?: string;
} & Omit<TProjectPermission, "projectId">;
export type TCertificateFromProfileResponse = {
@@ -71,34 +71,14 @@ export type TCertificateFromProfileResponse = {
privateKey?: string;
serialNumber: string;
certificateId: string;
certificateRequestId: string;
projectId: string;
profileName: string;
commonName: string;
};
export type TCertificateOrderResponse = {
orderId: string;
status: CertificateOrderStatus;
subjectAlternativeNames: Array<{
type: ACMESANType;
value: string;
status: CertificateOrderStatus;
}>;
authorizations: Array<{
identifier: {
type: ACMESANType;
value: string;
};
status: CertificateOrderStatus;
expires?: string;
challenges: Array<{
type: string;
status: CertificateOrderStatus;
url: string;
token: string;
}>;
}>;
finalize: string;
certificateRequestId: string;
certificate?: string;
projectId: string;
profileName: string;

View File

@@ -83,32 +83,40 @@ export type TDownloadPkcs12DTO = {
export type TUnifiedCertificateIssuanceDTO = {
projectSlug: string;
profileId: string;
projectId: string;
profileId?: string;
caId?: string;
csr?: string;
commonName?: string;
altNames?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
notBefore?: Date;
notAfter?: Date;
keyAlgorithm?: string;
signatureAlgorithm?: string;
ttl?: string;
friendlyName?: string;
pkiCollectionId?: string;
issuerType?: string;
attributes?: {
commonName?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
altNames?: Array<{
type: string;
value: string;
}>;
signatureAlgorithm: string;
keyAlgorithm: string;
subjectAlternativeNames?: Array<{
type: string;
value: string;
}>;
ttl: string;
notBefore?: string;
notAfter?: string;
};
removeRootsFromChain?: boolean;
};
export type TUnifiedCertificateResponse = {
certificate: string;
issuingCaCertificate: string;
certificateChain: string;
privateKey?: string;
serialNumber: string;
certificateId: string;
projectId: string;
certificate: {
certificate: string;
issuingCaCertificate: string;
certificateChain: string;
privateKey?: string;
serialNumber: string;
certificateId: string;
};
certificateRequestId: string;
};
export type TCertificateRequestResponse = {

View File

@@ -107,6 +107,7 @@ type TCertificateDetails = {
certificate?: string;
certificateChain?: string;
privateKey?: string;
issuingCaCertificate?: string;
};
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
@@ -280,41 +281,42 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
profileId: formProfileId,
projectSlug: currentProject.slug,
projectId: currentProject.id,
ttl,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
attributes: {
ttl,
signatureAlgorithm: signatureAlgorithm || "",
keyAlgorithm: keyAlgorithm || "",
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
}
};
if (constraints.shouldShowSubjectSection && commonName) {
request.commonName = commonName;
}
if (signatureAlgorithm) {
request.signatureAlgorithm = signatureAlgorithm;
}
if (keyAlgorithm) {
request.keyAlgorithm = keyAlgorithm;
request.attributes.commonName = commonName;
}
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) {
request.altNames = formattedSans;
request.attributes.altNames = formattedSans;
request.attributes.subjectAlternativeNames = formattedSans;
}
}
const response = await issueCertificate(request);
// Handle certificate issuance response
if ("certificate" in response) {
// Immediate certificate issuance
setCertificateDetails({
serialNumber: response.serialNumber,
certificate: response.certificate,
certificateChain: response.certificateChain,
privateKey: response.privateKey
});
if ("certificate" in response && response.certificate) {
const certData = response.certificate;
const certificateDetailsToSet = {
serialNumber: certData.serialNumber || "",
certificate: certData.certificate || "",
certificateChain: certData.certificateChain || "",
privateKey: certData.privateKey || "",
issuingCaCertificate: certData.issuingCaCertificate || ""
};
setCertificateDetails(certificateDetailsToSet);
createNotification({
text: "Successfully created certificate",