Address PR suggestions

This commit is contained in:
Carlos Monastyrski
2025-10-24 03:02:26 -03:00
parent e364eb15db
commit 0ba6b5e86b
21 changed files with 578 additions and 439 deletions
@@ -6,7 +6,6 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
import { categorizeCertificateRenewalError } from "../certificate-common/certificate-utils";
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
type TCertificateV3QueueServiceFactoryDep = {
@@ -70,9 +69,6 @@ export const certificateV3QueueServiceFactory = ({
internal: true
});
await certificateDAL.updateById(certificate.id, {
renewalError: null
});
totalCertificatesRenewed += 1;
await auditLogService.createAuditLog({
@@ -87,42 +83,32 @@ export const certificateV3QueueServiceFactory = ({
certificateId: certificate.id,
commonName: certificate.commonName || "",
profileId: certificate.profileId!,
renewBeforeDays: certificate.renewBeforeDays?.toString() || ""
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
profileName: certificate.profileName || ""
}
}
});
} catch (error) {
const categorizedError: string = categorizeCertificateRenewalError(error);
try {
await certificateDAL.updateById(certificate.id, {
renewalError: categorizedError
});
} catch (updateError) {
logger.error(updateError, `Failed to update renewal error for certificate ${certificate.id}`);
}
try {
await auditLogService.createAuditLog({
projectId: certificate.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
metadata: {
certificateId: certificate.id,
commonName: certificate.commonName || "",
profileId: certificate.profileId || "",
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
error: categorizedError
}
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(error, `Failed to renew certificate ${certificate.id}: ${errorMessage}`);
await auditLogService.createAuditLog({
projectId: certificate.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
metadata: {
certificateId: certificate.id,
commonName: certificate.commonName || "",
profileId: certificate.profileId || "",
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
profileName: certificate.profileName || "",
error: errorMessage
}
});
} catch (auditError) {
logger.error(auditError, `Failed to create audit log for failed certificate renewal ${certificate.id}`);
}
}
});
}
}
@@ -29,10 +29,14 @@ import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./cer
describe("CertificateV3Service", () => {
let service: TCertificateV3ServiceFactory;
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById"> = {
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
findOne: vi.fn(),
findById: vi.fn(),
updateById: vi.fn()
updateById: vi.fn(),
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
})
};
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
@@ -78,7 +82,7 @@ describe("CertificateV3Service", () => {
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();
vi.resetAllMocks();
// Mock ForbiddenError.from static method
vi.spyOn(ForbiddenError, "from").mockReturnValue({
@@ -1473,7 +1477,7 @@ describe("CertificateV3Service", () => {
notBefore: new Date("2024-01-01"),
notAfter: new Date("2024-02-01"), // 31 days
revokedAt: null,
renewedById: null,
renewedByCertificateId: null,
profileId: "profile-123",
renewBeforeDays: 7,
caId: "ca-123",
@@ -1487,7 +1491,7 @@ describe("CertificateV3Service", () => {
certificateTemplateId: "template-123",
revocationReason: null,
caCertId: null,
renewedFromId: null,
renewedFromCertificateId: null,
renewalError: null,
keyAlgorithm: "RSA_2048",
signatureAlgorithm: "RSA-SHA256"
@@ -1570,8 +1574,6 @@ describe("CertificateV3Service", () => {
};
beforeEach(() => {
vi.clearAllMocks();
// Mock current date to be within renewal window
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-26")); // 6 days before cert expires, within renewal window
@@ -1582,6 +1584,7 @@ describe("CertificateV3Service", () => {
});
it("should successfully renew eligible certificate", async () => {
// Mock the initial findById call
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
@@ -1604,6 +1607,13 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
// Mock the transaction to return the expected structure
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
const result = await callback(mockTx);
return result;
});
const result = await service.renewCertificate({
certificateId: "cert-123",
...mockActor
@@ -1611,15 +1621,23 @@ describe("CertificateV3Service", () => {
expect(result).toHaveProperty("certificate", "renewed-cert");
expect(result).toHaveProperty("certificateId", "cert-456");
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-456", {
profileId: "profile-123",
renewBeforeDays: 14,
renewedFromId: "cert-123"
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
renewedById: "cert-456",
renewalError: null
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
"cert-456",
{
profileId: "profile-123",
renewBeforeDays: 14,
renewedFromCertificateId: "cert-123"
},
{}
);
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
"cert-123",
{
renewedByCertificateId: "cert-456",
renewalError: null
},
{}
);
});
it("should validate certificate against current template during renewal", async () => {
@@ -1633,6 +1651,15 @@ describe("CertificateV3Service", () => {
warnings: []
});
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1645,9 +1672,7 @@ describe("CertificateV3Service", () => {
certificateId: "cert-123",
...mockActor
})
).rejects.toThrow(
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: Subject alternative name not allowed"
);
).rejects.toThrow("Certificate renewal failed. Errors: Subject alternative name not allowed");
// Should store template validation error
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -1659,6 +1684,12 @@ describe("CertificateV3Service", () => {
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1675,11 +1706,20 @@ describe("CertificateV3Service", () => {
});
it("should reject renewal if certificate is already renewed", async () => {
const alreadyRenewedCert = { ...mockOriginalCert, renewedById: "cert-456" };
const alreadyRenewedCert = { ...mockOriginalCert, renewedByCertificateId: "cert-456" };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(alreadyRenewedCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1704,6 +1744,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(expiredCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1728,6 +1777,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(revokedCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1749,6 +1807,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1776,6 +1843,15 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
// Mock updateById to handle the renewal error logging
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
await expect(
service.renewCertificate({
certificateId: "cert-123",
@@ -1816,6 +1892,12 @@ describe("CertificateV3Service", () => {
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
// Set up transaction mock to properly handle the renewal process
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
});
const result = await service.renewCertificate({
certificateId: "cert-123",
...mockActor
@@ -1830,12 +1912,13 @@ describe("CertificateV3Service", () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: null,
renewedByCertificateId: null,
notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-02-01"),
projectId: "project-123",
status: CertStatus.ACTIVE,
revokedAt: null
revokedAt: null,
commonName: ""
};
const mockProfile = {
@@ -1859,7 +1942,8 @@ describe("CertificateV3Service", () => {
expect(result).toEqual({
projectId: "project-123",
renewBeforeDays: 7
renewBeforeDays: 7,
commonName: ""
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -1871,7 +1955,7 @@ describe("CertificateV3Service", () => {
const mockCert = {
id: "cert-123",
profileId: null,
renewedById: null,
renewedByCertificateId: null,
projectId: "project-123"
};
@@ -1904,7 +1988,7 @@ describe("CertificateV3Service", () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: "cert-456",
renewedByCertificateId: "cert-456",
projectId: "project-123",
status: CertStatus.ACTIVE,
revokedAt: null,
@@ -1948,7 +2032,7 @@ describe("CertificateV3Service", () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
renewedById: null,
renewedByCertificateId: null,
notBefore: new Date("2026-01-01"),
notAfter: new Date("2026-01-08"),
projectId: "project-123",
@@ -1994,7 +2078,8 @@ describe("CertificateV3Service", () => {
const mockCert = {
id: "cert-123",
profileId: "profile-123",
projectId: "project-123"
projectId: "project-123",
commonName: ""
};
const mockProfile = {
@@ -2016,7 +2101,8 @@ describe("CertificateV3Service", () => {
});
expect(result).toEqual({
projectId: "project-123"
projectId: "project-123",
commonName: ""
});
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
@@ -16,6 +16,7 @@ import {
CertExtendedKeyUsage,
CertificateOrderStatus,
CertKeyAlgorithm,
CertKeyType,
CertKeyUsage,
CertSignatureAlgorithm,
CertStatus
@@ -56,7 +57,7 @@ import {
} from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById">;
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
certificateTemplateV2Service: Pick<
@@ -114,7 +115,7 @@ const validateRenewalEligibility = (
notBefore: Date;
notAfter: Date;
revokedAt?: Date | null;
renewedById?: string | null;
renewedByCertificateId?: string | null;
profileId?: string | null;
caId?: string | null;
pkiSubscriberId?: string | null;
@@ -153,7 +154,7 @@ const validateRenewalEligibility = (
errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`);
}
if (certificate.renewedById) {
if (certificate.renewedByCertificateId) {
errors.push("Certificate has already been renewed");
}
@@ -212,11 +213,11 @@ const validateAlgorithmCompatibility = (
const keyType = parts[parts.length - 1];
if (caKeyAlgorithm.startsWith("RSA")) {
return keyType === "RSA";
return keyType === CertKeyType.RSA;
}
if (caKeyAlgorithm.startsWith("EC")) {
return keyType === "ECDSA";
return keyType === CertKeyType.ECDSA;
}
return false;
@@ -338,7 +339,8 @@ export const certificateV3ServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId
templateId: profile.certificateTemplateId,
internal: true
});
if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
@@ -362,10 +364,6 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "direct certificate issuance");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
}
validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
@@ -433,7 +431,8 @@ export const certificateV3ServiceFactory = ({
serialNumber,
certificateId: cert.id,
projectId: profile.projectId,
profileName: profile.slug
profileName: profile.slug,
commonName: cert.commonName || ""
};
};
@@ -468,16 +467,13 @@ export const certificateV3ServiceFactory = ({
validateCaSupport(ca, "CSR signing");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
}
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId
templateId: profile.certificateTemplateId,
internal: true
});
if (!template) {
@@ -541,7 +537,8 @@ export const certificateV3ServiceFactory = ({
serialNumber,
certificateId: cert.id,
projectId: profile.projectId,
profileName: profile.slug
profileName: profile.slug,
commonName: cert.commonName || ""
};
};
@@ -645,178 +642,224 @@ export const certificateV3ServiceFactory = ({
actorOrgId,
internal = false
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const originalCert = await certificateDAL.findById(certificateId);
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 !== "api") {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
});
}
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) {
throw new BadRequestError({
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
}
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
);
}
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 isIp =
trimmed.length <= 45 &&
(new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed) ||
new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed));
return {
type: isIp ? CertSubjectAlternativeNameType.IP_ADDRESS : CertSubjectAlternativeNameType.DNS_NAME,
value: trimmed
};
})
: [],
validity: {
ttl
const renewalResult = await certificateDAL.transaction(async (tx) => {
const originalCert = await certificateDAL.findById(certificateId, tx);
if (!originalCert) {
throw new NotFoundError({ message: "Certificate not found" });
}
};
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
if (!originalCert.profileId) {
throw new ForbiddenRequestError({
message: "Only certificates issued from a profile can be renewed"
});
}
if (!validationResult.isValid) {
await certificateDAL.updateById(originalCert.id, {
renewalError: `Template validation failed: ${validationResult.errors.join(", ")}`
});
const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
throw new BadRequestError({
message: `Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: ${validationResult.errors.join(", ")}`
});
}
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
throw new BadRequestError({
message:
"Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
});
}
validateAlgorithmCompatibility(ca, template);
const notBefore = new Date();
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
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,
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
});
}
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
});
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!newCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
const certificateTtlInDays = parseTtlToDays(ttl);
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
const originalTtlInDays = Math.ceil(
(new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24)
);
const ttl = `${originalTtlInDays}d`;
await certificateDAL.updateById(newCert.id, {
profileId: originalCert.profileId,
renewBeforeDays: finalRenewBeforeDays,
renewedFromId: originalCert.id
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 certificateTtlInDays = parseTtlToDays(ttl);
const finalRenewBeforeDays = calculateRenewalThreshold(profile.apiConfig?.renewBeforeDays, certificateTtlInDays);
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
);
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber,
newCert,
originalCert,
profile
};
});
await certificateDAL.updateById(originalCert.id, {
renewedById: newCert.id,
renewalError: null
});
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: newCert.id,
projectId: profile.projectId,
profileName: profile.slug
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 || ""
};
};
@@ -858,7 +901,7 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== "api") {
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
});
@@ -883,7 +926,7 @@ export const certificateV3ServiceFactory = ({
});
}
if (certificate.renewedById) {
if (certificate.renewedByCertificateId) {
throw new BadRequestError({
message: "Certificate is not eligible for auto-renewal: certificate has already been renewed"
});
@@ -911,7 +954,8 @@ export const certificateV3ServiceFactory = ({
return {
projectId: certificate.projectId,
renewBeforeDays
renewBeforeDays,
commonName: certificate.commonName || ""
};
};
@@ -952,7 +996,7 @@ export const certificateV3ServiceFactory = ({
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== "api") {
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
});
@@ -963,7 +1007,8 @@ export const certificateV3ServiceFactory = ({
});
return {
projectId: certificate.projectId
projectId: certificate.projectId,
commonName: certificate.commonName || ""
};
};
@@ -68,6 +68,7 @@ export type TCertificateFromProfileResponse = {
certificateId: string;
projectId: string;
profileName: string;
commonName: string;
};
export type TCertificateOrderResponse = {
@@ -114,8 +115,10 @@ export type TDisableRenewalConfigDTO = {
export type TRenewalConfigResponse = {
projectId: string;
renewBeforeDays: number;
commonName: string;
};
export type TDisableRenewalResponse = {
projectId: string;
commonName: string;
};