Minor fix on keys

This commit is contained in:
Carlos Monastyrski
2025-10-10 03:14:24 -03:00
parent 15a6b8da04
commit ef9a1961f1
5 changed files with 144 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ import {
validateAndMapAltNameType,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
export const registerCertificatesRouter = async (server: FastifyZodProvider) => {
@@ -49,44 +50,48 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const rawCertificateRequest = {
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
subjectAlternativeNames: req.body.altNames
? req.body.altNames
.split(", ")
.map((name) => name.trim())
.map((name) => {
const mappedType = validateAndMapAltNameType(name);
if (!mappedType) return null;
const typeMapping = {
dns: "dns_name",
ip: "ip_address",
email: "email",
url: "uri"
} as const;
return {
type: typeMapping[mappedType.type] as "dns_name" | "ip_address" | "email" | "uri",
value: mappedType.value
};
})
.filter((item): item is NonNullable<typeof item> => item !== null)
: undefined,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
};
const mappedCertificateRequest = mapEnumsForValidation(rawCertificateRequest);
const data = await server.services.certificateV3.issueCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateRequest: {
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
subjectAlternativeNames: req.body.altNames
? req.body.altNames
.split(", ")
.map((name) => name.trim())
.map((name) => {
const mappedType = validateAndMapAltNameType(name);
if (!mappedType) return null;
const typeMapping = {
dns: "dns_name",
ip: "ip_address",
email: "email",
url: "uri"
} as const;
return {
type: typeMapping[mappedType.type] as "dns_name" | "ip_address" | "email" | "uri",
value: mappedType.value
};
})
.filter((item): item is NonNullable<typeof item> => item !== null)
: undefined,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
}
certificateRequest: mappedCertificateRequest
});
const profile = await server.services.certificateProfile.getProfileById({

View File

@@ -1,7 +1,6 @@
interface CertificateRequestInput {
keyUsages?: string[];
extendedKeyUsages?: string[];
[key: string]: unknown;
}
export const mapEnumsForValidation = <T extends CertificateRequestInput>(request: T): T => {
@@ -12,7 +11,7 @@ export const mapEnumsForValidation = <T extends CertificateRequestInput>(request
dataEncipherment: "data_encipherment",
keyAgreement: "key_agreement",
keyCertSign: "key_cert_sign",
crlSign: "crl_sign",
cRLSign: "crl_sign",
encipherOnly: "encipher_only",
decipherOnly: "decipher_only"
};

View File

@@ -608,6 +608,31 @@ describe("CertificateTemplateV2Service", () => {
expect(result.isValid).toBe(true);
});
it("should handle camelCase key usage mapping correctly", async () => {
const templateWithOptionalUsages = {
...sampleTemplate,
keyUsages: {
requiredUsages: { all: ["digital_signature", "non_repudiation", "key_agreement"] },
optionalUsages: { all: ["crl_sign", "decipher_only"] }
},
extendedKeyUsages: {
requiredUsages: { all: ["client_auth", "code_signing"] },
optionalUsages: { all: ["server_auth", "ocsp_signing"] }
}
};
mockCertificateTemplateV2DAL.findById.mockResolvedValue(templateWithOptionalUsages);
const requestWithCamelCaseUsages = {
...validRequest,
keyUsages: ["digital_signature", "non_repudiation", "key_agreement", "crl_sign", "decipher_only"],
extendedKeyUsages: ["client_auth", "code_signing"]
};
const result = await service.validateCertificateRequest("template-123", requestWithCamelCaseUsages);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it("should validate wildcard patterns in optional attributes", async () => {
const wildcardTemplate = {
...sampleTemplate,

View File

@@ -120,6 +120,7 @@ export const certificateTemplateV2ServiceFactory = ({
request: TCertificateRequest
): TTemplateValidationResult => {
const errors: string[] = [];
const warnings: string[] = [];
template.attributes?.forEach((attrPolicy) => {

View File

@@ -141,6 +141,86 @@ describe("CertificateV3Service", () => {
expect(result).toHaveProperty("certificateId", "cert-123");
});
it("should correctly map camelCase key usages to snake_case before validation", async () => {
const profileId = "profile-123";
const mockProfile = {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
caId: "ca-123",
certificateTemplateId: "template-123"
};
const mockCA = {
id: "ca-123",
externalCa: null
};
const mockTemplate = {
id: "template-123",
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
keyAlgorithm: { defaultKeyType: "RSA_2048" },
attributes: []
};
const mockCertificateResult = {
certificate: Buffer.from("cert"),
certificateChain: Buffer.from("chain"),
privateKey: Buffer.from("key"),
serialNumber: "123456"
};
const mockCertRecord = {
id: "cert-123",
serialNumber: "123456"
};
const camelCaseRequest = {
commonName: "test.example.com",
keyUsages: [
CertKeyUsage.DIGITAL_SIGNATURE,
CertKeyUsage.NON_REPUDIATION,
CertKeyUsage.KEY_AGREEMENT,
CertKeyUsage.CRL_SIGN,
CertKeyUsage.DECIPHER_ONLY
],
extendedKeyUsages: [
CertExtendedKeyUsage.CLIENT_AUTH,
CertExtendedKeyUsage.CODE_SIGNING,
CertExtendedKeyUsage.OCSP_SIGNING,
CertExtendedKeyUsage.SERVER_AUTH
],
validity: { ttl: "10d" }
};
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(mockCA);
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(mockTemplate);
mockInternalCaService.issueCertFromCa.mockResolvedValue(mockCertificateResult);
mockCertificateDAL.findOne.mockResolvedValue(mockCertRecord);
mockCertificateDAL.updateById.mockResolvedValue({});
await service.issueCertificateFromProfile({
profileId,
certificateRequest: camelCaseRequest,
...mockActor
});
// Verify that the template validation service was called with mapped snake_case values
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"]
})
);
});
it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
const profileId = "profile-123";
const mockProfile = {