From e9f50554814c41b8f9e1a38c2120293d99deffb3 Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Tue, 5 Aug 2025 20:19:17 +0300 Subject: [PATCH 1/2] fixed SAN extension field in certificate issuance --- .../internal-certificate-authority-service.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index dd30cc62e..4d54f79bf 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -1365,7 +1365,7 @@ export const internalCertificateAuthorityServiceFactory = ({ } let altNamesArray: { - type: "email" | "dns"; + type: "email" | "dns" | "url" | "ip"; value: string; }[] = []; @@ -1390,6 +1390,14 @@ export const internalCertificateAuthorityServiceFactory = ({ }; } + if (z.string().url().safeParse(altName).success) { + return { type: "url", value: altName }; + } + + if (z.string().ip().safeParse(altName).success) { + return { type: "ip", value: altName }; + } + // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly throw new Error(`Invalid altName: ${altName}`); }); @@ -1767,7 +1775,7 @@ export const internalCertificateAuthorityServiceFactory = ({ let altNamesFromCsr: string = ""; let altNamesArray: { - type: "email" | "dns"; + type: "email" | "dns" | "url" | "ip"; value: string; }[] = []; if (altNames) { @@ -1791,6 +1799,14 @@ export const internalCertificateAuthorityServiceFactory = ({ }; } + if (z.string().url().safeParse(altName).success) { + return { type: "url", value: altName }; + } + + if (z.string().ip().safeParse(altName).success) { + return { type: "ip", value: altName }; + } + // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly throw new Error(`Invalid altName: ${altName}`); }); From e7a6f46f5666c35985dfb91701804b57701efb12 Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Wed, 6 Aug 2025 00:26:27 +0300 Subject: [PATCH 2/2] refactored SAN validation logic --- .../certificate-authority-validators.ts | 17 ++++ .../internal-certificate-authority-fns.ts | 50 +++------- .../internal-certificate-authority-service.ts | 97 ++++++------------- .../services/certificate/certificate-types.ts | 11 +++ 4 files changed, 70 insertions(+), 105 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 3fa7e8d5e..908741bde 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { isValidIp } from "@app/lib/ip"; import { isFQDN } from "@app/lib/validator/validate-url"; +import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types"; const isValidDate = (dateString: string) => { const date = new Date(dateString); @@ -56,3 +57,19 @@ export const validateAltNamesField = z message: "Each alt name must be a valid hostname, email address, IP address or URL" } ); + +export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => { + if (isFQDN(name, { allow_wildcard: true, require_tld: false })) { + return { type: TAltNameType.DNS, value: name }; + } + if (z.string().url().safeParse(name).success) { + return { type: TAltNameType.URL, value: name }; + } + if (z.string().email().safeParse(name).success) { + return { type: TAltNameType.EMAIL, value: name }; + } + if (isValidIp(name)) { + return { type: TAltNameType.IP, value: name }; + } + return null; +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index eace4e47a..6811476f8 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -1,7 +1,6 @@ /* eslint-disable no-bitwise */ import * as x509 from "@peculiar/x509"; import RE2 from "re2"; -import { z } from "zod"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; @@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; @@ -17,7 +15,8 @@ import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -34,6 +33,7 @@ import { } from "../certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityFnsDeps = { certificateAuthorityDAL: Pick; @@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({ extensions.push(extendedKeyUsagesExtension); } - let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (subscriber.subjectAlternativeNames?.length) { altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) { - return { type: "dns", value: altName }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({ ); } - let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames.split(",").map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) { - return { type: "dns", value: altName }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 4d54f79bf..510a160e9 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; -import { z } from "zod"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -34,7 +32,8 @@ import { CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "../../certificate/certificate-types"; import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; @@ -69,6 +68,7 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -1364,42 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({ ); } - let altNamesArray: { - type: "email" | "dns" | "url" | "ip"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -1774,42 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({ } let altNamesFromCsr: string = ""; - let altNamesArray: { - type: "email" | "dns" | "url" | "ip"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; + if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - if (z.string().url().safeParse(altName).success) { - return { type: "url", value: altName }; - } - - if (z.string().ip().safeParse(altName).success) { - return { type: "ip", value: altName }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); } else { // attempt to read from CSR if altNames is not explicitly provided const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); @@ -1817,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({ const sanNames = new x509.GeneralNames(sanExtension.value); altNamesArray = sanNames.items - .filter((value) => value.type === "email" || value.type === "dns") - .map((name) => ({ - type: name.type as "email" | "dns", - value: name.value - })); + .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 Error(`Invalid altName from CSR: ${name.value}`); + } + return altNameType; + }); altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); } diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index f1c79a36f..527df2a39 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = { projectDAL: Pick; kmsService: Pick; }; + +export enum TAltNameType { + EMAIL = "email", + DNS = "dns", + IP = "ip", + URL = "url" +} +export type TAltNameMapping = { + type: TAltNameType; + value: string; +};