Minor improvements on self signed certificates

This commit is contained in:
Carlos Monastyrski
2025-11-24 11:20:21 -03:00
parent c0060c1d22
commit d69a2db80c
10 changed files with 491 additions and 237 deletions
@@ -739,11 +739,7 @@ export const pkiAcmeServiceFactory = ({
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
}
if (!profile.caId) {
throw new NotFoundError({ message: "Self-signed certificates are not supported for ACME enrollment" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId!);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
@@ -51,58 +51,100 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
return !!data.estConfig;
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.ACME) {
if (!data.acmeConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
}
if (data.issuerType === IssuerType.CA) {
if (!data.caId) {
return false;
}
}
if (data.issuerType === IssuerType.SELF_SIGNED) {
if (data.caId) {
return false;
}
if (data.enrollmentType !== EnrollmentType.API) {
return false;
}
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration. CA issuer type requires a CA ID. Self-signed issuer type cannot have a CA ID and only supports API enrollment."
message: "EST enrollment type requires EST configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.caId;
}
return true;
},
{
message: "CA issuer type requires a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.caId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
),
response: {
@@ -7,11 +7,7 @@ import { EnrollmentType, IssuerType } from "./certificate-profile-types";
export const createCertificateProfileSchema = z
.object({
projectId: z.string().uuid("Project ID must be valid"),
caId: z
.union([z.string().uuid(), z.literal("")])
.optional()
.nullable()
.transform((val) => (val === "" ? null : val)),
caId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
@@ -38,60 +34,101 @@ export const createCertificateProfileSchema = z
})
.refine(
(data) => {
// Validate enrollment type configurations
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
return !!data.estConfig;
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.ACME) {
if (!data.acmeConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
}
if (data.issuerType === IssuerType.CA) {
if (!data.caId) {
return false;
}
}
if (data.issuerType === IssuerType.SELF_SIGNED) {
if (data.caId) {
return false;
}
if (data.enrollmentType !== EnrollmentType.API) {
return false;
}
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration. CA issuer type requires a CA ID. Self-signed issuer type cannot have a CA ID and only supports API enrollment."
message: "EST enrollment type requires EST configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.caId;
}
return true;
},
{
message: "CA issuer type requires a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.caId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -123,27 +160,34 @@ export const updateCertificateProfileSchema = z
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
return !data.apiConfig;
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
}
if (data.issuerType === IssuerType.SELF_SIGNED) {
if (data.enrollmentType && data.enrollmentType !== EnrollmentType.API) {
return false;
}
}
return true;
},
{
message:
"Cannot have EST config with API enrollment type or API config with EST enrollment type. Self-signed issuer type only supports API enrollment."
message: "EST enrollment type cannot have API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.enrollmentType || data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
}
);
@@ -407,11 +407,16 @@ const generateSelfSignedCertificate = async ({
const signatureAlgorithmConfig = signatureAlgorithmToAlgCfg(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
const notBeforeDate = certificateRequest.notBefore ? new Date(certificateRequest.notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
let notAfterDate: Date;
if (certificateRequest.notAfter) {
notAfterDate = new Date(certificateRequest.notAfter);
} else if (certificateRequest.validity.ttl) {
notAfterDate = new Date(new Date().getTime() + ms(certificateRequest.validity.ttl));
} else {
throw new BadRequestError({
message: "Either notAfter date or TTL must be provided for certificate validity"
});
}
const serialNumber = createSerialNumber();
@@ -458,10 +463,22 @@ const generateSelfSignedCertificate = async ({
...(subjectAlternativeNames
? [
new x509.SubjectAlternativeNameExtension(
certificateRequest.altNames?.map((san) => ({
type: san.type === CertSubjectAlternativeNameType.DNS_NAME ? "dns" : "ip",
value: san.value
})) || [],
certificateRequest.altNames?.map((san) => {
switch (san.type) {
case CertSubjectAlternativeNameType.DNS_NAME:
return { type: "dns" as const, value: san.value };
case CertSubjectAlternativeNameType.IP_ADDRESS:
return { type: "ip" as const, value: san.value };
case CertSubjectAlternativeNameType.EMAIL:
return { type: "email" as const, value: san.value };
case CertSubjectAlternativeNameType.URI:
return { type: "url" as const, value: san.value };
default:
throw new BadRequestError({
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
});
}
}) || [],
false
)
]
@@ -545,7 +562,7 @@ const createSelfSignedCertificateRecord = async ({
(selfSignedResult.certificateSubject.common_name as string) ||
certificateRequest.commonName ||
originalCert?.commonName ||
(isRenewal ? "Renewed Self-signed Certificate" : "Self-signed Certificate");
"";
const altNamesList = selfSignedResult.subjectAlternativeNames.map((san) => san.value).join(",");
@@ -726,8 +743,8 @@ const processSelfSignedCertificate = async ({
await createEncryptedCertificateData({
certificateId: certificateData.id,
certificate: Buffer.from(selfSignedResult.certificate),
privateKey: Buffer.from(selfSignedResult.privateKey),
certificate: selfSignedResult.certificate,
privateKey: selfSignedResult.privateKey,
projectId,
certificateBodyDAL,
certificateSecretDAL,
@@ -1100,10 +1117,25 @@ export const certificateV3ServiceFactory = ({
commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
value: san.value
})),
subjectAlternativeNames: certificateOrder.altNames.map((san) => {
let certType: CertSubjectAlternativeNameType;
switch (san.type) {
case "dns":
certType = CertSubjectAlternativeNameType.DNS_NAME;
break;
case "ip":
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
break;
default:
throw new BadRequestError({
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
});
}
return {
type: certType,
value: san.value
};
}),
validity: certificateOrder.validity,
notBefore: certificateOrder.notBefore,
notAfter: certificateOrder.notAfter,
@@ -1216,7 +1248,8 @@ export const certificateV3ServiceFactory = ({
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
message:
"Certificate is not eligible for renewal: Only certificates issued from an API enrollment profile can be renewed through this endpoint"
});
}
}