Compare commits

..

5 Commits

Author SHA1 Message Date
Scott Wilson
fd89b3c702 fix: correctly parse audit log error message 2025-08-06 15:42:27 -07:00
x032205
59cffe8cfb Merge pull request #4313 from JuliusMieliauskas/fix-san-extension-contents
FIX: SAN extension field in certificate issuance
2025-08-05 21:26:43 -04:00
Maidul Islam
fa61867a72 Merge pull request #4316 from Infisical/docs/update-self-hostable-ips
Update prerequisites sections for secret syncs/rotations to include being able to accept requests…
2025-08-05 17:45:17 -07:00
Julius Mieliauskas
e7a6f46f56 refactored SAN validation logic 2025-08-06 00:26:27 +03:00
Julius Mieliauskas
e9f5055481 fixed SAN extension field in certificate issuance 2025-08-05 20:19:17 +03:00
5 changed files with 72 additions and 97 deletions

View File

@@ -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;
};

View File

@@ -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<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
@@ -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);

View File

@@ -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,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
);
}
let altNamesArray: {
type: "email" | "dns";
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 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);
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
}
let altNamesFromCsr: string = "";
let altNamesArray: {
type: "email" | "dns";
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 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");
@@ -1801,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(",");
}

View File

@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export enum TAltNameType {
EMAIL = "email",
DNS = "dns",
IP = "ip",
URL = "url"
}
export type TAltNameMapping = {
type: TAltNameType;
value: string;
};

View File

@@ -1,8 +1,7 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { onRequestError } from "@app/hooks/api/reactQuery";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
@@ -46,12 +45,7 @@ export const useGetAuditLogs = (
);
return data.auditLogs;
} catch (error) {
if (error instanceof AxiosError) {
createNotification({
type: "error",
text: error.response?.data.message
});
}
onRequestError(error);
return [];
}
},