mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 17:27:16 +00:00
Add cert support for alt names
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
|
const hasAltNamesColumn = await knex.schema.hasColumn(TableName.Certificate, "altNames");
|
||||||
|
if (!hasAltNamesColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("altNames").defaultTo("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Certificate, "altNames")) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.dropColumn("altNames");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ export const CertificatesSchema = z.object({
|
|||||||
notBefore: z.date(),
|
notBefore: z.date(),
|
||||||
notAfter: z.date(),
|
notAfter: z.date(),
|
||||||
revokedAt: z.date().nullable().optional(),
|
revokedAt: z.date().nullable().optional(),
|
||||||
revocationReason: z.number().nullable().optional()
|
revocationReason: z.number().nullable().optional(),
|
||||||
|
altNames: z.string().default("").nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export const ProjectsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
version: z.number().default(1),
|
version: z.number().default(1),
|
||||||
upgradeStatus: z.string().nullable().optional(),
|
upgradeStatus: z.string().nullable().optional(),
|
||||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
pitVersionLimit: z.number().default(10),
|
||||||
pitVersionLimit: z.number().default(10)
|
kmsCertificateKeyId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -804,6 +804,8 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
caId: "The ID of the CA to issue the certificate from",
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
friendlyName: "A friendly name for the certificate",
|
friendlyName: "A friendly name for the certificate",
|
||||||
commonName: "The common name (CN) for the certificate",
|
commonName: "The common name (CN) for the certificate",
|
||||||
|
altNames:
|
||||||
|
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
|
||||||
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
||||||
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
import {
|
||||||
|
validateAltNamesField,
|
||||||
|
validateCaDateField
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
|
|
||||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -452,6 +455,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
.object({
|
.object({
|
||||||
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||||
|
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||||
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")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import crypto, { KeyObject } from "crypto";
|
import crypto, { KeyObject } from "crypto";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -38,6 +39,7 @@ import {
|
|||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./certificate-authority-types";
|
} from "./certificate-authority-types";
|
||||||
|
import { hostnameRegex } from "./certificate-authority-validators";
|
||||||
|
|
||||||
type TCertificateAuthorityServiceFactoryDep = {
|
type TCertificateAuthorityServiceFactoryDep = {
|
||||||
certificateAuthorityDAL: Pick<
|
certificateAuthorityDAL: Pick<
|
||||||
@@ -653,6 +655,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
caId,
|
caId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
|
altNames,
|
||||||
ttl,
|
ttl,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
@@ -738,6 +741,46 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (altNames) {
|
||||||
|
const altNamesArray: {
|
||||||
|
type: "email" | "dns";
|
||||||
|
value: string;
|
||||||
|
}[] = 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the altName is a valid hostname
|
||||||
|
if (hostnameRegex.test(altName)) {
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("the altNamesArray: ", altNamesArray);
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -748,12 +791,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
signingKey: caPrivateKey,
|
signingKey: caPrivateKey,
|
||||||
publicKey: csrObj.publicKey,
|
publicKey: csrObj.publicKey,
|
||||||
signingAlgorithm: alg,
|
signingAlgorithm: alg,
|
||||||
extensions: [
|
extensions
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
|
||||||
new x509.BasicConstraintsExtension(false),
|
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
@@ -771,6 +809,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
status: CertStatus.ACTIVE,
|
status: CertStatus.ACTIVE,
|
||||||
friendlyName: friendlyName || commonName,
|
friendlyName: friendlyName || commonName,
|
||||||
commonName,
|
commonName,
|
||||||
|
altNames,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
notBefore: notBeforeDate,
|
notBefore: notBeforeDate,
|
||||||
notAfter: notAfterDate
|
notAfter: notAfterDate
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export type TIssueCertFromCaDTO = {
|
|||||||
caId: string;
|
caId: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
altNames: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
|||||||
@@ -6,3 +6,29 @@ const isValidDate = (dateString: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||||
|
|
||||||
|
export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||||
|
export const validateAltNamesField = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("")
|
||||||
|
.transform((data) => {
|
||||||
|
if (data === "") return "";
|
||||||
|
// Trim each alt name and join with ', ' to ensure formatting
|
||||||
|
return data
|
||||||
|
.split(",")
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.join(", ");
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data === "") return true;
|
||||||
|
// Split and validate each alt name
|
||||||
|
return data.split(", ").every((name) => {
|
||||||
|
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Each alt name must be a valid hostname or email address"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
|
|
||||||
- Issuing CA: The CA under which to issue the certificate.
|
- Issuing CA: The CA under which to issue the certificate.
|
||||||
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
|
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
|
||||||
- Common Name (CN): The (common) name of the certificate.
|
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
|
||||||
|
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.
|
||||||
- TTL: The lifetime of the certificate in seconds.
|
- TTL: The lifetime of the certificate in seconds.
|
||||||
- Valid Until: The date until which the certificate is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`.
|
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Copying the certificate details">
|
<Step title="Copying the certificate details">
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export type TCreateCertificateDTO = {
|
|||||||
caId: string;
|
caId: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
altNames: string; // sans
|
||||||
ttl: string; // string compatible with ms
|
ttl: string; // string compatible with ms
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type TCertificate = {
|
|||||||
status: CertStatus;
|
status: CertStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
altNames: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
notBefore: string;
|
notBefore: string;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
|
|||||||
+23
-1
@@ -24,6 +24,7 @@ const schema = z.object({
|
|||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
commonName: z.string().trim().min(1),
|
commonName: z.string().trim().min(1),
|
||||||
|
altNames: z.string(),
|
||||||
ttl: z.string().trim()
|
ttl: z.string().trim()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
caId: cert.caId,
|
caId: cert.caId,
|
||||||
friendlyName: cert.friendlyName,
|
friendlyName: cert.friendlyName,
|
||||||
commonName: cert.commonName,
|
commonName: cert.commonName,
|
||||||
|
altNames: cert.altNames,
|
||||||
ttl: ""
|
ttl: ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -78,12 +80,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
caId: "",
|
caId: "",
|
||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
commonName: "",
|
commonName: "",
|
||||||
|
altNames: "",
|
||||||
ttl: ""
|
ttl: ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [cert]);
|
}, [cert]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ caId, friendlyName, commonName, ttl }: FormData) => {
|
const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
@@ -92,6 +95,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
caId,
|
caId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
|
altNames,
|
||||||
ttl
|
ttl
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +196,24 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="altNames"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Alternative Names (SANs)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="app1.acme.com, app2.acme.com, ..."
|
||||||
|
isDisabled={Boolean(cert)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="ttl"
|
name="ttl"
|
||||||
|
|||||||
Reference in New Issue
Block a user