mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 14:34:09 +00:00
Merge pull request #2704 from Infisical/feat/add-support-for-no-bootstrap-cert-est
feat: add support for EST device enrollment without bootstrap certs
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||
TableName.CertificateTemplateEstConfig,
|
||||
"disableBootstrapCertValidation"
|
||||
);
|
||||
|
||||
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||
if (!hasDisableBootstrapCertValidationCol) {
|
||||
t.boolean("disableBootstrapCertValidation").defaultTo(false).notNullable();
|
||||
}
|
||||
|
||||
if (hasCaChainCol) {
|
||||
t.binary("encryptedCaChain").nullable().alter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
|
||||
TableName.CertificateTemplateEstConfig,
|
||||
"disableBootstrapCertValidation"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
|
||||
if (hasDisableBootstrapCertValidationCol) {
|
||||
t.dropColumn("disableBootstrapCertValidation");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
encryptedCaChain: zodBuffer,
|
||||
encryptedCaChain: zodBuffer.nullable().optional(),
|
||||
hashedPassphrase: z.string(),
|
||||
isEnabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
disableBootstrapCertValidation: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||
|
||||
@@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
if (!estConfig.disableBootstrapCertValidation) {
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
|
||||
@@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
disableBootstrapCertValidation: true
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true),
|
||||
disableBootstrapCertValidation: z.boolean().default(false)
|
||||
})
|
||||
.refine(
|
||||
({ caChain, disableBootstrapCertValidation }) =>
|
||||
disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain),
|
||||
"CA chain is required"
|
||||
),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
@@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1).optional(),
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
disableBootstrapCertValidation: z.boolean().optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
|
||||
@@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
disableBootstrapCertValidation
|
||||
}: TCreateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
@@ -266,39 +267,45 @@ export const certificateTemplateServiceFactory = ({
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
let encryptedCaChain: Buffer | undefined;
|
||||
if (caChain) {
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
encryptedCaChain = cipherTextBlob;
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||
certificateTemplateId,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain,
|
||||
isEnabled
|
||||
isEnabled,
|
||||
disableBootstrapCertValidation
|
||||
});
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
@@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
disableBootstrapCertValidation
|
||||
}: TUpdateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
@@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
|
||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||
isEnabled
|
||||
isEnabled,
|
||||
disableBootstrapCertValidation
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
@@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaChain = await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
});
|
||||
let decryptedCaChain = "";
|
||||
if (estConfig.encryptedCaChain) {
|
||||
decryptedCaChain = (
|
||||
await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
})
|
||||
).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
certificateTemplateId,
|
||||
id: estConfig.id,
|
||||
isEnabled: estConfig.isEnabled,
|
||||
caChain: decryptedCaChain.toString(),
|
||||
caChain: decryptedCaChain,
|
||||
hashedPassphrase: estConfig.hashedPassphrase,
|
||||
projectId: certTemplate.projectId,
|
||||
orgId: certTemplate.orgId
|
||||
orgId: certTemplate.orgId,
|
||||
disableBootstrapCertValidation: estConfig.disableBootstrapCertValidation
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
|
||||
|
||||
export type TCreateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
caChain?: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
disableBootstrapCertValidation: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateEstConfigurationDTO = {
|
||||
@@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
disableBootstrapCertValidation?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetEstConfigurationDTO =
|
||||
|
||||
Reference in New Issue
Block a user