Merge pull request #4925 from Infisical/feat/PKI-28

Add self-sign certs support
This commit is contained in:
carlosmonastyrski
2025-11-24 12:38:20 -03:00
committed by GitHub
22 changed files with 1407 additions and 248 deletions
@@ -67,6 +67,12 @@ export const certificateEstV3ServiceFactory = ({
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for EST enrollment"
});
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
@@ -169,6 +175,12 @@ export const certificateEstV3ServiceFactory = ({
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for EST enrollment"
});
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
@@ -281,6 +293,12 @@ export const certificateEstV3ServiceFactory = ({
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for EST enrollment"
});
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
@@ -7,6 +7,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
EnrollmentType,
IssuerType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
@@ -198,6 +199,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
issuerType: result.issuerType as IssuerType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
acmeConfigId: result.acmeConfigId,
@@ -239,12 +241,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
} = {},
tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
try {
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
const { offset = 0, limit = 20, search, enrollmentType, issuerType, caId } = options;
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
`${TableName.PkiCertificateProfile}.projectId`,
@@ -269,6 +272,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
}
if (issuerType) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
}
const query = baseQuery
.leftJoin(
TableName.PkiEstEnrollmentConfig,
@@ -338,8 +345,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
issuerType: result.issuerType as IssuerType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
acmeConfigId: result.acmeConfigId,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
estConfig,
@@ -359,12 +368,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
options: {
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
} = {},
tx?: Knex
): Promise<number> => {
try {
const { search, enrollmentType, caId } = options;
const { search, enrollmentType, issuerType, caId } = options;
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
@@ -384,6 +394,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
query = query.where({ caId });
}
if (issuerType) {
query = query.where({ issuerType });
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {
@@ -1,12 +1,13 @@
import RE2 from "re2";
import { z } from "zod";
import { EnrollmentType } from "./certificate-profile-types";
import { CertStatus } from "../certificate/certificate-types";
import { EnrollmentType, IssuerType } from "./certificate-profile-types";
export const createCertificateProfileSchema = z
.object({
projectId: z.string().uuid("Project ID must be valid"),
caId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
@@ -15,6 +16,7 @@ export const createCertificateProfileSchema = z
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
@@ -33,43 +35,100 @@ export const createCertificateProfileSchema = z
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.ACME) {
if (!data.acmeConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
return !!data.estConfig;
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
message: "EST enrollment type requires EST configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.caId;
}
return true;
},
{
message: "CA issuer type requires a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.caId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -83,6 +142,7 @@ export const updateCertificateProfileSchema = z
.optional(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
@@ -100,19 +160,34 @@ export const updateCertificateProfileSchema = z
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
return !data.apiConfig;
}
return true;
},
{
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
message: "EST enrollment type cannot have API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.enrollmentType || data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -131,6 +206,7 @@ export const listCertificateProfilesSchema = z.object({
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
caId: z.string().uuid().optional()
});
@@ -142,6 +218,6 @@ export const listCertificatesByProfileSchema = z.object({
profileId: z.string().uuid(),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
status: z.enum(["active", "expired", "revoked"]).optional(),
status: z.nativeEnum(CertStatus).optional(),
search: z.string().optional()
});
@@ -22,7 +22,12 @@ import type { TKmsServiceFactory } from "../kms/kms-service";
import type { TProjectDALFactory } from "../project/project-dal";
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types";
import {
EnrollmentType,
IssuerType,
TCertificateProfile,
TCertificateProfileWithConfigs
} from "./certificate-profile-types";
vi.mock("@app/lib/crypto/cryptography", () => ({
crypto: {
@@ -90,6 +95,7 @@ describe("CertificateProfileService", () => {
description: "Test certificate profile",
slug: "test-profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfigId: "api-config-123",
@@ -272,6 +278,7 @@ describe("CertificateProfileService", () => {
slug: "new-profile",
description: "New test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -312,6 +319,7 @@ describe("CertificateProfileService", () => {
slug: "new-profile",
description: "New test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfigId: "api-config-123",
@@ -383,6 +391,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-profile",
description: "Invalid test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123"
};
@@ -401,6 +410,7 @@ describe("CertificateProfileService", () => {
slug: "api-profile",
description: "Profile with API enrollment",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -726,6 +736,7 @@ describe("CertificateProfileService", () => {
slug: "est-profile",
description: "Profile with EST enrollment",
enrollmentType: EnrollmentType.EST,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
estConfig: {
@@ -776,6 +787,7 @@ describe("CertificateProfileService", () => {
slug: "different-profile-name",
description: "Profile with duplicate slug",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -801,6 +813,7 @@ describe("CertificateProfileService", () => {
slug: "auto-renew-profile",
description: "Profile with auto-renewal",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -965,6 +978,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-template-profile",
description: "Profile with invalid template",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "nonexistent-template",
apiConfig: {
@@ -990,6 +1004,7 @@ describe("CertificateProfileService", () => {
slug: "concurrent-profile",
description: "Profile created concurrently",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -1018,6 +1033,7 @@ describe("CertificateProfileService", () => {
slug: "cross-project-profile",
description: "Profile using template from different project",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-456",
apiConfig: {
@@ -1047,6 +1063,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-slug-profile",
description: "Profile with invalid slug format",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -32,6 +32,7 @@ import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
import {
EnrollmentType,
IssuerType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
@@ -39,6 +40,34 @@ import {
TCertificateProfileWithConfigs
} from "./certificate-profile-types";
const validateIssuerTypeConstraints = (
issuerType: IssuerType,
enrollmentType: EnrollmentType,
caId: string | null,
existingCaId?: string | null
) => {
if (issuerType === IssuerType.CA) {
if (!caId && !existingCaId) {
throw new ForbiddenRequestError({
message: "CA issuer type requires a Certificate Authority to be selected"
});
}
}
if (issuerType === IssuerType.SELF_SIGNED) {
if (caId) {
throw new ForbiddenRequestError({
message: "Self-signed issuer type cannot have a Certificate Authority"
});
}
if (enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Self-signed issuer type only supports API enrollment"
});
}
}
};
const generateAndEncryptAcmeEabSecret = async (
projectId: string,
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
@@ -163,7 +192,8 @@ export type TCertificateProfileServiceFactory = ReturnType<typeof certificatePro
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
return {
...dalResult,
enrollmentType: dalResult.enrollmentType as EnrollmentType
enrollmentType: dalResult.enrollmentType as EnrollmentType,
issuerType: dalResult.issuerType as IssuerType
} as TCertificateProfile;
};
@@ -240,6 +270,8 @@ export const certificateProfileServiceFactory = ({
});
}
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
// Validate enrollment configuration requirements
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
throw new ForbiddenRequestError({
@@ -376,7 +408,16 @@ export const certificateProfileServiceFactory = ({
}
}
const { estConfig, apiConfig, ...profileUpdateData } = data;
const finalIssuerType = data.issuerType || existingProfile.issuerType;
const finalEnrollmentType = data.enrollmentType || existingProfile.enrollmentType;
const finalCaId = data.caId !== undefined ? data.caId : existingProfile.caId;
validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId);
const updatedData =
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
const { estConfig, apiConfig, ...profileUpdateData } = updatedData;
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
if (estConfig && existingProfile.estConfigId) {
@@ -569,6 +610,7 @@ export const certificateProfileServiceFactory = ({
limit = 20,
search,
enrollmentType,
issuerType,
caId
}: {
actor: ActorType;
@@ -580,6 +622,7 @@ export const certificateProfileServiceFactory = ({
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
}): Promise<{
profiles: TCertificateProfileWithConfigs[];
@@ -603,12 +646,14 @@ export const certificateProfileServiceFactory = ({
limit,
search,
enrollmentType,
issuerType,
caId
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
search,
enrollmentType,
issuerType,
caId
});
@@ -10,16 +10,24 @@ export enum EnrollmentType {
ACME = "acme"
}
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType"> & {
export enum IssuerType {
CA = "ca",
SELF_SIGNED = "self-signed"
}
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType" | "issuerType"> & {
enrollmentType: EnrollmentType;
issuerType: IssuerType;
};
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType" | "issuerType"> & {
enrollmentType: EnrollmentType;
issuerType: IssuerType;
};
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType" | "issuerType"> & {
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
estConfig?: {
disableBootstrapCaValidation?: boolean;
passphrase?: string;
@@ -22,7 +22,7 @@ import {
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { EnrollmentType } 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 { ActorType, AuthMethod } from "../auth/auth-type";
@@ -40,18 +40,29 @@ vi.mock("../certificate-common/certificate-csr-utils", () => ({
describe("CertificateV3Service", () => {
let service: TCertificateV3ServiceFactory;
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
const mockCertificateDAL: Pick<
TCertificateDALFactory,
"findOne" | "findById" | "updateById" | "transaction" | "create"
> = {
findOne: vi.fn(),
findById: vi.fn(),
updateById: vi.fn(),
create: vi.fn().mockResolvedValue({
id: "new-cert-id",
serialNumber: "123456789",
friendlyName: "Test Certificate",
commonName: "test.example.com",
status: "ACTIVE"
}),
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
})
};
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
findOne: vi.fn()
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
findOne: vi.fn(),
create: vi.fn()
};
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
@@ -150,7 +161,24 @@ describe("CertificateV3Service", () => {
},
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
}
},
certificateBodyDAL: {
create: vi.fn().mockResolvedValue({ id: "body-123" })
},
kmsService: {
generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"),
encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))),
decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted")))
},
projectDAL: {
findOne: vi.fn().mockResolvedValue({ id: "project-123" }),
findById: vi.fn().mockResolvedValue({ id: "project-123" }),
updateById: vi.fn().mockResolvedValue({ id: "project-123" }),
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
const mockTx = {};
return callback(mockTx);
})
} as any
});
});
@@ -175,6 +203,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -319,6 +348,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -508,6 +538,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -561,6 +592,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -721,6 +753,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -772,6 +805,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -933,6 +967,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -971,6 +1006,7 @@ describe("CertificateV3Service", () => {
caId: "ca-1",
certificateTemplateId: "template-1",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
createdAt: new Date(),
updatedAt: new Date(),
description: "Test profile for algorithm compatibility",
@@ -1552,6 +1588,7 @@ describe("CertificateV3Service", () => {
id: "profile-123",
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -1733,9 +1770,9 @@ describe("CertificateV3Service", () => {
});
});
it("should reject renewal if certificate is not from a profile", async () => {
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
it("should reject renewal if certificate has no profile and no CA", async () => {
const certWithoutProfileAndCA = { ...mockOriginalCert, profileId: null, caId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfileAndCA);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
@@ -2008,6 +2045,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2084,6 +2122,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2129,6 +2168,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2172,6 +2212,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -1,8 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { randomUUID } from "crypto";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { ActionProjectType, TCertificates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateActions,
@@ -10,8 +11,11 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
@@ -28,12 +32,25 @@ import {
TCertificateAuthorityWithAssociatedCa
} from "@app/services/certificate-authority/certificate-authority-dal";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
createDistinguishedName,
createSerialNumber,
keyAlgorithmToAlgCfg,
signatureAlgorithmToAlgCfg
} 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 } 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";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType
} from "../certificate-common/certificate-constants";
import {
extractAlgorithmsFromCSR,
extractCertificateRequestFromCSR
@@ -68,8 +85,9 @@ import {
} from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction" | "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
@@ -85,6 +103,8 @@ type TCertificateV3ServiceFactoryDep = {
>;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
projectDAL: TProjectDALFactory;
};
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
@@ -329,6 +349,158 @@ const parseTtlToDays = (ttl: string): number => {
}
};
const generateSelfSignedCertificate = async ({
certificateRequest,
template,
effectiveSignatureAlgorithm,
effectiveKeyAlgorithm
}: {
certificateRequest: {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
altNames?: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
validity: { ttl: string };
notBefore?: Date;
notAfter?: Date;
};
template?: {
subject?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
sans?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
} | null;
effectiveSignatureAlgorithm: CertSignatureAlgorithm;
effectiveKeyAlgorithm: CertKeyAlgorithm;
}): Promise<{
certificate: Buffer;
privateKey: Buffer;
serialNumber: string;
notBefore: Date;
notAfter: Date;
certificateSubject: Record<string, unknown>;
subjectAlternativeNames: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
}> => {
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
{ subjectAlternativeNames: certificateRequest.altNames },
template?.sans
);
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
const keyPair = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
const signatureAlgorithmConfig = signatureAlgorithmToAlgCfg(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
const notBeforeDate = certificateRequest.notBefore ? new Date(certificateRequest.notBefore) : new Date();
let notAfterDate: Date;
if (certificateRequest.notAfter) {
notAfterDate = new Date(certificateRequest.notAfter);
} else if (certificateRequest.validity.ttl) {
notAfterDate = new Date(new Date().getTime() + ms(certificateRequest.validity.ttl));
} else {
throw new BadRequestError({
message: "Either notAfter date or TTL must be provided for certificate validity"
});
}
const serialNumber = createSerialNumber();
const dn = createDistinguishedName({
commonName: certificateSubject.common_name,
organization: certificateSubject.organization,
ou: certificateSubject.organizational_unit,
country: certificateSubject.country,
province: certificateSubject.state_or_province_name,
locality: certificateSubject.locality_name
});
const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: dn,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingAlgorithm: signatureAlgorithmConfig,
keys: keyPair,
extensions: [
new x509.BasicConstraintsExtension(false, undefined, false),
...(certificateRequest.keyUsages?.length
? [
new x509.KeyUsagesExtension(
(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []).reduce(
// eslint-disable-next-line no-bitwise
(acc: number, usage) => acc | x509.KeyUsageFlags[usage],
0
),
false
)
]
: []),
...(certificateRequest.extendedKeyUsages?.length
? [
new x509.ExtendedKeyUsageExtension(
(convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || []).map(
(eku) => x509.ExtendedKeyUsage[eku]
),
false
)
]
: []),
...(subjectAlternativeNames
? [
new x509.SubjectAlternativeNameExtension(
certificateRequest.altNames?.map((san) => {
switch (san.type) {
case CertSubjectAlternativeNameType.DNS_NAME:
return { type: "dns" as const, value: san.value };
case CertSubjectAlternativeNameType.IP_ADDRESS:
return { type: "ip" as const, value: san.value };
case CertSubjectAlternativeNameType.EMAIL:
return { type: "email" as const, value: san.value };
case CertSubjectAlternativeNameType.URI:
return { type: "url" as const, value: san.value };
default:
throw new BadRequestError({
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
});
}
}) || [],
false
)
]
: [])
]
});
const certificatePem = cert.toString("pem");
const privateKeyObj = crypto.nativeCrypto.KeyObject.from(keyPair.privateKey);
const privateKeyPem = privateKeyObj.export({ format: "pem", type: "pkcs8" }) as string;
return {
certificate: Buffer.from(certificatePem),
privateKey: Buffer.from(privateKeyPem),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
certificateSubject,
subjectAlternativeNames: certificateRequest.altNames || []
};
};
const calculateFinalRenewBeforeDays = (
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
ttl: string,
@@ -348,8 +520,248 @@ const calculateFinalRenewBeforeDays = (
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
};
const getEffectiveAlgorithms = (
requestSignatureAlgorithm?: CertSignatureAlgorithm,
requestKeyAlgorithm?: CertKeyAlgorithm,
originalSignatureAlgorithm?: CertSignatureAlgorithm,
originalKeyAlgorithm?: CertKeyAlgorithm
) => {
return {
signatureAlgorithm: requestSignatureAlgorithm || originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256,
keyAlgorithm: requestKeyAlgorithm || originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048
};
};
const createSelfSignedCertificateRecord = async ({
selfSignedResult,
certificateRequest,
profile,
originalCert,
certificateDAL,
tx,
isRenewal = false
}: {
selfSignedResult: Awaited<ReturnType<typeof generateSelfSignedCertificate>>;
certificateRequest: {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
};
profile?: { id: string; projectId: string } | null;
originalCert?: {
id: string;
friendlyName?: string | null;
commonName?: string | null;
projectId: string;
};
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
tx: Parameters<TCertificateDALFactory["create"]>[1];
isRenewal?: boolean;
}) => {
const subjectCommonName =
(selfSignedResult.certificateSubject.common_name as string) ||
certificateRequest.commonName ||
originalCert?.commonName ||
"";
const altNamesList = selfSignedResult.subjectAlternativeNames.map((san) => san.value).join(",");
const projectId = originalCert?.projectId || profile?.projectId;
if (!projectId) {
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
}
const baseRecord = {
serialNumber: selfSignedResult.serialNumber,
friendlyName: originalCert?.friendlyName || subjectCommonName,
commonName: subjectCommonName,
altNames: altNamesList,
status: CertStatus.ACTIVE,
notBefore: selfSignedResult.notBefore,
notAfter: selfSignedResult.notAfter,
projectId,
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
profileId: profile?.id || null
};
const renewalRecord =
isRenewal && originalCert
? {
renewedFromCertificateId: originalCert.id
}
: {};
return certificateDAL.create(
{
...baseRecord,
...renewalRecord
},
tx
);
};
const createEncryptedCertificateData = async ({
certificateId,
certificate,
privateKey,
projectId,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
tx
}: {
certificateId: string;
certificate: Buffer;
privateKey: Buffer;
projectId: string;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
projectDAL: TProjectDALFactory;
tx: Parameters<TCertificateBodyDALFactory["create"]>[1];
}) => {
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKeyId });
const encryptedCertificate = await kmsEncryptor({
plainText: certificate
});
await certificateBodyDAL.create(
{
certId: certificateId,
encryptedCertificate: encryptedCertificate.cipherTextBlob
},
tx
);
const encryptedPrivateKey = await kmsEncryptor({
plainText: privateKey
});
await certificateSecretDAL.create(
{
certId: certificateId,
encryptedPrivateKey: encryptedPrivateKey.cipherTextBlob
},
tx
);
};
const processSelfSignedCertificate = async ({
certificateRequest,
template,
profile,
originalCert,
effectiveAlgorithms,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
tx,
isRenewal = false
}: {
certificateRequest: {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
validity: { ttl: string };
notBefore?: Date;
notAfter?: Date;
};
template?: {
subject?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
sans?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
} | null;
profile?: { id: string; projectId: string } | null;
originalCert?: {
id: string;
friendlyName?: string | null;
commonName?: string | null;
projectId: string;
};
effectiveAlgorithms: {
signatureAlgorithm: CertSignatureAlgorithm;
keyAlgorithm: CertKeyAlgorithm;
};
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
projectDAL: TProjectDALFactory;
tx: Parameters<TCertificateDALFactory["create"]>[1];
isRenewal?: boolean;
}) => {
const projectId = originalCert?.projectId || profile?.projectId;
if (!projectId) {
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
}
const selfSignedResult = await generateSelfSignedCertificate({
certificateRequest,
template,
effectiveSignatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
effectiveKeyAlgorithm: effectiveAlgorithms.keyAlgorithm
});
const certificateData = await createSelfSignedCertificateRecord({
selfSignedResult,
certificateRequest,
profile,
originalCert,
certificateDAL,
tx,
isRenewal
});
await certificateDAL.updateById(
certificateData.id,
{
signatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
keyAlgorithm: effectiveAlgorithms.keyAlgorithm
},
tx
);
await createEncryptedCertificateData({
certificateId: certificateData.id,
certificate: selfSignedResult.certificate,
privateKey: selfSignedResult.privateKey,
projectId,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
tx
});
return {
selfSignedResult,
certificateData
};
};
export const certificateV3ServiceFactory = ({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateProfileDAL,
@@ -359,7 +771,9 @@ export const certificateV3ServiceFactory = ({
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
pkiSyncQueue,
kmsService,
projectDAL
}: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({
profileId,
@@ -416,15 +830,6 @@ export const certificateV3ServiceFactory = ({
});
}
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;
@@ -440,12 +845,76 @@ export const certificateV3ServiceFactory = ({
});
}
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
{ subjectAlternativeNames: certificateRequest.altNames },
template.sans
template?.sans
);
const issuerType = profile?.issuerType || (profile?.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
if (issuerType === IssuerType.SELF_SIGNED) {
const result = await certificateDAL.transaction(async (tx) => {
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
return processSelfSignedCertificate({
certificateRequest,
template,
profile,
effectiveAlgorithms,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
tx
});
});
const { selfSignedResult, certificateData } = result;
const subjectCommonName =
(selfSignedResult.certificateSubject.common_name as string) ||
certificateRequest.commonName ||
"Self-signed Certificate";
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
profile,
certificateRequest.validity.ttl,
selfSignedResult.notAfter
);
if (finalRenewBeforeDays !== undefined) {
await certificateDAL.updateById(certificateData.id, {
renewBeforeDays: finalRenewBeforeDays
});
}
return {
certificate: selfSignedResult.certificate.toString("utf8"),
issuingCaCertificate: "",
certificateChain: selfSignedResult.certificate.toString("utf8"),
privateKey: selfSignedResult.privateKey.toString("utf8"),
serialNumber: selfSignedResult.serialNumber,
certificateId: certificateData.id,
projectId: profile.projectId,
profileName: profile.slug,
commonName: subjectCommonName
};
}
if (!profile.caId) {
throw new NotFoundError({ message: "Certificate Authority ID not found" });
}
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 { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
await internalCaService.issueCertFromCa({
caId: ca.id,
@@ -477,10 +946,11 @@ export const certificateV3ServiceFactory = ({
new Date(cert.notAfter)
);
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(cert.id, updateData);
let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) {
@@ -525,6 +995,12 @@ export const certificateV3ServiceFactory = ({
enrollmentType
);
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for CSR signing"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
@@ -592,10 +1068,11 @@ export const certificateV3ServiceFactory = ({
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
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);
@@ -640,10 +1117,25 @@ export const certificateV3ServiceFactory = ({
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
})),
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,
@@ -663,6 +1155,12 @@ export const certificateV3ServiceFactory = ({
});
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for certificate ordering"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
@@ -741,15 +1239,19 @@ export const certificateV3ServiceFactory = ({
});
}
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
let profile = null;
if (originalCert.profileId) {
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"
});
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message:
"Certificate is not eligible for renewal: Only certificates issued from an API enrollment profile can be renewed through this endpoint"
});
}
}
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
@@ -761,10 +1263,11 @@ export const certificateV3ServiceFactory = ({
}
if (!internal) {
const projectId = profile?.projectId || originalCert.projectId;
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -776,33 +1279,46 @@ export const certificateV3ServiceFactory = ({
);
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
let ca;
if (issuerType === IssuerType.CA) {
const caId = profile?.caId || originalCert.caId;
if (!caId) {
throw new NotFoundError({ message: "Certificate Authority ID not found" });
}
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(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 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(", ")}`
});
}
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
const template = templateId
? await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId,
internal
})
: null;
validateCaSupport(ca, "direct certificate issuance");
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId,
internal
});
if (!template) {
if (!template && profile) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
@@ -857,10 +1373,13 @@ export const certificateV3ServiceFactory = ({
keyAlgorithm: originalCert.keyAlgorithm || undefined
};
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
let validationResult: { isValid: boolean; errors: string[] } = { isValid: true, errors: [] };
if (profile?.certificateTemplateId) {
validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
}
if (!validationResult.isValid) {
await certificateDAL.updateById(originalCert.id, {
@@ -872,14 +1391,28 @@ export const certificateV3ServiceFactory = ({
});
}
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 finalRenewBeforeDays = profile ? calculateFinalRenewBeforeDays(profile, ttl, notAfter) : undefined;
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
await internalCaService.issueCertFromCa({
let certificate: string;
let certificateChain: string;
let issuingCaCertificate: string;
let serialNumber: string;
let newCert: TCertificates;
if (issuerType === IssuerType.CA) {
// CA-signed certificate renewal
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" });
}
validateAlgorithmCompatibility(ca, {
algorithms: template?.algorithms
} as { algorithms?: { signature?: string[] } });
const caResult = await internalCaService.issueCertFromCa({
caId: ca.id,
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
commonName: originalCert.commonName || "",
@@ -900,20 +1433,72 @@ export const certificateV3ServiceFactory = ({
tx
});
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
certificate = caResult.certificate;
certificateChain = caResult.certificateChain;
issuingCaCertificate = caResult.issuingCaCertificate;
serialNumber = caResult.serialNumber;
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
if (!foundCert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
newCert = foundCert;
} else {
// Self-signed certificate renewal
const effectiveAlgorithms = getEffectiveAlgorithms(
undefined,
undefined,
originalSignatureAlgorithm,
originalKeyAlgorithm
);
const selfSignedRenewalResult = await processSelfSignedCertificate({
certificateRequest,
template,
profile,
originalCert,
effectiveAlgorithms,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
tx,
isRenewal: true
});
certificate = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8");
certificateChain = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8"); // Self-signed has no chain
issuingCaCertificate = ""; // No issuing CA for self-signed
serialNumber = selfSignedRenewalResult.selfSignedResult.serialNumber;
newCert = selfSignedRenewalResult.certificateData;
}
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,
// For self-signed certificates, we already set the renewal data during creation
// For CA-signed certificates, we need to set it now
if (issuerType === IssuerType.CA) {
const renewalUpdateData: {
profileId: string | null;
renewedFromCertificateId: string;
renewBeforeDays?: number;
} = {
profileId: originalCert.profileId || null,
renewedFromCertificateId: originalCert.id
},
tx
);
};
if (finalRenewBeforeDays !== undefined) {
renewalUpdateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(newCert.id, renewalUpdateData, tx);
} else if (finalRenewBeforeDays !== undefined) {
// For self-signed certificates, just update the renewBeforeDays if needed
await certificateDAL.updateById(newCert.id, { renewBeforeDays: finalRenewBeforeDays }, tx);
}
await certificateDAL.updateById(
originalCert.id,
@@ -953,8 +1538,8 @@ export const certificateV3ServiceFactory = ({
certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id,
projectId: renewalResult.profile.projectId,
profileName: renewalResult.profile.slug,
projectId: renewalResult.originalCert.projectId,
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
commonName: renewalResult.originalCert.commonName || ""
};
};
@@ -309,6 +309,14 @@ export const certificateServiceFactory = ({
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
if (!certBody) {
throw new NotFoundError({ message: "Certificate body not found" });
}
if (!certBody.encryptedCertificate) {
throw new BadRequestError({ message: "Certificate data not available" });
}
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: cert.projectId,
projectDAL,
@@ -599,6 +607,14 @@ export const certificateServiceFactory = ({
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
if (!certBody) {
throw new NotFoundError({ message: "Certificate body not found" });
}
if (!certBody.encryptedCertificate) {
throw new BadRequestError({ message: "Certificate data not available" });
}
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: cert.projectId,
projectDAL,