mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: subject alternative name policy enforcement
This commit is contained in:
@@ -12,6 +12,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
tb.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
tb.string("name").notNullable();
|
||||
tb.string("commonName").notNullable();
|
||||
tb.string("subjectAlternativeName").notNullable();
|
||||
tb.string("ttl").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ export const CertificateTemplatesSchema = z.object({
|
||||
caId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
subjectAlternativeName: z.string(),
|
||||
ttl: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
|
||||
@@ -12,6 +12,7 @@ const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||
caId: true,
|
||||
name: true,
|
||||
commonName: true,
|
||||
subjectAlternativeName: true,
|
||||
ttl: true
|
||||
});
|
||||
|
||||
@@ -62,6 +63,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
caId: z.string(),
|
||||
name: z.string().min(1),
|
||||
commonName: validateTemplateRegexField,
|
||||
subjectAlternativeName: validateTemplateRegexField,
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
}),
|
||||
response: {
|
||||
@@ -95,6 +97,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
caId: z.string().optional(),
|
||||
name: z.string().min(1).optional(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
subjectAlternativeName: validateTemplateRegexField.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
|
||||
@@ -1096,17 +1096,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName,
|
||||
notBeforeDate,
|
||||
notAfterDate
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
@@ -1136,11 +1125,13 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@@ -1168,6 +1159,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName,
|
||||
notBeforeDate,
|
||||
notAfterDate,
|
||||
altNames: altNamesArray.map((entry) => entry.value)
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
@@ -1345,17 +1348,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||
});
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName: cn,
|
||||
notBeforeDate,
|
||||
notAfterDate
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
@@ -1371,11 +1363,12 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@@ -1403,6 +1396,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName: cn,
|
||||
notBeforeDate,
|
||||
notAfterDate,
|
||||
altNames: altNamesArray.map((entry) => entry.value)
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ms from "ms";
|
||||
|
||||
import { TCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
export const validateCertificateDetailsAgainstTemplate = (
|
||||
@@ -7,11 +8,9 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
commonName: string;
|
||||
notBeforeDate: Date;
|
||||
notAfterDate: Date;
|
||||
altNames: string[];
|
||||
},
|
||||
template: {
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
}
|
||||
template: TCertificateTemplates
|
||||
) => {
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
@@ -25,4 +24,13 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
message: "Invalid validity date based on template policy"
|
||||
});
|
||||
}
|
||||
|
||||
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
|
||||
cert.altNames.forEach((altName) => {
|
||||
if (!subjectAlternativeNameRegex.test(altName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid subject alternative name based on template policy"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -59,6 +60,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
@@ -70,6 +72,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -108,6 +111,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
const updatedCertTemplate = await certificateTemplateDAL.updateById(certTemplate.id, {
|
||||
caId,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ export type TCreateCertTemplateDTO = {
|
||||
caId: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -12,6 +13,7 @@ export type TUpdateCertTemplateDTO = {
|
||||
caId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type TCertificateTemplate = {
|
||||
caId: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
@@ -17,6 +18,7 @@ export type TCreateCertificateTemplateDTO = {
|
||||
caId: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
projectId: string;
|
||||
};
|
||||
@@ -26,6 +28,7 @@ export type TUpdateCertificateTemplateDTO = {
|
||||
caId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
@@ -110,6 +110,12 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}, [cert]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cert && selectedCertTemplate) {
|
||||
setValue("ttl", selectedCertTemplate.ttl);
|
||||
}
|
||||
}, [selectedCertTemplate, cert]);
|
||||
|
||||
const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
@@ -24,18 +24,21 @@ import {
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const validateTemplateRegexField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
name: z.string().min(1),
|
||||
commonName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
}),
|
||||
commonName: validateTemplateRegexField,
|
||||
subjectAlternativeName: validateTemplateRegexField,
|
||||
ttl: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
@@ -78,6 +81,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
caId: certTemplate.caId,
|
||||
name: certTemplate.name,
|
||||
commonName: certTemplate.commonName,
|
||||
subjectAlternativeName: certTemplate.subjectAlternativeName,
|
||||
ttl: certTemplate.ttl
|
||||
});
|
||||
} else {
|
||||
@@ -90,7 +94,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
}
|
||||
}, [certTemplate]);
|
||||
|
||||
const onFormSubmit = async ({ caId, name, commonName, ttl }: FormData) => {
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
}: FormData) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -103,6 +113,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
@@ -116,6 +127,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
@@ -199,7 +211,22 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="service.acme.com" />
|
||||
<Input {...field} placeholder=".*\.acme.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="subjectAlternativeName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Alternative Names (SANs)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="service\.acme.\..*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -208,7 +235,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) =>
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL"
|
||||
label="Max TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
|
||||
Reference in New Issue
Block a user