mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 08:28:59 +00:00
PKI revamp: general improvements
This commit is contained in:
@@ -7,6 +7,7 @@ import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertStatus } from "@app/services/certificate/certificate-types";
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
|
||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -453,7 +454,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
querystring: z.object({
|
||||
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()
|
||||
}),
|
||||
response: {
|
||||
|
||||
@@ -54,8 +54,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
commonName: validateTemplateRegexField,
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
@@ -162,8 +166,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
csr: z.string().trim().min(1).max(4096),
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional()
|
||||
})
|
||||
@@ -234,11 +242,19 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string()
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "SAN value cannot be empty")
|
||||
.max(255, "SAN value must be less than 255 characters")
|
||||
})
|
||||
)
|
||||
.min(1),
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
.min(1, "At least one subject alternative name must be provided"),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
|
||||
+23
-8
@@ -32,6 +32,7 @@ import {
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertSignatureAlgorithm,
|
||||
CertStatus,
|
||||
TAltNameMapping
|
||||
} from "../../certificate/certificate-types";
|
||||
@@ -1289,12 +1290,19 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||
const requestedKeyType = signatureAlgorithm.split("-")[0];
|
||||
|
||||
const isRsaCa = caKeyAlgorithm.startsWith("RSA");
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith("EC");
|
||||
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
|
||||
|
||||
if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) {
|
||||
if (
|
||||
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
|
||||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
|
||||
) {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown";
|
||||
const supportedType = isRsaCa
|
||||
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
|
||||
: isEcdsaCa
|
||||
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
|
||||
: "unknown";
|
||||
throw new BadRequestError({
|
||||
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
|
||||
});
|
||||
@@ -1655,12 +1663,19 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
|
||||
|
||||
const isRsaCa = caKeyAlgorithm.startsWith("RSA");
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith("EC");
|
||||
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
|
||||
|
||||
if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) {
|
||||
if (
|
||||
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
|
||||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
|
||||
) {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown";
|
||||
const supportedType = isRsaCa
|
||||
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
|
||||
: isEcdsaCa
|
||||
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
|
||||
: "unknown";
|
||||
throw new BadRequestError({
|
||||
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
|
||||
});
|
||||
|
||||
@@ -559,7 +559,6 @@ describe("CertificateProfileService", () => {
|
||||
|
||||
expect(result).toEqual(sampleProfile);
|
||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||
expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123");
|
||||
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
||||
});
|
||||
|
||||
@@ -573,18 +572,6 @@ describe("CertificateProfileService", () => {
|
||||
})
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it("should throw ForbiddenRequestError when profile is in use", async () => {
|
||||
(mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
service.deleteProfile({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
})
|
||||
).rejects.toThrow(ForbiddenRequestError);
|
||||
expect(mockCertificateProfileDAL.deleteById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProfileCertificates", () => {
|
||||
@@ -841,23 +828,8 @@ describe("CertificateProfileService", () => {
|
||||
expect(result.enrollmentType).toBe(EnrollmentType.EST);
|
||||
});
|
||||
|
||||
it("should prevent deletion of profiles with active certificates", async () => {
|
||||
it("should allow deletion of profiles", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
service.deleteProfile({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
})
|
||||
).rejects.toThrow(ForbiddenRequestError);
|
||||
|
||||
expect(mockCertificateProfileDAL.deleteById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should allow deletion of unused profiles", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(false);
|
||||
(mockCertificateProfileDAL.deleteById as any).mockResolvedValue(sampleProfile);
|
||||
|
||||
const result = await service.deleteProfile({
|
||||
@@ -866,7 +838,6 @@ describe("CertificateProfileService", () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual(sampleProfile);
|
||||
expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123");
|
||||
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ export const certificateProfileServiceFactory = ({
|
||||
const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId);
|
||||
if (existingSlugProfile) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate profile with this slug already exists in project"
|
||||
message: "Certificate profile with this name already exists in project"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export const certificateProfileServiceFactory = ({
|
||||
);
|
||||
if (conflictingProfile && conflictingProfile.id !== profileId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Certificate profile with this slug already exists in project"
|
||||
message: "Certificate profile with this name already exists in project"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -521,14 +521,6 @@ export const certificateProfileServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
// Check if profile is in use by any certificates
|
||||
const isInUse = await certificateProfileDAL.isProfileInUse(profileId);
|
||||
if (isInUse) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Cannot delete certificate profile that has issued certificates"
|
||||
});
|
||||
}
|
||||
|
||||
const deletedProfile = await certificateProfileDAL.deleteById(profileId);
|
||||
if (!deletedProfile) {
|
||||
throw new NotFoundError({ message: "Failed to delete certificate profile" });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@@ -13,9 +14,9 @@ const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
||||
const templateV2SubjectSchema = z
|
||||
.object({
|
||||
type: attributeTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -68,9 +69,9 @@ const templateV2ExtendedKeyUsagesSchema = z
|
||||
const templateV2SanSchema = z
|
||||
.object({
|
||||
type: sanTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -87,22 +88,33 @@ const templateV2SanSchema = z
|
||||
const templateV2ValiditySchema = z.object({
|
||||
max: z
|
||||
.string()
|
||||
.regex(/^\d+[dhmy]$/, {
|
||||
.regex(new RE2("^\\d+[dhmy]$"), {
|
||||
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2AlgorithmsSchema = z.object({
|
||||
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
|
||||
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
|
||||
signature: z
|
||||
.array(z.string().trim().min(1, "Algorithm cannot be empty"))
|
||||
.min(1, "At least one signature algorithm must be provided")
|
||||
.optional(),
|
||||
keyAlgorithm: z
|
||||
.array(z.string().trim().min(1, "Algorithm cannot be empty"))
|
||||
.min(1, "At least one key algorithm must be provided")
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const certificateTemplateV2ResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string().uuid("Project ID must be valid"),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Template name is required")
|
||||
.max(255, "Template name must be less than 255 characters")
|
||||
.regex(new RE2("^[a-zA-Z0-9-_]+$"), "Template name must contain only letters, numbers, hyphens, and underscores"),
|
||||
description: z.string().trim().max(1000, "Description must be less than 1000 characters").nullable().optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
@@ -114,26 +126,53 @@ export const certificateTemplateV2ResponseSchema = z.object({
|
||||
});
|
||||
|
||||
export const certificateRequestSchema = z.object({
|
||||
commonName: z.string().optional(),
|
||||
organization: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
commonName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Common name cannot be empty")
|
||||
.max(64, "Common name must be less than 64 characters")
|
||||
.optional(),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization cannot be empty")
|
||||
.max(64, "Organization must be less than 64 characters")
|
||||
.optional(),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, "Country code must be 2 characters")
|
||||
.max(2, "Country code must be 2 characters")
|
||||
.optional(),
|
||||
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).min(1, "At least one key usage must be provided").optional(),
|
||||
extendedKeyUsages: z
|
||||
.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
.min(1, "At least one extended key usage must be provided")
|
||||
.optional(),
|
||||
subjectAlternativeNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: sanTypeSchema,
|
||||
value: z.string()
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "SAN value cannot be empty")
|
||||
.max(255, "SAN value must be less than 255 characters")
|
||||
})
|
||||
)
|
||||
.min(1, "At least one SAN must be provided")
|
||||
.optional(),
|
||||
validity: z
|
||||
.object({
|
||||
ttl: z.string()
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.regex(new RE2("^\\d+[dhmy]$"), "TTL must be in format like '365d', '12m', '1y', or '24h'")
|
||||
})
|
||||
.optional(),
|
||||
signatureAlgorithm: z.string().optional(),
|
||||
keyAlgorithm: z.string().optional()
|
||||
signatureAlgorithm: z.string().trim().min(1, "Signature algorithm cannot be empty").optional(),
|
||||
keyAlgorithm: z.string().trim().min(1, "Key algorithm cannot be empty").optional()
|
||||
});
|
||||
|
||||
export const validateCertificateRequestSchema = z.object({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||
|
||||
export enum CertStatus {
|
||||
ACTIVE = "active",
|
||||
EXPIRED = "expired",
|
||||
REVOKED = "revoked"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user