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:
Sheen
2024-11-12 02:40:37 +08:00
committed by GitHub
10 changed files with 186 additions and 82 deletions
@@ -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({ export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
certificateTemplateId: z.string().uuid(), certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer, encryptedCaChain: zodBuffer.nullable().optional(),
hashedPassphrase: z.string(), hashedPassphrase: z.string(),
isEnabled: z.boolean(), isEnabled: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
disableBootstrapCertValidation: z.boolean().default(false)
}); });
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>; export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
@@ -171,6 +171,7 @@ export const certificateEstServiceFactory = ({
}); });
} }
if (!estConfig.disableBootstrapCertValidation) {
const caCerts = estConfig.caChain const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => { ?.map((cert) => {
@@ -193,6 +194,7 @@ export const certificateEstServiceFactory = ({
if (!(await isCertChainValid([certObj, ...caCerts]))) { if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" }); throw new BadRequestError({ message: "Invalid certificate chain" });
} }
}
const { certificate } = await certificateAuthorityService.signCertFromCa({ const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true, isInternal: true,
@@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({ const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
id: true, id: true,
certificateTemplateId: true, certificateTemplateId: true,
isEnabled: true isEnabled: true,
disableBootstrapCertValidation: true
}); });
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => { export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
@@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
params: z.object({ params: z.object({
certificateTemplateId: z.string().trim() certificateTemplateId: z.string().trim()
}), }),
body: z.object({ body: z
caChain: z.string().trim().min(1), .object({
caChain: z.string().trim().optional(),
passphrase: z.string().min(1), passphrase: z.string().min(1),
isEnabled: z.boolean().default(true) isEnabled: z.boolean().default(true),
}), disableBootstrapCertValidation: z.boolean().default(false)
})
.refine(
({ caChain, disableBootstrapCertValidation }) =>
disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain),
"CA chain is required"
),
response: { response: {
200: sanitizedEstConfig 200: sanitizedEstConfig
} }
@@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
certificateTemplateId: z.string().trim() certificateTemplateId: z.string().trim()
}), }),
body: z.object({ body: z.object({
caChain: z.string().trim().min(1).optional(), caChain: z.string().trim().optional(),
passphrase: z.string().min(1).optional(), passphrase: z.string().min(1).optional(),
disableBootstrapCertValidation: z.boolean().optional(),
isEnabled: z.boolean().optional() isEnabled: z.boolean().optional()
}), }),
response: { response: {
@@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId actorOrgId,
disableBootstrapCertValidation
}: TCreateEstConfigurationDTO) => { }: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) { if (!plan.pkiEst) {
@@ -266,6 +267,8 @@ export const certificateTemplateServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
let encryptedCaChain: Buffer | undefined;
if (caChain) {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId, projectId: certTemplate.projectId,
projectDAL, projectDAL,
@@ -289,16 +292,20 @@ export const certificateTemplateServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({ const { cipherTextBlob } = await kmsEncryptor({
plainText: Buffer.from(caChain) plainText: Buffer.from(caChain)
}); });
encryptedCaChain = cipherTextBlob;
}
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS); const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
const estConfig = await certificateTemplateEstConfigDAL.create({ const estConfig = await certificateTemplateEstConfigDAL.create({
certificateTemplateId, certificateTemplateId,
hashedPassphrase, hashedPassphrase,
encryptedCaChain, encryptedCaChain,
isEnabled isEnabled,
disableBootstrapCertValidation
}); });
return { ...estConfig, projectId: certTemplate.projectId }; return { ...estConfig, projectId: certTemplate.projectId };
@@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId actorOrgId,
disableBootstrapCertValidation
}: TUpdateEstConfigurationDTO) => { }: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) { if (!plan.pkiEst) {
@@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
}); });
const updatedData: TCertificateTemplateEstConfigsUpdate = { const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled isEnabled,
disableBootstrapCertValidation
}; };
if (caChain) { if (caChain) {
@@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const decryptedCaChain = await kmsDecryptor({ let decryptedCaChain = "";
if (estConfig.encryptedCaChain) {
decryptedCaChain = (
await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain cipherTextBlob: estConfig.encryptedCaChain
}); })
).toString();
}
return { return {
certificateTemplateId, certificateTemplateId,
id: estConfig.id, id: estConfig.id,
isEnabled: estConfig.isEnabled, isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(), caChain: decryptedCaChain,
hashedPassphrase: estConfig.hashedPassphrase, hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId, projectId: certTemplate.projectId,
orgId: certTemplate.orgId orgId: certTemplate.orgId,
disableBootstrapCertValidation: estConfig.disableBootstrapCertValidation
}; };
}; };
@@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
export type TCreateEstConfigurationDTO = { export type TCreateEstConfigurationDTO = {
certificateTemplateId: string; certificateTemplateId: string;
caChain: string; caChain?: string;
passphrase: string; passphrase: string;
isEnabled: boolean; isEnabled: boolean;
disableBootstrapCertValidation: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = { export type TUpdateEstConfigurationDTO = {
@@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
caChain?: string; caChain?: string;
passphrase?: string; passphrase?: string;
isEnabled?: boolean; isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO = export type TGetEstConfigurationDTO =
+1
View File
@@ -35,6 +35,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png) ![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Disable Bootstrap Certificate Validation** - Enable this if your devices are not configured with a bootstrap certificate.
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server. - **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password. - **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 507 KiB

@@ -46,9 +46,10 @@ export type TDeleteCertificateTemplateDTO = {
export type TCreateEstConfigDTO = { export type TCreateEstConfigDTO = {
certificateTemplateId: string; certificateTemplateId: string;
caChain: string; caChain?: string;
passphrase: string; passphrase: string;
isEnabled: boolean; isEnabled: boolean;
disableBootstrapCertValidation: boolean;
}; };
export type TUpdateEstConfigDTO = { export type TUpdateEstConfigDTO = {
@@ -56,11 +57,13 @@ export type TUpdateEstConfigDTO = {
caChain?: string; caChain?: string;
passphrase?: string; passphrase?: string;
isEnabled?: boolean; isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
}; };
export type TEstConfig = { export type TEstConfig = {
id: string; id: string;
certificateTemplateId: string; certificateTemplateId: string;
caChain: string; caChain: string;
isEnabled: false; isEnabled: boolean;
disableBootstrapCertValidation: boolean;
}; };
@@ -33,9 +33,10 @@ type Props = {
const schema = z.object({ const schema = z.object({
method: z.nativeEnum(EnrollmentMethod), method: z.nativeEnum(EnrollmentMethod),
caChain: z.string(), caChain: z.string().optional(),
passphrase: z.string().optional(), passphrase: z.string().optional(),
isEnabled: z.boolean() isEnabled: z.boolean(),
disableBootstrapCertValidation: z.boolean().optional().default(false)
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -53,6 +54,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
handleSubmit, handleSubmit,
reset, reset,
setError, setError,
watch,
setValue,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema) resolver: zodResolver(schema)
@@ -62,16 +65,26 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
const { mutateAsync: updateEstConfig } = useUpdateEstConfig(); const { mutateAsync: updateEstConfig } = useUpdateEstConfig();
const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false); const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false);
const disableBootstrapCertValidation = watch("disableBootstrapCertValidation");
useEffect(() => {
if (disableBootstrapCertValidation) {
setValue("caChain", "");
}
}, [disableBootstrapCertValidation]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
reset({ reset({
caChain: data.caChain, caChain: data.caChain,
isEnabled: data.isEnabled isEnabled: data.isEnabled,
disableBootstrapCertValidation: data.disableBootstrapCertValidation
}); });
} else { } else {
reset({ reset({
caChain: "", caChain: "",
isEnabled: false isEnabled: false,
disableBootstrapCertValidation: false
}); });
} }
}, [data]); }, [data]);
@@ -83,7 +96,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
certificateTemplateId, certificateTemplateId,
caChain, caChain,
passphrase, passphrase,
isEnabled isEnabled,
disableBootstrapCertValidation
}); });
} else { } else {
if (!passphrase) { if (!passphrase) {
@@ -95,7 +109,8 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
certificateTemplateId, certificateTemplateId,
caChain, caChain,
passphrase, passphrase,
isEnabled isEnabled,
disableBootstrapCertValidation
}); });
} }
@@ -150,24 +165,45 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
<Input value={data.certificateTemplateId} isDisabled className="bg-white/[0.07]" /> <Input value={data.certificateTemplateId} isDisabled className="bg-white/[0.07]" />
</FormControl> </FormControl>
)} )}
<Controller
control={control}
name="disableBootstrapCertValidation"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="skip-bootstrap-cert-validation"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="ml-1 w-full">Disable Bootstrap Certificate Validation</p>
</Switch>
</FormControl>
);
}}
/>
{!disableBootstrapCertValidation && (
<Controller <Controller
control={control} control={control}
name="caChain" name="caChain"
disabled={disableBootstrapCertValidation}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Certificate Authority Chain" label="Certificate Authority Chain"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
isRequired isRequired={!disableBootstrapCertValidation}
> >
<TextArea <TextArea
{...field} {...field}
isDisabled={disableBootstrapCertValidation}
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400" className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
reSize="none" reSize="none"
/> />
</FormControl> </FormControl>
)} )}
/> />
)}
<Controller <Controller
control={control} control={control}
name="passphrase" name="passphrase"