mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 21:21:29 +00:00
Add self-sign certs support
This commit is contained in:
@@ -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,17 @@
|
||||
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
|
||||
.union([z.string().uuid(), z.literal("")])
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val === "" ? null : val)),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
slug: z
|
||||
.string()
|
||||
@@ -15,6 +20,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),
|
||||
@@ -32,6 +38,7 @@ export const createCertificateProfileSchema = z
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Validate enrollment type configurations
|
||||
if (data.enrollmentType === EnrollmentType.EST) {
|
||||
if (!data.estConfig) {
|
||||
return false;
|
||||
@@ -65,11 +72,26 @@ export const createCertificateProfileSchema = z
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.issuerType === IssuerType.CA) {
|
||||
if (!data.caId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||
if (data.caId) {
|
||||
return false;
|
||||
}
|
||||
if (data.enrollmentType !== EnrollmentType.API) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration. CA issuer type requires a CA ID. Self-signed issuer type cannot have a CA ID and only supports API enrollment."
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,6 +105,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),
|
||||
@@ -109,10 +132,18 @@ export const updateCertificateProfileSchema = z
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||
if (data.enrollmentType && data.enrollmentType !== EnrollmentType.API) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
|
||||
message:
|
||||
"Cannot have EST config with API enrollment type or API config with EST enrollment type. Self-signed issuer type only supports API enrollment."
|
||||
}
|
||||
);
|
||||
|
||||
@@ -131,6 +162,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 +174,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,18 @@ 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.issuerType === IssuerType.SELF_SIGNED
|
||||
? { ...data, caId: null }
|
||||
: data;
|
||||
|
||||
const { estConfig, apiConfig, ...profileUpdateData } = updatedData;
|
||||
|
||||
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||
if (estConfig && existingProfile.estConfigId) {
|
||||
@@ -569,6 +612,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
issuerType,
|
||||
caId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
@@ -580,6 +624,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit?: number;
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
issuerType?: IssuerType;
|
||||
caId?: string;
|
||||
}): Promise<{
|
||||
profiles: TCertificateProfileWithConfigs[];
|
||||
@@ -603,12 +648,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;
|
||||
|
||||
Reference in New Issue
Block a user