mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 20:21:18 +00:00
PKI revamp EST improvements and fixes
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-bitwise */
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { EnrollmentType } from "../certificate-profile/certificate-profile-types";
|
||||
import { certificateEstV3ServiceFactory, TCertificateEstV3ServiceFactory } from "./certificate-est-v3-service";
|
||||
|
||||
// Mock the x509 module
|
||||
vi.mock("@peculiar/x509", () => ({
|
||||
Pkcs10CertificateRequest: vi.fn(),
|
||||
GeneralNames: vi.fn(),
|
||||
KeyUsagesExtension: vi.fn(),
|
||||
ExtendedKeyUsageExtension: vi.fn(),
|
||||
X509Certificate: vi.fn(),
|
||||
KeyUsageFlags: {
|
||||
digitalSignature: 1,
|
||||
nonRepudiation: 2,
|
||||
keyEncipherment: 4,
|
||||
dataEncipherment: 8,
|
||||
keyAgreement: 16,
|
||||
keyCertSign: 32,
|
||||
cRLSign: 64,
|
||||
encipherOnly: 128,
|
||||
decipherOnly: 256
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock other dependencies
|
||||
vi.mock("@app/services/certificate-authority/certificate-authority-fns", () => ({
|
||||
parseDistinguishedName: vi.fn((subject: string) => {
|
||||
const parts = subject.split(",");
|
||||
const result: any = {};
|
||||
parts.forEach((part) => {
|
||||
const [key, value] = part.split("=");
|
||||
switch (key.trim()) {
|
||||
case "CN":
|
||||
result.commonName = value;
|
||||
break;
|
||||
case "O":
|
||||
result.organization = value;
|
||||
break;
|
||||
case "OU":
|
||||
result.ou = value;
|
||||
break;
|
||||
case "L":
|
||||
result.locality = value;
|
||||
break;
|
||||
case "ST":
|
||||
result.province = value;
|
||||
break;
|
||||
case "C":
|
||||
result.country = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/certificate-authority/certificate-authority-validators", () => ({
|
||||
validateAndMapAltNameType: vi.fn((value: string) => {
|
||||
if (value.includes(".") && !value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return { type: "dns", value };
|
||||
}
|
||||
if (value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return { type: "ip", value };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/certificate-common/certificate-constants", () => ({
|
||||
mapLegacyKeyUsageToStandard: vi.fn((usage: string) => {
|
||||
const mapping: Record<string, string> = {
|
||||
digitalSignature: "digital_signature",
|
||||
keyEncipherment: "key_encipherment",
|
||||
keyCertSign: "key_cert_sign"
|
||||
};
|
||||
return mapping[usage] || usage;
|
||||
}),
|
||||
mapLegacyExtendedKeyUsageToStandard: vi.fn((usage: string) => {
|
||||
const mapping: Record<string, string> = {
|
||||
clientAuth: "client_auth",
|
||||
serverAuth: "server_auth",
|
||||
codeSigning: "code_signing"
|
||||
};
|
||||
return mapping[usage] || usage;
|
||||
}),
|
||||
CertKeyUsageType: {
|
||||
DIGITAL_SIGNATURE: "digital_signature",
|
||||
KEY_ENCIPHERMENT: "key_encipherment",
|
||||
KEY_CERT_SIGN: "key_cert_sign"
|
||||
},
|
||||
CertExtendedKeyUsageType: {
|
||||
CLIENT_AUTH: "client_auth",
|
||||
SERVER_AUTH: "server_auth",
|
||||
CODE_SIGNING: "code_signing"
|
||||
},
|
||||
CertSubjectAlternativeNameType: {
|
||||
DNS_NAME: "dns_name",
|
||||
IP_ADDRESS: "ip_address",
|
||||
RFC822_NAME: "rfc822_name",
|
||||
UNIFORM_RESOURCE_IDENTIFIER: "uniform_resource_identifier"
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/certificate/certificate-types", () => ({
|
||||
mapLegacyAltNameType: vi.fn((type: string) => {
|
||||
const mapping: Record<string, string> = {
|
||||
dns: "dns_name",
|
||||
ip: "ip_address",
|
||||
email: "rfc822_name",
|
||||
url: "uniform_resource_identifier"
|
||||
};
|
||||
return mapping[type] || type;
|
||||
}),
|
||||
CertExtendedKeyUsageOIDToName: {
|
||||
"1.3.6.1.5.5.7.3.1": "serverAuth",
|
||||
"1.3.6.1.5.5.7.3.2": "clientAuth",
|
||||
"1.3.6.1.5.5.7.3.3": "codeSigning"
|
||||
},
|
||||
CertKeyUsage: {
|
||||
DIGITAL_SIGNATURE: "digitalSignature",
|
||||
KEY_ENCIPHERMENT: "keyEncipherment",
|
||||
KEY_CERT_SIGN: "keyCertSign",
|
||||
NON_REPUDIATION: "nonRepudiation",
|
||||
DATA_ENCIPHERMENT: "dataEncipherment",
|
||||
KEY_AGREEMENT: "keyAgreement",
|
||||
CRL_SIGN: "cRLSign",
|
||||
ENCIPHER_ONLY: "encipherOnly",
|
||||
DECIPHER_ONLY: "decipherOnly"
|
||||
},
|
||||
CertExtendedKeyUsage: {
|
||||
CLIENT_AUTH: "clientAuth",
|
||||
SERVER_AUTH: "serverAuth",
|
||||
CODE_SIGNING: "codeSigning"
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/certificate-common/certificate-utils", () => ({
|
||||
mapEnumsForValidation: vi.fn((req: any) => req)
|
||||
}));
|
||||
|
||||
vi.mock("../../ee/services/certificate-est/certificate-est-fns", () => ({
|
||||
convertRawCertsToPkcs7: vi.fn(() => "mocked-pkcs7-response")
|
||||
}));
|
||||
|
||||
describe("CertificateEstV3Service Security Fix", () => {
|
||||
let service: TCertificateEstV3ServiceFactory;
|
||||
|
||||
const mockInternalCertificateAuthorityService = {
|
||||
signCertFromCa: vi.fn()
|
||||
};
|
||||
|
||||
const mockCertificateTemplateDAL = {
|
||||
findById: vi.fn()
|
||||
};
|
||||
|
||||
const mockCertificateTemplateV2Service = {
|
||||
validateCertificateRequest: vi.fn()
|
||||
};
|
||||
|
||||
const mockCertificateAuthorityDAL = {
|
||||
findById: vi.fn(),
|
||||
findByIdWithAssociatedCa: vi.fn()
|
||||
};
|
||||
|
||||
const mockCertificateAuthorityCertDAL = {
|
||||
find: vi.fn(),
|
||||
findById: vi.fn()
|
||||
};
|
||||
|
||||
const mockProjectDAL = {
|
||||
findOne: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
transaction: vi.fn()
|
||||
};
|
||||
|
||||
const mockKmsService = {
|
||||
decryptWithKmsKey: vi.fn(),
|
||||
generateKmsKey: vi.fn()
|
||||
};
|
||||
|
||||
const mockLicenseService = {
|
||||
getPlan: vi.fn()
|
||||
};
|
||||
|
||||
const mockCertificateProfileDAL = {
|
||||
findByIdWithConfigs: vi.fn()
|
||||
};
|
||||
|
||||
const mockEstEnrollmentConfigDAL = {
|
||||
findById: vi.fn()
|
||||
};
|
||||
|
||||
const mockProfile = {
|
||||
id: "profile-123",
|
||||
projectId: "project-123",
|
||||
caId: "ca-123",
|
||||
certificateTemplateId: "template-v2-123",
|
||||
enrollmentType: EnrollmentType.EST,
|
||||
estConfigId: "est-config-123"
|
||||
};
|
||||
|
||||
const mockEstConfig = {
|
||||
id: "est-config-123",
|
||||
disableBootstrapCaValidation: true
|
||||
};
|
||||
|
||||
const mockProject = {
|
||||
id: "project-123",
|
||||
orgId: "org-123"
|
||||
};
|
||||
|
||||
const mockPlan = {
|
||||
pkiEst: true
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const { Pkcs10CertificateRequest, GeneralNames } = await import("@peculiar/x509");
|
||||
|
||||
service = certificateEstV3ServiceFactory({
|
||||
internalCertificateAuthorityService: mockInternalCertificateAuthorityService,
|
||||
certificateTemplateDAL: mockCertificateTemplateDAL,
|
||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
|
||||
projectDAL: mockProjectDAL,
|
||||
kmsService: mockKmsService,
|
||||
licenseService: mockLicenseService,
|
||||
certificateProfileDAL: mockCertificateProfileDAL,
|
||||
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL
|
||||
});
|
||||
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||
mockEstEnrollmentConfigDAL.findById.mockResolvedValue(mockEstConfig);
|
||||
mockProjectDAL.findOne.mockResolvedValue(mockProject);
|
||||
mockLicenseService.getPlan.mockResolvedValue(mockPlan);
|
||||
|
||||
// Set up the default CSR parsing behavior
|
||||
(Pkcs10CertificateRequest as any).mockImplementation((csr: string) => {
|
||||
const parsed = JSON.parse(csr);
|
||||
const mockExtensions: any[] = [];
|
||||
|
||||
if (parsed.sans && parsed.sans.length > 0) {
|
||||
mockExtensions.push({ type: "2.5.29.17", value: "mock-san-value" });
|
||||
}
|
||||
|
||||
return {
|
||||
subject: parsed.subject,
|
||||
extensions: mockExtensions,
|
||||
getExtension: vi.fn((oid: string) => {
|
||||
if (oid === "2.5.29.15" && parsed.keyUsages && parsed.keyUsages.length > 0) {
|
||||
// Calculate usages as bitwise OR of key usage flags
|
||||
let usages = 0;
|
||||
parsed.keyUsages.forEach((usage: string) => {
|
||||
switch (usage) {
|
||||
case "digital_signature":
|
||||
usages |= 1; // KeyUsageFlags.digitalSignature
|
||||
break;
|
||||
case "key_encipherment":
|
||||
usages |= 4; // KeyUsageFlags.keyEncipherment
|
||||
break;
|
||||
case "key_cert_sign":
|
||||
usages |= 32; // KeyUsageFlags.keyCertSign
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return { usages };
|
||||
}
|
||||
if (oid === "2.5.29.37" && parsed.extendedKeyUsages && parsed.extendedKeyUsages.length > 0) {
|
||||
const ekuOids = parsed.extendedKeyUsages.map((eku: string) => {
|
||||
switch (eku) {
|
||||
case "client_auth":
|
||||
return "1.3.6.1.5.5.7.3.2";
|
||||
case "server_auth":
|
||||
return "1.3.6.1.5.5.7.3.1";
|
||||
case "code_signing":
|
||||
return "1.3.6.1.5.5.7.3.3";
|
||||
default:
|
||||
return "1.3.6.1.5.5.7.3.1";
|
||||
}
|
||||
});
|
||||
return { usages: ekuOids };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
(GeneralNames as any).mockImplementation(() => ({
|
||||
items: [
|
||||
{ type: "dns", value: "test.example.com" },
|
||||
{ type: "ip", value: "192.168.1.1" }
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const createMockCSR = (
|
||||
options: {
|
||||
subject?: string;
|
||||
keyUsages?: string[];
|
||||
extendedKeyUsages?: string[];
|
||||
sans?: Array<{ type: string; value: string }>;
|
||||
} = {}
|
||||
) => {
|
||||
const {
|
||||
subject = "CN=test.example.com,O=Test Org,C=US",
|
||||
keyUsages = [],
|
||||
extendedKeyUsages = [],
|
||||
sans = []
|
||||
} = options;
|
||||
|
||||
return JSON.stringify({
|
||||
subject,
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
sans
|
||||
});
|
||||
};
|
||||
|
||||
describe("CSR Extraction and Template Validation", () => {
|
||||
it("should extract subject attributes from CSR", async () => {
|
||||
const csr = createMockCSR({
|
||||
subject: "CN=test.example.com,O=Test Organization,OU=IT Department,L=San Francisco,ST=California,C=US"
|
||||
});
|
||||
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
commonName: "test.example.com",
|
||||
organization: "Test Organization",
|
||||
organizationUnit: "IT Department",
|
||||
locality: "San Francisco",
|
||||
state: "California",
|
||||
country: "US"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should extract key usages from CSR", async () => {
|
||||
const csr = createMockCSR({
|
||||
keyUsages: ["digital_signature", "key_encipherment"]
|
||||
});
|
||||
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
keyUsages: expect.arrayContaining(["digital_signature", "key_encipherment"])
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should extract extended key usages from CSR", async () => {
|
||||
const csr = createMockCSR({
|
||||
extendedKeyUsages: ["client_auth", "server_auth"]
|
||||
});
|
||||
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
extendedKeyUsages: expect.arrayContaining(["client_auth", "server_auth"])
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should extract Subject Alternative Names from CSR", async () => {
|
||||
const { GeneralNames } = await import("@peculiar/x509");
|
||||
|
||||
const csr = createMockCSR({
|
||||
sans: [
|
||||
{ type: "dns", value: "test.example.com" },
|
||||
{ type: "ip", value: "192.168.1.1" }
|
||||
]
|
||||
});
|
||||
|
||||
(GeneralNames as any).mockImplementation(() => ({
|
||||
items: [
|
||||
{ type: "dns", value: "test.example.com" },
|
||||
{ type: "ip", value: "192.168.1.1" }
|
||||
]
|
||||
}));
|
||||
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
subjectAlternativeNames: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "dns_name",
|
||||
value: "test.example.com"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "ip_address",
|
||||
value: "192.168.1.1"
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Template Validation Enforcement", () => {
|
||||
const basicCSR = createMockCSR();
|
||||
|
||||
it("should enforce template validation and reject invalid requests", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: false,
|
||||
errors: ["Common name 'test.example.com' is not allowed", "Key usage 'digital_signature' is denied"],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should allow valid requests that pass template validation", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
|
||||
isInternal: true,
|
||||
caId: "ca-123",
|
||||
csr: basicCSR
|
||||
});
|
||||
});
|
||||
|
||||
it("should validate template for both simpleEnrollByProfile and simpleReenrollByProfile", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: false,
|
||||
errors: ["SAN value 'evil.com' is denied"],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalled();
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Policy Bypass Prevention", () => {
|
||||
const maliciousCSR = createMockCSR({
|
||||
subject: "CN=evil.com,O=Evil Corp,C=XX",
|
||||
keyUsages: ["key_cert_sign"],
|
||||
sans: [
|
||||
{ type: "dns", value: "*.example.com" },
|
||||
{ type: "ip", value: "127.0.0.1" }
|
||||
]
|
||||
});
|
||||
|
||||
it("should block attempts to bypass subject attribute policies", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: false,
|
||||
errors: ["Organization 'Evil Corp' is denied", "Country 'XX' is not allowed"],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: maliciousCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
commonName: "evil.com",
|
||||
organization: "Evil Corp",
|
||||
country: "XX"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should block attempts to bypass key usage policies", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: false,
|
||||
errors: ["Key usage 'key_cert_sign' is denied - certificate authority privileges not allowed"],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: maliciousCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-v2-123",
|
||||
expect.objectContaining({
|
||||
keyUsages: expect.arrayContaining(["key_cert_sign"])
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should block attempts to bypass SAN policies", async () => {
|
||||
const { GeneralNames } = await import("@peculiar/x509");
|
||||
|
||||
(GeneralNames as any).mockImplementation(() => ({
|
||||
items: [
|
||||
{ type: "dns", value: "*.example.com" },
|
||||
{ type: "ip", value: "127.0.0.1" }
|
||||
]
|
||||
}));
|
||||
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: false,
|
||||
errors: ["SAN value '*.example.com' matches denied wildcard pattern", "SAN value '127.0.0.1' is denied"],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: maliciousCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
const basicCSR = createMockCSR();
|
||||
|
||||
it("should handle profile not found", async () => {
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "nonexistent",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it("should handle non-EST enrollment type", async () => {
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue({
|
||||
...mockProfile,
|
||||
enrollmentType: EnrollmentType.API
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow(BadRequestError);
|
||||
});
|
||||
|
||||
it("should handle template validation service errors", async () => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockRejectedValue(
|
||||
new Error("Template validation service unavailable")
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
})
|
||||
).rejects.toThrow("Template validation service unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integration with existing flow", () => {
|
||||
const basicCSR = createMockCSR();
|
||||
|
||||
beforeEach(() => {
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
});
|
||||
|
||||
it("should call internal CA service with correct parameters after validation", async () => {
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
|
||||
isInternal: true,
|
||||
caId: "ca-123",
|
||||
csr: basicCSR
|
||||
});
|
||||
});
|
||||
|
||||
it("should use profile's CA ID instead of template ID to avoid v1/v2 mismatch", async () => {
|
||||
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||
certificate: { rawData: new ArrayBuffer(0) }
|
||||
});
|
||||
|
||||
await service.simpleEnrollByProfile({
|
||||
csr: basicCSR,
|
||||
profileId: "profile-123",
|
||||
sslClientCert: ""
|
||||
});
|
||||
|
||||
// Verify it uses caId from profile, not certificateTemplateId
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
caId: "ca-123"
|
||||
})
|
||||
);
|
||||
|
||||
// Verify it does NOT pass certificateTemplateId to avoid v1/v2 confusion
|
||||
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
certificateTemplateId: expect.anything()
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,31 @@ import * as x509 from "@peculiar/x509";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import {
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyUsage,
|
||||
mapLegacyAltNameType,
|
||||
TAltNameMapping
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import {
|
||||
getCaCertChain,
|
||||
getCaCertChains,
|
||||
parseDistinguishedName
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import {
|
||||
mapLegacyExtendedKeyUsageToStandard,
|
||||
mapLegacyKeyUsageToStandard
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
|
||||
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -21,6 +39,7 @@ import { TLicenseServiceFactory } from "../../ee/services/license/license-servic
|
||||
type TCertificateEstV3ServiceFactoryDep = {
|
||||
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
@@ -35,6 +54,7 @@ export type TCertificateEstV3ServiceFactory = ReturnType<typeof certificateEstV3
|
||||
export const certificateEstV3ServiceFactory = ({
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateV2Service,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
@@ -43,6 +63,59 @@ export const certificateEstV3ServiceFactory = ({
|
||||
certificateProfileDAL,
|
||||
estEnrollmentConfigDAL
|
||||
}: TCertificateEstV3ServiceFactoryDep) => {
|
||||
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
const subject = parseDistinguishedName(csrObj.subject);
|
||||
|
||||
const certificateRequest: TCertificateRequest = {
|
||||
commonName: subject.commonName,
|
||||
organization: subject.organization,
|
||||
organizationUnit: subject.ou,
|
||||
locality: subject.locality,
|
||||
state: subject.province,
|
||||
country: subject.country
|
||||
};
|
||||
|
||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||
if (csrKeyUsageExtension) {
|
||||
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||
);
|
||||
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
||||
}
|
||||
|
||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||
if (csrExtendedKeyUsageExtension) {
|
||||
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||
);
|
||||
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
||||
}
|
||||
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
const altNamesArray: TAltNameMapping[] = sanNames.items
|
||||
.filter(
|
||||
(value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
|
||||
)
|
||||
.map((name): TAltNameMapping => {
|
||||
const altNameType = validateAndMapAltNameType(name.value);
|
||||
if (!altNameType) {
|
||||
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
||||
}
|
||||
return altNameType;
|
||||
});
|
||||
|
||||
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
||||
type: mapLegacyAltNameType(altName.type),
|
||||
value: altName.value
|
||||
}));
|
||||
}
|
||||
|
||||
return certificateRequest;
|
||||
};
|
||||
const simpleEnrollByProfile = async ({
|
||||
csr,
|
||||
profileId,
|
||||
@@ -122,9 +195,22 @@ export const certificateEstV3ServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
mappedCertificateRequest
|
||||
);
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
throw new BadRequestError({
|
||||
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId: profile.certificateTemplateId,
|
||||
caId: profile.caId,
|
||||
csr
|
||||
});
|
||||
|
||||
@@ -235,9 +321,22 @@ export const certificateEstV3ServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
mappedCertificateRequest
|
||||
);
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
throw new BadRequestError({
|
||||
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId: profile.certificateTemplateId,
|
||||
caId: profile.caId,
|
||||
csr
|
||||
});
|
||||
|
||||
|
||||
@@ -142,17 +142,19 @@ export const certificateProfileServiceFactory = ({
|
||||
// Hash the passphrase
|
||||
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphraseInput, appCfg.SALT_ROUNDS);
|
||||
|
||||
let encryptedCaChainBuffer: Buffer;
|
||||
try {
|
||||
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
|
||||
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
|
||||
let encryptedCaChainBuffer: Buffer | null = null;
|
||||
if (!data.estConfig.disableBootstrapCaValidation) {
|
||||
try {
|
||||
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
|
||||
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
|
||||
}
|
||||
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
|
||||
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
|
||||
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
||||
}
|
||||
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
|
||||
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
|
||||
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
||||
}
|
||||
|
||||
const estConfig = await estEnrollmentConfigDAL.create(
|
||||
@@ -614,37 +616,49 @@ export const certificateProfileServiceFactory = ({
|
||||
return metrics;
|
||||
};
|
||||
|
||||
const getEstConfigurationByProfile = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
profileId: string;
|
||||
}) => {
|
||||
const getEstConfigurationByProfile = async (
|
||||
params:
|
||||
| {
|
||||
profileId: string;
|
||||
isInternal: true;
|
||||
}
|
||||
| {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
profileId: string;
|
||||
isInternal?: false;
|
||||
}
|
||||
) => {
|
||||
const { profileId, isInternal = false } = params;
|
||||
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
if (!isInternal) {
|
||||
const { actor, actorId, actorAuthMethod, actorOrgId } = params as {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
};
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||
throw new ForbiddenRequestError({
|
||||
|
||||
@@ -27,12 +27,34 @@ type TCertificateTemplateV2ServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
|
||||
|
||||
export const certificateTemplateV2ServiceFactory = ({
|
||||
certificateTemplateV2DAL,
|
||||
permissionService
|
||||
}: TCertificateTemplateV2ServiceFactoryDep) => {
|
||||
const consolidateAttributeArray = <
|
||||
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
|
||||
>(
|
||||
attributes: T[]
|
||||
): T[] => {
|
||||
const consolidated = new Map<string, T>();
|
||||
|
||||
attributes.forEach((attr) => {
|
||||
const existing = consolidated.get(attr.type);
|
||||
if (existing) {
|
||||
consolidated.set(attr.type, {
|
||||
...attr,
|
||||
allowed: [...new Set([...(existing.allowed || []), ...(attr.allowed || [])])],
|
||||
required: [...new Set([...(existing.required || []), ...(attr.required || [])])],
|
||||
denied: [...new Set([...(existing.denied || []), ...(attr.denied || [])])]
|
||||
} as T);
|
||||
} else {
|
||||
consolidated.set(attr.type, attr);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(consolidated.values());
|
||||
};
|
||||
|
||||
const parseTTL = (ttl: string): number => {
|
||||
const regex = new RE2("^(\\d+)([dmyh])$");
|
||||
const match = regex.exec(ttl);
|
||||
@@ -298,44 +320,14 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
|
||||
|
||||
if (subjectPolicies && subjectPolicies.length > 0) {
|
||||
// Validate each template subject attribute policy
|
||||
for (const attrPolicy of subjectPolicies) {
|
||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
||||
|
||||
// Check denied values first
|
||||
if (requestValue && attrPolicy.denied && attrPolicy.denied.length > 0) {
|
||||
const validation = validateValueAgainstConstraints(requestValue, attrPolicy.denied, attrPolicy.type);
|
||||
if (validation.isValid) {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is denied by template policy`);
|
||||
// Skip further validation for this attribute if it's denied
|
||||
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||
// Check allowed values if present and not denied
|
||||
const allowedValidation = validateValueAgainstConstraints(
|
||||
requestValue,
|
||||
attrPolicy.allowed,
|
||||
attrPolicy.type
|
||||
);
|
||||
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||
errors.push(allowedValidation.error);
|
||||
}
|
||||
}
|
||||
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||
// Check allowed values if present and not denied
|
||||
const allowedValidation = validateValueAgainstConstraints(requestValue, attrPolicy.allowed, attrPolicy.type);
|
||||
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||
errors.push(allowedValidation.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for required subject attributes
|
||||
for (const attrPolicy of subjectPolicies) {
|
||||
if (attrPolicy.required && attrPolicy.required.length > 0) {
|
||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
||||
if (!requestValue) {
|
||||
errors.push(`Missing required ${attrPolicy.type} attribute`);
|
||||
} else {
|
||||
// Validate that the request value matches at least one required pattern
|
||||
// Validate that the request value matches the required pattern
|
||||
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
|
||||
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
||||
return validation.isValid;
|
||||
@@ -347,6 +339,38 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestValue) {
|
||||
let isValueDenied = false;
|
||||
if (attrPolicy.denied && attrPolicy.denied.length > 0) {
|
||||
const validation = validateValueAgainstConstraints(requestValue, attrPolicy.denied, attrPolicy.type);
|
||||
if (validation.isValid) {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is denied by template policy`);
|
||||
isValueDenied = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValueDenied && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||
let satisfiesRequired = false;
|
||||
if (attrPolicy.required && attrPolicy.required.length > 0) {
|
||||
satisfiesRequired = attrPolicy.required.some((requiredValue) => {
|
||||
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
||||
return validation.isValid;
|
||||
});
|
||||
}
|
||||
|
||||
if (!satisfiesRequired) {
|
||||
const allowedValidation = validateValueAgainstConstraints(
|
||||
requestValue,
|
||||
attrPolicy.allowed,
|
||||
attrPolicy.type
|
||||
);
|
||||
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||
errors.push(allowedValidation.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any request attributes are not covered by template policies
|
||||
@@ -409,9 +433,19 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
|
||||
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
|
||||
for (const sanValue of requestSans) {
|
||||
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
|
||||
if (!validation.isValid && validation.error) {
|
||||
errors.push(validation.error);
|
||||
let satisfiesRequired = false;
|
||||
if (sanPolicy.required && sanPolicy.required.length > 0) {
|
||||
satisfiesRequired = sanPolicy.required.some((requiredValue) => {
|
||||
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
|
||||
return validation.isValid;
|
||||
});
|
||||
}
|
||||
|
||||
if (!satisfiesRequired) {
|
||||
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
|
||||
if (!validation.isValid && validation.error) {
|
||||
errors.push(validation.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,20 +649,26 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
throw new Error("Template data is required");
|
||||
}
|
||||
|
||||
if (data.subject) {
|
||||
validateSubjectAttributePolicy(data.subject);
|
||||
const consolidatedData = {
|
||||
...data,
|
||||
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
|
||||
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
|
||||
};
|
||||
|
||||
if (consolidatedData.subject) {
|
||||
validateSubjectAttributePolicy(consolidatedData.subject);
|
||||
}
|
||||
|
||||
if (data.sans) {
|
||||
validateSanPolicy(data.sans);
|
||||
if (consolidatedData.sans) {
|
||||
validateSanPolicy(consolidatedData.sans);
|
||||
}
|
||||
|
||||
if (data.keyUsages) {
|
||||
validateKeyUsagePolicy(data.keyUsages);
|
||||
if (consolidatedData.keyUsages) {
|
||||
validateKeyUsagePolicy(consolidatedData.keyUsages);
|
||||
}
|
||||
|
||||
if (data.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
||||
if (consolidatedData.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
|
||||
}
|
||||
|
||||
// Generate slug from name and ensure it's unique within project
|
||||
@@ -640,7 +680,7 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
||||
|
||||
const template = await certificateTemplateV2DAL.create({
|
||||
...data,
|
||||
...consolidatedData,
|
||||
name: uniqueSlug,
|
||||
projectId
|
||||
});
|
||||
@@ -682,23 +722,29 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
if (data.subject) {
|
||||
validateSubjectAttributePolicy(data.subject);
|
||||
const consolidatedData = {
|
||||
...data,
|
||||
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
|
||||
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
|
||||
};
|
||||
|
||||
if (consolidatedData.subject) {
|
||||
validateSubjectAttributePolicy(consolidatedData.subject);
|
||||
}
|
||||
|
||||
if (data.sans) {
|
||||
validateSanPolicy(data.sans);
|
||||
if (consolidatedData.sans) {
|
||||
validateSanPolicy(consolidatedData.sans);
|
||||
}
|
||||
|
||||
if (data.keyUsages) {
|
||||
validateKeyUsagePolicy(data.keyUsages);
|
||||
if (consolidatedData.keyUsages) {
|
||||
validateKeyUsagePolicy(consolidatedData.keyUsages);
|
||||
}
|
||||
|
||||
if (data.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
||||
if (consolidatedData.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
|
||||
}
|
||||
|
||||
const updateData = { ...data };
|
||||
const updateData = { ...consolidatedData };
|
||||
if (data.name && typeof data.name === "string") {
|
||||
const newSlug = generateTemplateSlug(data.name);
|
||||
if (newSlug !== existingTemplate.name) {
|
||||
@@ -871,7 +917,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
||||
if (isInUse) {
|
||||
const profilesUsingTemplate = await certificateTemplateV2DAL.getProfilesUsingTemplate(templateId);
|
||||
const profileNames = profilesUsingTemplate.map((profile) => profile.slug || profile.id).join(", ");
|
||||
const profileNames = profilesUsingTemplate
|
||||
.map((profile: { slug?: string; id: string }) => profile.slug || profile.id)
|
||||
.join(", ");
|
||||
|
||||
throw new ForbiddenRequestError({
|
||||
message:
|
||||
@@ -910,3 +958,5 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
validateCertificateRequest
|
||||
};
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
|
||||
|
||||
@@ -192,6 +192,26 @@ export const certificateV3ServiceFactory = ({
|
||||
...certificateRequest,
|
||||
subjectAlternativeNames: certificateRequest.altNames
|
||||
});
|
||||
|
||||
let template;
|
||||
try {
|
||||
template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: `Certificate profile is using a legacy template (${profile.certificateTemplateId}) that doesn't support security validation policies. Please migrate to a template v2 for proper security enforcement.`
|
||||
});
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
}
|
||||
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
mappedCertificateRequest
|
||||
@@ -214,18 +234,6 @@ export const certificateV3ServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
|
||||
}
|
||||
|
||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
}
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||
|
||||
@@ -20,7 +20,7 @@ export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate;
|
||||
export interface TEstConfigData {
|
||||
disableBootstrapCaValidation: boolean;
|
||||
passphraseInput: string;
|
||||
encryptedCaChain: string;
|
||||
encryptedCaChain?: string;
|
||||
}
|
||||
|
||||
export interface TApiConfigData {
|
||||
|
||||
Reference in New Issue
Block a user