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