mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 14:27:23 +00:00
Addressed PR comments
This commit is contained in:
@@ -7,7 +7,13 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { ACMESANType, CertificateOrderStatus } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAttributeType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
|
||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||
@@ -76,8 +82,8 @@ describe("CertificateV3Service", () => {
|
||||
describe("issueCertificateFromProfile", () => {
|
||||
const mockCertificateRequest = {
|
||||
commonName: "test.example.com",
|
||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
||||
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
|
||||
validity: { ttl: "30d" },
|
||||
signatureAlgorithm: "RSA-SHA256",
|
||||
keyAlgorithm: "RSA_2048"
|
||||
@@ -102,12 +108,19 @@ describe("CertificateV3Service", () => {
|
||||
id: "template-123",
|
||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||
attributes: []
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockCertificateResult = {
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
};
|
||||
@@ -136,6 +149,8 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("certificate");
|
||||
expect(result).toHaveProperty("issuingCaCertificate");
|
||||
expect(result).toHaveProperty("certificateChain");
|
||||
expect(result).toHaveProperty("privateKey");
|
||||
expect(result).toHaveProperty("serialNumber", "123456");
|
||||
expect(result).toHaveProperty("certificateId", "cert-123");
|
||||
@@ -160,12 +175,19 @@ describe("CertificateV3Service", () => {
|
||||
id: "template-123",
|
||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||
attributes: []
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockCertificateResult = {
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
};
|
||||
@@ -178,17 +200,17 @@ describe("CertificateV3Service", () => {
|
||||
const camelCaseRequest = {
|
||||
commonName: "test.example.com",
|
||||
keyUsages: [
|
||||
CertKeyUsage.DIGITAL_SIGNATURE,
|
||||
CertKeyUsage.NON_REPUDIATION,
|
||||
CertKeyUsage.KEY_AGREEMENT,
|
||||
CertKeyUsage.CRL_SIGN,
|
||||
CertKeyUsage.DECIPHER_ONLY
|
||||
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||
CertKeyUsageType.NON_REPUDIATION,
|
||||
CertKeyUsageType.KEY_AGREEMENT,
|
||||
CertKeyUsageType.CRL_SIGN,
|
||||
CertKeyUsageType.DECIPHER_ONLY
|
||||
],
|
||||
extendedKeyUsages: [
|
||||
CertExtendedKeyUsage.CLIENT_AUTH,
|
||||
CertExtendedKeyUsage.CODE_SIGNING,
|
||||
CertExtendedKeyUsage.OCSP_SIGNING,
|
||||
CertExtendedKeyUsage.SERVER_AUTH
|
||||
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||
CertExtendedKeyUsageType.CODE_SIGNING,
|
||||
CertExtendedKeyUsageType.OCSP_SIGNING,
|
||||
CertExtendedKeyUsageType.SERVER_AUTH
|
||||
],
|
||||
validity: { ttl: "10d" }
|
||||
};
|
||||
@@ -215,8 +237,19 @@ describe("CertificateV3Service", () => {
|
||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||
"template-123",
|
||||
expect.objectContaining({
|
||||
keyUsages: ["digital_signature", "non_repudiation", "key_agreement", "crl_sign", "decipher_only"],
|
||||
extendedKeyUsages: ["client_auth", "code_signing", "ocsp_signing", "server_auth"]
|
||||
keyUsages: [
|
||||
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||
CertKeyUsageType.NON_REPUDIATION,
|
||||
CertKeyUsageType.KEY_AGREEMENT,
|
||||
CertKeyUsageType.CRL_SIGN,
|
||||
CertKeyUsageType.DECIPHER_ONLY
|
||||
],
|
||||
extendedKeyUsages: [
|
||||
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||
CertExtendedKeyUsageType.CODE_SIGNING,
|
||||
CertExtendedKeyUsageType.OCSP_SIGNING,
|
||||
CertExtendedKeyUsageType.SERVER_AUTH
|
||||
]
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -286,6 +319,7 @@ describe("CertificateV3Service", () => {
|
||||
const mockSignResult = {
|
||||
certificate: Buffer.from("signed-cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||
serialNumber: "789012"
|
||||
};
|
||||
|
||||
@@ -308,6 +342,8 @@ describe("CertificateV3Service", () => {
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("certificate");
|
||||
expect(result).toHaveProperty("issuingCaCertificate");
|
||||
expect(result).toHaveProperty("certificateChain");
|
||||
expect(result).toHaveProperty("serialNumber", "789012");
|
||||
expect(result).toHaveProperty("certificateId", "cert-456");
|
||||
expect(result).not.toHaveProperty("privateKey");
|
||||
@@ -347,11 +383,11 @@ describe("CertificateV3Service", () => {
|
||||
|
||||
describe("orderCertificateFromProfile", () => {
|
||||
const mockCertificateOrder = {
|
||||
subjectAlternativeNames: [{ type: "dns" as const, value: "example.com" }],
|
||||
altNames: [{ type: ACMESANType.DNS, value: "example.com" }],
|
||||
validity: { ttl: "30d" },
|
||||
commonName: "example.com",
|
||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
||||
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
|
||||
signatureAlgorithm: "RSA-SHA256",
|
||||
keyAlgorithm: "RSA_2048"
|
||||
};
|
||||
@@ -375,12 +411,19 @@ describe("CertificateV3Service", () => {
|
||||
id: "template-123",
|
||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||
attributes: []
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const mockCertificateResult = {
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
};
|
||||
@@ -413,9 +456,9 @@ describe("CertificateV3Service", () => {
|
||||
expect(result).toHaveProperty("certificate");
|
||||
expect(result.subjectAlternativeNames).toHaveLength(1);
|
||||
expect(result.subjectAlternativeNames[0]).toEqual({
|
||||
type: "dns",
|
||||
type: ACMESANType.DNS,
|
||||
value: "example.com",
|
||||
status: "valid"
|
||||
status: CertificateOrderStatus.VALID
|
||||
});
|
||||
});
|
||||
|
||||
@@ -448,4 +491,230 @@ describe("CertificateV3Service", () => {
|
||||
).rejects.toThrow("Profile is not configured for api enrollment");
|
||||
});
|
||||
});
|
||||
|
||||
describe("algorithm compatibility (integration tests)", () => {
|
||||
const mockProfile = {
|
||||
id: "profile-1",
|
||||
slug: "test-profile",
|
||||
projectId: "project-1",
|
||||
caId: "ca-1",
|
||||
certificateTemplateId: "template-1",
|
||||
enrollmentType: EnrollmentType.API
|
||||
};
|
||||
|
||||
const mockCertificateRequest = {
|
||||
commonName: "test.example.com",
|
||||
validity: { ttl: "30d" },
|
||||
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should successfully process RSA algorithms with RSA CAs", async () => {
|
||||
const rsaCa = {
|
||||
id: "ca-1",
|
||||
projectId: "project-1",
|
||||
status: "active",
|
||||
internalCa: {
|
||||
keyAlgorithm: "RSA_2048"
|
||||
}
|
||||
};
|
||||
|
||||
const rsaTemplate = {
|
||||
id: "template-1",
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: ["SHA256-RSA", "SHA384-RSA"]
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsaCa);
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate);
|
||||
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
});
|
||||
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||
|
||||
// Should not throw - RSA CA is compatible with RSA signature algorithms
|
||||
await expect(
|
||||
service.issueCertificateFromProfile({
|
||||
profileId: mockProfile.id,
|
||||
certificateRequest: {
|
||||
...mockCertificateRequest,
|
||||
signatureAlgorithm: "RSA-SHA256"
|
||||
},
|
||||
...mockActor
|
||||
})
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("should successfully process ECDSA algorithms with EC CAs", async () => {
|
||||
const ecCa = {
|
||||
id: "ca-1",
|
||||
projectId: "project-1",
|
||||
status: "active",
|
||||
internalCa: {
|
||||
keyAlgorithm: "EC_prime256v1"
|
||||
}
|
||||
};
|
||||
|
||||
const ecdsaTemplate = {
|
||||
id: "template-1",
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: ["SHA256-ECDSA", "SHA384-ECDSA"]
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(ecCa);
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate);
|
||||
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
});
|
||||
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||
|
||||
// Should not throw - EC CA is compatible with ECDSA signature algorithms
|
||||
await expect(
|
||||
service.issueCertificateFromProfile({
|
||||
profileId: mockProfile.id,
|
||||
certificateRequest: {
|
||||
...mockCertificateRequest,
|
||||
signatureAlgorithm: "ECDSA-SHA256"
|
||||
},
|
||||
...mockActor
|
||||
})
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("should dynamically support new RSA key sizes", async () => {
|
||||
const rsa8192Ca = {
|
||||
id: "ca-1",
|
||||
projectId: "project-1",
|
||||
status: "active",
|
||||
internalCa: {
|
||||
keyAlgorithm: "RSA_8192" // Future RSA key size
|
||||
}
|
||||
};
|
||||
|
||||
const rsaTemplate = {
|
||||
id: "template-1",
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: ["SHA256-RSA"]
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsa8192Ca);
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate);
|
||||
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
});
|
||||
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||
|
||||
// Should not throw - dynamic check supports new RSA key sizes
|
||||
await expect(
|
||||
service.issueCertificateFromProfile({
|
||||
profileId: mockProfile.id,
|
||||
certificateRequest: {
|
||||
...mockCertificateRequest,
|
||||
signatureAlgorithm: "RSA-SHA256"
|
||||
},
|
||||
...mockActor
|
||||
})
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("should dynamically support new EC curve types", async () => {
|
||||
const newEcCa = {
|
||||
id: "ca-1",
|
||||
projectId: "project-1",
|
||||
status: "active",
|
||||
internalCa: {
|
||||
keyAlgorithm: "EC_secp521r1" // Future EC curve
|
||||
}
|
||||
};
|
||||
|
||||
const ecdsaTemplate = {
|
||||
id: "template-1",
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: ["SHA384-ECDSA"]
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertIncludeType.OPTIONAL,
|
||||
value: ["example.com"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(newEcCa);
|
||||
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate);
|
||||
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||
certificate: Buffer.from("cert"),
|
||||
certificateChain: Buffer.from("chain"),
|
||||
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||
privateKey: Buffer.from("key"),
|
||||
serialNumber: "123456"
|
||||
});
|
||||
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||
|
||||
// Should not throw - dynamic check supports new EC curves
|
||||
await expect(
|
||||
service.issueCertificateFromProfile({
|
||||
profileId: mockProfile.id,
|
||||
certificateRequest: {
|
||||
...mockCertificateRequest,
|
||||
signatureAlgorithm: "ECDSA-SHA384"
|
||||
},
|
||||
...mockActor
|
||||
})
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { CertificateOrderStatus } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
TCertificateAuthorityDALFactory,
|
||||
TCertificateAuthorityWithAssociatedCa
|
||||
@@ -20,10 +21,13 @@ import { TCertificateProfileDALFactory } from "@app/services/certificate-profile
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||
|
||||
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
||||
import {
|
||||
bufferToString,
|
||||
buildCertificateSubjectFromTemplate,
|
||||
buildSubjectAlternativeNamesFromTemplate,
|
||||
convertExtendedKeyUsageArrayToLegacy,
|
||||
convertKeyUsageArrayToLegacy,
|
||||
mapEnumsForValidation,
|
||||
normalizeDateForApi
|
||||
} from "../certificate-common/certificate-utils";
|
||||
@@ -95,6 +99,45 @@ const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation:
|
||||
return caType;
|
||||
};
|
||||
|
||||
const validateAlgorithmCompatibility = (
|
||||
ca: TCertificateAuthorityWithAssociatedCa,
|
||||
template: {
|
||||
signatureAlgorithm?: {
|
||||
allowedAlgorithms?: string[];
|
||||
};
|
||||
}
|
||||
) => {
|
||||
if (!template.signatureAlgorithm || !template.signatureAlgorithm.allowedAlgorithms) {
|
||||
return;
|
||||
}
|
||||
|
||||
const caKeyAlgorithm = ca.internalCa?.keyAlgorithm;
|
||||
if (!caKeyAlgorithm) {
|
||||
throw new BadRequestError({ message: "CA key algorithm not found" });
|
||||
}
|
||||
|
||||
const compatibleAlgorithms = template.signatureAlgorithm.allowedAlgorithms.filter((sigAlg: string) => {
|
||||
const parts = sigAlg.split("-");
|
||||
const keyType = parts[parts.length - 1];
|
||||
|
||||
if (caKeyAlgorithm.startsWith("RSA")) {
|
||||
return keyType === "RSA";
|
||||
}
|
||||
|
||||
if (caKeyAlgorithm.startsWith("EC")) {
|
||||
return keyType === "ECDSA";
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (compatibleAlgorithms.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: `Template signature algorithms (${template.signatureAlgorithm.allowedAlgorithms.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | string): string => {
|
||||
if (typeof certData === "string") return certData;
|
||||
if (Buffer.isBuffer(certData)) return bufferToString(certData);
|
||||
@@ -131,6 +174,12 @@ export const certificateV3ServiceFactory = ({
|
||||
EnrollmentType.API
|
||||
);
|
||||
|
||||
if (certificateRequest.commonName && Array.isArray(certificateRequest.commonName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Common Name must be a single value, not an array"
|
||||
});
|
||||
}
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
@@ -166,33 +215,48 @@ export const certificateV3ServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
}
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm =
|
||||
certificateRequest.signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||
|
||||
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||
});
|
||||
}
|
||||
|
||||
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||
});
|
||||
}
|
||||
|
||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.attributes);
|
||||
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||
certificateRequest,
|
||||
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||
template.subjectAlternativeNames
|
||||
);
|
||||
|
||||
const { certificate, certificateChain, privateKey, serialNumber } = await internalCaService.issueCertFromCa({
|
||||
caId: ca.id,
|
||||
friendlyName: certificateSubject.common_name || "Certificate",
|
||||
commonName: certificateSubject.common_name || "",
|
||||
altNames: subjectAlternativeNames,
|
||||
ttl: certificateRequest.validity.ttl,
|
||||
keyUsages: certificateRequest.keyUsages,
|
||||
extendedKeyUsages: certificateRequest.extendedKeyUsages,
|
||||
notBefore: normalizeDateForApi(certificateRequest.notBefore),
|
||||
notAfter: normalizeDateForApi(certificateRequest.notAfter),
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
await internalCaService.issueCertFromCa({
|
||||
caId: ca.id,
|
||||
friendlyName: certificateSubject.common_name || "Certificate",
|
||||
commonName: certificateSubject.common_name || "",
|
||||
altNames: subjectAlternativeNames,
|
||||
ttl: certificateRequest.validity.ttl,
|
||||
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
|
||||
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
|
||||
notBefore: normalizeDateForApi(certificateRequest.notBefore),
|
||||
notAfter: normalizeDateForApi(certificateRequest.notAfter),
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||
keyAlgorithm: effectiveKeyAlgorithm,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||
if (!cert) {
|
||||
@@ -201,14 +265,15 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
await certificateDAL.updateById(cert.id, { profileId });
|
||||
|
||||
const certificateChainString = bufferToString(certificateChain);
|
||||
return {
|
||||
certificate: bufferToString(certificate),
|
||||
issuingCaCertificate: certificateChainString.split("\n").pop() || bufferToString(certificate),
|
||||
certificateChain: certificateChainString,
|
||||
issuingCaCertificate: bufferToString(issuingCaCertificate),
|
||||
certificateChain: bufferToString(certificateChain),
|
||||
privateKey: bufferToString(privateKey),
|
||||
serialNumber,
|
||||
certificateId: cert.id
|
||||
certificateId: cert.id,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug
|
||||
};
|
||||
};
|
||||
|
||||
@@ -218,6 +283,8 @@ export const certificateV3ServiceFactory = ({
|
||||
validity,
|
||||
notBefore,
|
||||
notAfter,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -241,16 +308,52 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
validateCaSupport(ca, "CSR signing");
|
||||
|
||||
const { certificate, certificateChain, serialNumber } = await internalCaService.signCertFromCa({
|
||||
isInternal: true,
|
||||
caId: ca.id,
|
||||
csr,
|
||||
ttl: validity.ttl,
|
||||
altNames: "",
|
||||
notBefore: normalizeDateForApi(notBefore),
|
||||
notAfter: normalizeDateForApi(notAfter)
|
||||
if (!actorAuthMethod) {
|
||||
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
|
||||
}
|
||||
|
||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
templateId: profile.certificateTemplateId
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||
}
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm = signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||
const effectiveKeyAlgorithm = keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||
|
||||
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||
});
|
||||
}
|
||||
|
||||
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||
await internalCaService.signCertFromCa({
|
||||
isInternal: true,
|
||||
caId: ca.id,
|
||||
csr,
|
||||
ttl: validity.ttl,
|
||||
altNames: "",
|
||||
notBefore: normalizeDateForApi(notBefore),
|
||||
notAfter: normalizeDateForApi(notAfter),
|
||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||
keyAlgorithm: effectiveKeyAlgorithm
|
||||
});
|
||||
|
||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||
if (!cert) {
|
||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||
@@ -263,10 +366,12 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
return {
|
||||
certificate: certificateString,
|
||||
issuingCaCertificate: certificateChainString.split("\n").pop() || certificateString,
|
||||
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer),
|
||||
certificateChain: certificateChainString,
|
||||
serialNumber,
|
||||
certificateId: cert.id
|
||||
certificateId: cert.id,
|
||||
projectId: profile.projectId,
|
||||
profileName: profile.slug
|
||||
};
|
||||
};
|
||||
|
||||
@@ -293,8 +398,8 @@ export const certificateV3ServiceFactory = ({
|
||||
commonName: certificateOrder.commonName,
|
||||
keyUsages: certificateOrder.keyUsages,
|
||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||
subjectAlternativeNames: certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||
type: san.type === "dns" ? ("dns_name" as const) : ("ip_address" as const),
|
||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
value: san.value
|
||||
})),
|
||||
validity: certificateOrder.validity,
|
||||
@@ -334,43 +439,26 @@ export const certificateV3ServiceFactory = ({
|
||||
});
|
||||
|
||||
const orderId = randomUUID();
|
||||
const subjectAlternativeNames = certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||
type: san.type,
|
||||
value: san.value,
|
||||
status: "valid" as const
|
||||
}));
|
||||
|
||||
const authorizations = certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||
identifier: {
|
||||
type: san.type,
|
||||
value: san.value
|
||||
},
|
||||
status: "valid" as const,
|
||||
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
challenges: [
|
||||
{
|
||||
type: "internal-validation",
|
||||
status: "valid" as const,
|
||||
url: `/api/v3/certificates/orders/${orderId}/internal`,
|
||||
token: "internal-ca-validation"
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
return {
|
||||
orderId,
|
||||
status: "valid",
|
||||
subjectAlternativeNames,
|
||||
authorizations,
|
||||
finalize: `/api/v3/certificates/orders/${orderId}/finalize`,
|
||||
certificate: certificateResult.certificate
|
||||
status: CertificateOrderStatus.VALID,
|
||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||
type: san.type,
|
||||
value: san.value,
|
||||
status: CertificateOrderStatus.VALID
|
||||
})),
|
||||
authorizations: [],
|
||||
finalize: `/api/v3/certificates/orders/${orderId}/completed`,
|
||||
certificate: certificateResult.certificate,
|
||||
projectId: certificateResult.projectId,
|
||||
profileName: certificateResult.profileName
|
||||
};
|
||||
}
|
||||
|
||||
if (caType === CaType.ACME) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"ACME certificate ordering via profiles is not yet implemented. Use direct certificate issuance for ACME CAs."
|
||||
message: "ACME certificate ordering via profiles is not yet implemented."
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
||||
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType
|
||||
} from "../certificate-common/certificate-constants";
|
||||
|
||||
export type TIssueCertificateFromProfileDTO = {
|
||||
profileId: string;
|
||||
certificateRequest: {
|
||||
commonName?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
subjectAlternativeNames?: Array<{
|
||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
||||
keyUsages?: CertKeyUsageType[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||
altNames?: Array<{
|
||||
type: CertSubjectAlternativeNameType;
|
||||
value: string;
|
||||
}>;
|
||||
validity: {
|
||||
@@ -30,21 +35,23 @@ export type TSignCertificateFromProfileDTO = {
|
||||
};
|
||||
notBefore?: Date;
|
||||
notAfter?: Date;
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TOrderCertificateFromProfileDTO = {
|
||||
profileId: string;
|
||||
certificateOrder: {
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
altNames: Array<{
|
||||
type: ACMESANType;
|
||||
value: string;
|
||||
}>;
|
||||
validity: {
|
||||
ttl: string;
|
||||
};
|
||||
commonName?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
keyUsages?: CertKeyUsageType[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||
notBefore?: Date;
|
||||
notAfter?: Date;
|
||||
signatureAlgorithm?: string;
|
||||
@@ -59,30 +66,34 @@ export type TCertificateFromProfileResponse = {
|
||||
privateKey?: string;
|
||||
serialNumber: string;
|
||||
certificateId: string;
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
};
|
||||
|
||||
export type TCertificateOrderResponse = {
|
||||
orderId: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
status: CertificateOrderStatus;
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
type: ACMESANType;
|
||||
value: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
status: CertificateOrderStatus;
|
||||
}>;
|
||||
authorizations: Array<{
|
||||
identifier: {
|
||||
type: "dns" | "ip";
|
||||
type: ACMESANType;
|
||||
value: string;
|
||||
};
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
status: CertificateOrderStatus;
|
||||
expires?: string;
|
||||
challenges: Array<{
|
||||
type: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
status: CertificateOrderStatus;
|
||||
url: string;
|
||||
token: string;
|
||||
}>;
|
||||
}>;
|
||||
finalize: string;
|
||||
certificate?: string;
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user