Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for EST device enrollment without bootstrap certs #2704

Merged
merged 3 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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");
}
});
}
5 changes: 3 additions & 2 deletions backend/src/db/schemas/certificate-template-est-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
44 changes: 23 additions & 21 deletions backend/src/ee/services/certificate-est/certificate-est-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}

const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];

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" });
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" });
}

const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];

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 { certificate } = await certificateAuthorityService.signCertFromCa({
Expand Down
23 changes: 16 additions & 7 deletions backend/src/server/routes/v1/certificate-template-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
Expand Down Expand Up @@ -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" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}

const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});

const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});

encryptedCaChain = cipherTextBlob;
}

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 };
Expand All @@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
Expand Down Expand Up @@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
});

const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled
isEnabled,
disableBootstrapCertValidation
};

if (caChain) {
Expand Down Expand Up @@ -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
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ export type TDeleteCertTemplateDTO = {

export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
caChain?: string;
passphrase: string;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
} & Omit<TProjectPermission, "projectId">;

export type TUpdateEstConfigurationDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
} & Omit<TProjectPermission, "projectId">;

export type TGetEstConfigurationDTO =
Expand Down
1 change: 1 addition & 0 deletions docs/documentation/platform/pki/est.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

- **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.
- **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.

Expand Down
Binary file modified docs/images/platform/pki/est/template-enrollment-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions frontend/src/hooks/api/certificateTemplates/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,24 @@ export type TDeleteCertificateTemplateDTO = {

export type TCreateEstConfigDTO = {
certificateTemplateId: string;
caChain: string;
caChain?: string;
passphrase: string;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
};

export type TUpdateEstConfigDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
};

export type TEstConfig = {
id: string;
certificateTemplateId: string;
caChain: string;
isEnabled: false;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
};
Loading
Loading