import { ForbiddenError } from "@casl/ability"; import { randomUUID } from "crypto"; import RE2 from "re2"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, ProjectPermissionCertificateProfileActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, CertificateOrderStatus, CertKeyAlgorithm, CertKeyType, CertKeyUsage, CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityDALFactory, TCertificateAuthorityWithAssociatedCa } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants"; import { extractAlgorithmsFromCSR, extractCertificateRequestFromCSR } from "../certificate-common/certificate-csr-utils"; import { bufferToString, buildCertificateSubjectFromTemplate, buildSubjectAlternativeNamesFromTemplate, convertExtendedKeyUsageArrayFromLegacy, convertExtendedKeyUsageArrayToLegacy, convertKeyUsageArrayFromLegacy, convertKeyUsageArrayToLegacy, mapEnumsForValidation, normalizeDateForApi } from "../certificate-common/certificate-utils"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; 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"; import { TCertificateFromProfileResponse, TCertificateOrderResponse, TDisableRenewalConfigDTO, TDisableRenewalResponse, TIssueCertificateFromProfileDTO, TOrderCertificateFromProfileDTO, TRenewalConfigResponse, TRenewCertificateDTO, TSignCertificateFromProfileDTO, TUpdateRenewalConfigDTO } from "./certificate-v3-types"; type TCertificateV3ServiceFactoryDep = { certificateDAL: Pick; certificateSecretDAL: Pick; certificateAuthorityDAL: Pick; certificateProfileDAL: Pick; certificateTemplateV2Service: Pick< TCertificateTemplateV2ServiceFactory, "validateCertificateRequest" | "getTemplateV2ById" >; internalCaService: Pick; permissionService: Pick; certificateSyncDAL: Pick< TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate" >; pkiSyncDAL: Pick; pkiSyncQueue: Pick; }; export type TCertificateV3ServiceFactory = ReturnType; const validateProfileAndPermissions = async ( profileId: string, actor: ActorType, actorId: string, actorAuthMethod: ActorAuthMethod, actorOrgId: string, certificateProfileDAL: Pick, permissionService: Pick, requiredEnrollmentType: EnrollmentType ) => { const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } if (profile.enrollmentType !== requiredEnrollmentType) { throw new ForbiddenRequestError({ message: `Profile is not configured for ${requiredEnrollmentType} enrollment` }); } // XXX: NOT SURE IF THIS IS SECURE TO BY PASS THE PERMISSION CHECK FOR ACME ACCOUNTS // may need to consider this carefully // TODO: check actor/profile ownership as well if (actor === ActorType.ACME_ACCOUNT && requiredEnrollmentType === EnrollmentType.ACME) { return profile; } const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: profile.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.IssueCert, ProjectPermissionSub.CertificateProfiles ); return profile; }; const validateRenewalEligibility = ( certificate: { id: string; status: string; notBefore: Date; notAfter: Date; revokedAt?: Date | null; renewedByCertificateId?: string | null; profileId?: string | null; caId?: string | null; pkiSubscriberId?: string | null; }, ca: TCertificateAuthorityWithAssociatedCa ) => { const errors: string[] = []; if (certificate.status !== CertStatus.ACTIVE) { errors.push(`Certificate status is ${certificate.status}, must be ${CertStatus.ACTIVE}`); } const now = new Date(); if (certificate.notAfter <= now) { errors.push("Certificate is already expired"); } if (certificate.revokedAt) { errors.push("Certificate is revoked and cannot be renewed"); } const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; const isInternalCa = caType === CaType.INTERNAL; const isConnectedExternalCa = caType === CaType.ACME || caType === CaType.AZURE_AD_CS; const isImportedCertificate = certificate.pkiSubscriberId != null && !certificate.profileId; if (!isInternalCa && !isConnectedExternalCa) { errors.push(`CA type ${String(caType)} does not support renewal`); } if (isImportedCertificate) { errors.push("Externally imported certificates cannot be renewed"); } if (ca.status !== CaStatus.ACTIVE) { errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`); } if (certificate.renewedByCertificateId) { errors.push("Certificate has already been renewed"); } const certificateTtlInDays = Math.ceil( (certificate.notAfter.getTime() - certificate.notBefore.getTime()) / (24 * 60 * 60 * 1000) ); if (ca.internalCa?.notAfter) { const caExpiryDate = new Date(ca.internalCa.notAfter); const proposedCertExpiryDate = new Date(now.getTime() + certificateTtlInDays * 24 * 60 * 60 * 1000); if (proposedCertExpiryDate > caExpiryDate) { errors.push( `New certificate would expire (${proposedCertExpiryDate.toISOString()}) after its issuing CA (${caExpiryDate.toISOString()})` ); } } return { isEligible: errors.length === 0, errors }; }; const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation: string) => { const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; if (caType !== CaType.INTERNAL) { throw new BadRequestError({ message: `Only internal CAs support ${operation}` }); } return caType; }; const validateAlgorithmCompatibility = ( ca: TCertificateAuthorityWithAssociatedCa, template: { algorithms?: { signature?: string[]; }; } ) => { if (!template.algorithms?.signature || template.algorithms.signature.length === 0) { return; } const caKeyAlgorithm = ca.internalCa?.keyAlgorithm; if (!caKeyAlgorithm) { throw new BadRequestError({ message: "CA key algorithm not found" }); } const compatibleAlgorithms = template.algorithms?.signature?.filter((sigAlg: string) => { const parts = sigAlg.split("-"); if (parts.length === 0) { return false; } const keyType = parts[parts.length - 1]; if (caKeyAlgorithm.startsWith("RSA")) { return keyType === CertKeyType.RSA; } if (caKeyAlgorithm.startsWith("EC")) { return keyType === CertKeyType.ECDSA; } return false; }) || []; if (compatibleAlgorithms.length === 0) { throw new BadRequestError({ message: `Template signature algorithms (${template.algorithms?.signature?.join(", ") || "none"}) are not compatible with CA key algorithm (${caKeyAlgorithm})` }); } }; const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | string): string => { if (typeof certData === "string") return certData; if (Buffer.isBuffer(certData)) return bufferToString(certData); if (certData && typeof certData === "object" && "rawData" in certData && Buffer.isBuffer(certData.rawData)) { return bufferToString(certData.rawData); } return bufferToString(certData as unknown as Buffer); }; const parseKeyUsages = (keyUsages: unknown): CertKeyUsage[] => { if (!keyUsages) return []; if (Array.isArray(keyUsages)) return keyUsages as CertKeyUsage[]; return (keyUsages as string).split(",").map((usage) => usage.trim() as CertKeyUsage); }; const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => { if (!extendedKeyUsages) return []; if (Array.isArray(extendedKeyUsages)) return extendedKeyUsages as CertExtendedKeyUsage[]; return (extendedKeyUsages as string).split(",").map((usage) => usage.trim() as CertExtendedKeyUsage); }; const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => { const renewalDate = new Date(certificateExpiryDate.getTime() - renewBeforeDays * 24 * 60 * 60 * 1000); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(0, 0, 0, 0); return renewalDate >= tomorrow; }; const calculateRenewalThreshold = ( profileRenewBeforeDays: number | undefined, certificateTtlInDays: number ): number | undefined => { if (!profileRenewBeforeDays) { return undefined; } if (certificateTtlInDays > profileRenewBeforeDays) { return profileRenewBeforeDays; } return Math.max(1, certificateTtlInDays - 1); }; const parseTtlToDays = (ttl: string): number => { const match = ttl.match(new RE2("^(\\d+)([dhm])$")); if (!match) { throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` }); } const [, value, unit] = match; const numValue = parseInt(value, 10); switch (unit) { case "d": return numValue; case "h": return Math.ceil(numValue / 24); case "m": return Math.ceil(numValue / (24 * 60)); default: throw new BadRequestError({ message: `Unsupported TTL unit: ${unit}` }); } }; const calculateFinalRenewBeforeDays = ( profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } }, ttl: string, certificateExpiryDate: Date ): number | undefined => { if (!profile.apiConfig?.autoRenew || !profile.apiConfig.renewBeforeDays) { return undefined; } const certificateTtlInDays = parseTtlToDays(ttl); const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig.renewBeforeDays, certificateTtlInDays); if (!renewBeforeDays) { return undefined; } return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined; }; export const certificateV3ServiceFactory = ({ certificateDAL, certificateSecretDAL, certificateAuthorityDAL, certificateProfileDAL, certificateTemplateV2Service, internalCaService, permissionService, certificateSyncDAL, pkiSyncDAL, pkiSyncQueue }: TCertificateV3ServiceFactoryDep) => { const issueCertificateFromProfile = async ({ profileId, certificateRequest, actor, actorId, actorAuthMethod, actorOrgId }: TIssueCertificateFromProfileDTO): Promise => { const profile = await validateProfileAndPermissions( profileId, actor, actorId, actorAuthMethod, actorOrgId, certificateProfileDAL, permissionService, EnrollmentType.API ); if (certificateRequest.commonName && Array.isArray(certificateRequest.commonName)) { throw new BadRequestError({ message: "Common Name must be a single value, not an array" }); } const mappedCertificateRequest = mapEnumsForValidation({ ...certificateRequest, subjectAlternativeNames: certificateRequest.altNames }); const template = await certificateTemplateV2Service.getTemplateV2ById({ actor, actorId, actorAuthMethod, actorOrgId, templateId: profile.certificateTemplateId, internal: true }); if (!template) { throw new NotFoundError({ message: "Certificate template not found for this profile" }); } const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, mappedCertificateRequest ); if (!validationResult.isValid) { throw new BadRequestError({ message: `Certificate request validation failed: ${validationResult.errors.join(", ")}` }); } const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); } validateCaSupport(ca, "direct certificate issuance"); validateAlgorithmCompatibility(ca, template); const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined; const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined; if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) { throw new BadRequestError({ message: "Key algorithm is required by template policy but not provided in request" }); } if (template.algorithms?.signature && !effectiveSignatureAlgorithm) { throw new BadRequestError({ message: "Signature algorithm is required by template policy but not provided in request" }); } const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject); const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate( { subjectAlternativeNames: certificateRequest.altNames }, template.sans ); const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } = await internalCaService.issueCertFromCa({ caId: ca.id, friendlyName: certificateSubject.common_name || "Certificate", commonName: certificateSubject.common_name || "", altNames: subjectAlternativeNames, ttl: certificateRequest.validity.ttl, keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [], extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [], notBefore: normalizeDateForApi(certificateRequest.notBefore), notAfter: normalizeDateForApi(certificateRequest.notAfter), signatureAlgorithm: effectiveSignatureAlgorithm, keyAlgorithm: effectiveKeyAlgorithm, actor, actorId, actorAuthMethod, actorOrgId, 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 finalRenewBeforeDays = calculateFinalRenewBeforeDays( profile, certificateRequest.validity.ttl, new Date(cert.notAfter) ); await certificateDAL.updateById(cert.id, { profileId, renewBeforeDays: finalRenewBeforeDays }); return { certificate: bufferToString(certificate), issuingCaCertificate: bufferToString(issuingCaCertificate), certificateChain: bufferToString(certificateChain), privateKey: bufferToString(privateKey), serialNumber, certificateId: cert.id, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" }; }; const signCertificateFromProfile = async ({ profileId, csr, validity, notBefore, notAfter, actor, actorId, actorAuthMethod, actorOrgId, enrollmentType }: TSignCertificateFromProfileDTO): Promise> => { const profile = await validateProfileAndPermissions( profileId, actor, actorId, actorAuthMethod, actorOrgId, certificateProfileDAL, permissionService, enrollmentType ); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); } validateCaSupport(ca, "CSR signing"); const template = await certificateTemplateV2Service.getTemplateV2ById({ actor, actorId, actorAuthMethod, actorOrgId, templateId: profile.certificateTemplateId, internal: true }); if (!template) { throw new NotFoundError({ message: "Certificate template not found for this profile" }); } const certificateRequest = extractCertificateRequestFromCSR(csr); const mappedCertificateRequest = mapEnumsForValidation(certificateRequest); const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } = extractAlgorithmsFromCSR(csr); const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, mappedCertificateRequest ); if (!validationResult.isValid) { throw new BadRequestError({ message: `Certificate request validation failed: ${validationResult.errors.join(", ")}` }); } validateAlgorithmCompatibility(ca, template); 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 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)); await certificateDAL.updateById(cert.id, { profileId, renewBeforeDays: finalRenewBeforeDays }); const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer); const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer); return { certificate: certificateString, issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer), certificateChain: certificateChainString, serialNumber, certificateId: cert.id, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" }; }; const orderCertificateFromProfile = async ({ profileId, certificateOrder, actor, actorId, actorAuthMethod, actorOrgId }: TOrderCertificateFromProfileDTO): Promise => { const profile = await validateProfileAndPermissions( profileId, actor, actorId, actorAuthMethod, actorOrgId, certificateProfileDAL, permissionService, EnrollmentType.API ); const certificateRequest = { commonName: certificateOrder.commonName, keyUsages: certificateOrder.keyUsages, extendedKeyUsages: certificateOrder.extendedKeyUsages, subjectAlternativeNames: certificateOrder.altNames.map((san) => ({ type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS, value: san.value })), validity: certificateOrder.validity, notBefore: certificateOrder.notBefore, notAfter: certificateOrder.notAfter, signatureAlgorithm: certificateOrder.signatureAlgorithm, keyAlgorithm: certificateOrder.keyAlgorithm }; const mappedCertificateRequest = mapEnumsForValidation(certificateRequest); const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, mappedCertificateRequest ); if (!validationResult.isValid) { throw new BadRequestError({ message: `Certificate order validation failed: ${validationResult.errors.join(", ")}` }); } const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); } const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; if (caType === CaType.INTERNAL) { const certificateResult = await issueCertificateFromProfile({ profileId, certificateRequest, actor, actorId, actorAuthMethod, actorOrgId }); 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/v3/certificates/orders/${orderId}/completed`, certificate: certificateResult.certificate, projectId: certificateResult.projectId, profileName: certificateResult.profileName }; } if (caType === CaType.ACME) { throw new BadRequestError({ message: "ACME certificate ordering via profiles is not yet implemented." }); } throw new BadRequestError({ message: `Certificate ordering is not supported for CA type: ${caType}` }); }; const renewCertificate = async ({ certificateId, actor, actorId, actorAuthMethod, actorOrgId, internal = false }: TRenewCertificateDTO & { internal?: boolean }): Promise => { const renewalResult = await certificateDAL.transaction(async (tx) => { const originalCert = await certificateDAL.findById(certificateId, tx); if (!originalCert) { throw new NotFoundError({ message: "Certificate not found" }); } if (!originalCert.profileId) { throw new ForbiddenRequestError({ message: "Only certificates issued from a profile can be renewed" }); } const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm; const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm; if (!originalSignatureAlgorithm || !originalKeyAlgorithm) { throw new BadRequestError({ message: "Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented." }); } const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } if (profile.enrollmentType !== EnrollmentType.API) { throw new ForbiddenRequestError({ message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint" }); } const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx); if (!certificateSecret) { throw new ForbiddenRequestError({ message: "Certificate is not eligible for renewal: certificates issued from CSR (external private key) cannot be renewed" }); } if (!internal) { const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: profile.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.IssueCert, ProjectPermissionSub.CertificateProfiles ); } const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); } const eligibilityCheck = validateRenewalEligibility(originalCert, ca); if (!eligibilityCheck.isEligible) { await certificateDAL.updateById(originalCert.id, { renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` }); throw new BadRequestError({ message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` }); } validateCaSupport(ca, "direct certificate issuance"); const template = await certificateTemplateV2Service.getTemplateV2ById({ actor, actorId, actorAuthMethod, actorOrgId, templateId: profile.certificateTemplateId, internal }); if (!template) { throw new NotFoundError({ message: "Certificate template not found for this profile" }); } const originalTtlInDays = Math.ceil( (new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24) ); const ttl = `${originalTtlInDays}d`; const certificateRequest = { commonName: originalCert.commonName || undefined, keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)), extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy( parseExtendedKeyUsages(originalCert.extendedKeyUsages) ), subjectAlternativeNames: originalCert.altNames ? originalCert.altNames.split(",").map((san) => { const trimmed = san.trim(); const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed); const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed); if (isIpv4 || isIpv6) { return { type: CertSubjectAlternativeNameType.IP_ADDRESS, value: trimmed }; } if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(trimmed)) { return { type: CertSubjectAlternativeNameType.EMAIL, value: trimmed }; } if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(trimmed)) { return { type: CertSubjectAlternativeNameType.URI, value: trimmed }; } return { type: CertSubjectAlternativeNameType.DNS_NAME, value: trimmed }; }) : [], validity: { ttl }, signatureAlgorithm: originalCert.signatureAlgorithm || undefined, keyAlgorithm: originalCert.keyAlgorithm || undefined }; const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, certificateRequest ); if (!validationResult.isValid) { await certificateDAL.updateById(originalCert.id, { renewalError: `Template validation failed: ${validationResult.errors.join(", ")}` }); throw new BadRequestError({ message: `Certificate renewal failed. Errors: ${validationResult.errors.join(", ")}` }); } validateAlgorithmCompatibility(ca, template); const notBefore = new Date(); const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000); const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter); const { certificate, certificateChain, issuingCaCertificate, serialNumber } = await internalCaService.issueCertFromCa({ caId: ca.id, friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate", commonName: originalCert.commonName || "", altNames: originalCert.altNames || "", ttl, notBefore: normalizeDateForApi(notBefore), notAfter: normalizeDateForApi(notAfter), keyUsages: parseKeyUsages(originalCert.keyUsages), extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), signatureAlgorithm: originalSignatureAlgorithm, keyAlgorithm: originalKeyAlgorithm, isFromProfile: true, actor, actorId, actorAuthMethod, actorOrgId, internal: true, tx }); const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx); if (!newCert) { throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); } await certificateDAL.updateById( newCert.id, { profileId: originalCert.profileId, renewBeforeDays: finalRenewBeforeDays, renewedFromCertificateId: originalCert.id }, tx ); await certificateDAL.updateById( originalCert.id, { renewedByCertificateId: newCert.id, renewalError: null }, tx ); await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); return { certificate, certificateChain, issuingCaCertificate, serialNumber, newCert, originalCert, profile }; }); await triggerAutoSyncForCertificate(renewalResult.newCert.id, { certificateSyncDAL, pkiSyncDAL, pkiSyncQueue }); return { certificate: renewalResult.certificate, issuingCaCertificate: renewalResult.issuingCaCertificate, certificateChain: renewalResult.certificateChain, serialNumber: renewalResult.serialNumber, certificateId: renewalResult.newCert.id, projectId: renewalResult.profile.projectId, profileName: renewalResult.profile.slug, commonName: renewalResult.originalCert.commonName || "" }; }; const updateRenewalConfig = async ({ certificateId, renewBeforeDays, actor, actorId, actorAuthMethod, actorOrgId }: TUpdateRenewalConfigDTO): Promise => { const certificate = await certificateDAL.findById(certificateId); if (!certificate) { throw new NotFoundError({ message: "Certificate not found" }); } const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: certificate.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Edit, ProjectPermissionSub.Certificates ); if (!certificate.profileId) { throw new BadRequestError({ message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile" }); } const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } if (profile.enrollmentType !== EnrollmentType.API) { throw new ForbiddenRequestError({ message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed" }); } const certificateSecret = await certificateSecretDAL.findOne({ certId: certificate.id }); if (!certificateSecret) { throw new ForbiddenRequestError({ message: "Certificate is not eligible for auto-renewal: certificates issued from CSR (external private key) cannot be auto-renewed" }); } if (certificate.status !== CertStatus.ACTIVE) { throw new BadRequestError({ message: `Certificate is not eligible for auto-renewal: certificate status is ${certificate.status}, must be active` }); } const now = new Date(); if (certificate.notAfter <= now) { throw new BadRequestError({ message: "Certificate is not eligible for auto-renewal: certificate has expired" }); } if (certificate.revokedAt) { throw new BadRequestError({ message: "Certificate is not eligible for auto-renewal: certificate has been revoked" }); } if (certificate.renewedByCertificateId) { throw new BadRequestError({ message: "Certificate is not eligible for auto-renewal: certificate has already been renewed" }); } const certificateTtlInDays = Math.ceil( (new Date(certificate.notAfter).getTime() - new Date(certificate.notBefore).getTime()) / (24 * 60 * 60 * 1000) ); if (renewBeforeDays >= certificateTtlInDays) { throw new BadRequestError({ message: "Invalid renewal configuration: renewal threshold exceeds certificate validity period" }); } if (!isValidRenewalTiming(renewBeforeDays, new Date(certificate.notAfter))) { throw new BadRequestError({ message: "Invalid renewal configuration: renewal would be triggered immediately or in the past" }); } await certificateDAL.updateById(certificateId, { renewBeforeDays }); return { projectId: certificate.projectId, renewBeforeDays, commonName: certificate.commonName || "" }; }; const disableRenewalConfig = async ({ certificateId, actor, actorId, actorAuthMethod, actorOrgId }: TDisableRenewalConfigDTO): Promise => { const certificate = await certificateDAL.findById(certificateId); if (!certificate) { throw new NotFoundError({ message: "Certificate not found" }); } const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: certificate.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Edit, ProjectPermissionSub.Certificates ); if (!certificate.profileId) { throw new BadRequestError({ message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile" }); } const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } if (profile.enrollmentType !== EnrollmentType.API) { throw new ForbiddenRequestError({ message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed" }); } await certificateDAL.updateById(certificateId, { renewBeforeDays: null }); return { projectId: certificate.projectId, commonName: certificate.commonName || "" }; }; return { issueCertificateFromProfile, signCertificateFromProfile, orderCertificateFromProfile, renewCertificate, updateRenewalConfig, disableRenewalConfig }; };