feat: finished up client cert generation

This commit is contained in:
Sheen Capadngan
2025-02-07 01:24:32 +08:00
parent 0269f57768
commit a3acfa65a2
21 changed files with 593 additions and 14 deletions
+8
View File
@@ -143,6 +143,9 @@ import {
TInternalKms,
TInternalKmsInsert,
TInternalKmsUpdate,
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate,
TKmipClients,
TKmipClientsInsert,
TKmipClientsUpdate,
@@ -922,5 +925,10 @@ declare module "knex/types/tables" {
TKmipInstanceServerCertificatesInsert,
TKmipInstanceServerCertificatesUpdate
>;
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
}
}
@@ -63,6 +63,19 @@ export async function up(knex: Knex): Promise<void> {
t.binary("encryptedChain").notNullable();
});
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (!hasKmipClientCertTable) {
await knex.schema.createTable(TableName.KmipClientCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("kmipClientId").notNullable();
t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE");
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -81,4 +94,9 @@ export async function down(knex: Knex): Promise<void> {
if (hasKmipInstanceServerCertTable) {
await knex.schema.dropTable(TableName.KmipInstanceServerCertificates);
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (hasKmipClientCertTable) {
await knex.schema.dropTable(TableName.KmipClientCertificates);
}
}
+1
View File
@@ -45,6 +45,7 @@ export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
export * from "./internal-kms";
export * from "./kmip-client-certificates";
export * from "./kmip-clients";
export * from "./kmip-instance-configs";
export * from "./kmip-instance-server-certificates";
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const KmipClientCertificatesSchema = z.object({
id: z.string().uuid(),
kmipClientId: z.string().uuid(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date()
});
export type TKmipClientCertificates = z.infer<typeof KmipClientCertificatesSchema>;
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
export type TKmipClientCertificatesUpdate = Partial<
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
>;
+2 -1
View File
@@ -135,7 +135,8 @@ export enum TableName {
SecretSync = "secret_syncs",
KmipClient = "kmip_clients",
KmipInstanceConfig = "kmip_instance_configs",
KmipInstanceServerCertificates = "kmip_instance_server_certificates"
KmipInstanceServerCertificates = "kmip_instance_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
+55
View File
@@ -1,3 +1,4 @@
import ms from "ms";
import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas";
@@ -8,6 +9,7 @@ import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
const KmipClientResponseSchema = KmipClientsSchema.pick({
projectId: true,
@@ -230,4 +232,57 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => {
return { kmipClients, totalCount };
}
});
server.route({
method: "POST",
url: "/clients/:id/certificates",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
serialNumber: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificate = await server.services.kmip.createKmipClientCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
clientId: req.params.id,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: certificate.projectId,
event: {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE,
metadata: {
clientId: req.params.id,
serialNumber: certificate.serialNumber,
ttl: req.body.ttl,
keyAlgorithm: req.body.keyAlgorithm
}
}
});
return certificate;
}
});
};
@@ -258,7 +258,8 @@ export enum EventType {
UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client",
GET_KMIP_CLIENT = "get-kmip-client",
GET_KMIP_CLIENTS = "get-kmip-clients"
GET_KMIP_CLIENTS = "get-kmip-clients",
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate"
}
interface UserActorMetadata {
@@ -2112,6 +2113,16 @@ interface GetKmipClientsEvent {
};
}
interface CreateKmipClientCertificateEvent {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE;
metadata: {
clientId: string;
ttl: string;
keyAlgorithm: string;
serialNumber: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -2307,4 +2318,5 @@ export type Event =
| UpdateKmipClientEvent
| DeleteKmipClientEvent
| GetKmipClientEvent
| GetKmipClientsEvent;
| GetKmipClientsEvent
| CreateKmipClientCertificateEvent;
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipClientCertificateDALFactory = ReturnType<typeof kmipClientCertificateDALFactory>;
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
return {
...kmipClientCertOrm
};
};
@@ -0,0 +1 @@
export const INSTANCE_KMIP_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
+183 -2
View File
@@ -1,11 +1,25 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
createSerialNumber,
keyAlgorithmToAlgCfg
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
import { TKmipClientDALFactory } from "./kmip-client-dal";
import { INSTANCE_KMIP_CONFIG_ID } from "./kmip-constants";
import { TKmipInstanceConfigDALFactory } from "./kmip-instance-config-dal";
import {
TCreateKmipClientCertificateDTO,
TCreateKmipClientDTO,
TDeleteKmipClientDTO,
TGetKmipClientDTO,
@@ -15,12 +29,21 @@ import {
type TKmipServiceFactoryDep = {
kmipClientDAL: TKmipClientDALFactory;
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "decryptWithRootKey">;
kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory;
};
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipServiceFactoryDep) => {
export const kmipServiceFactory = ({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipInstanceConfigDAL,
kmsService
}: TKmipServiceFactoryDep) => {
const createKmipClient = async ({
actor,
actorId,
@@ -67,6 +90,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
}: TUpdateKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -93,6 +122,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -115,6 +150,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -151,5 +192,145 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe
return kmipClientDAL.findByProjectId({ projectId, ...rest });
};
return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient, listKmipClientsByProjectId };
const createKmipClientCertificate = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
ttl,
keyAlgorithm,
clientId
}: TCreateKmipClientCertificateDTO) => {
const kmipClient = await kmipClientDAL.findById(clientId);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${clientId} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(INSTANCE_KMIP_CONFIG_ID);
if (!kmipInstanceConfig) {
throw new InternalServerError({
message: "KMIP has not been configured for the instance."
});
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const caCertObj = new x509.X509Certificate(
decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate)
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
];
const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm);
const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaChain).toString(
"utf-8"
);
const caSkObj = crypto.createPrivateKey({
key: decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaPrivateKey),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `OU=${kmipClient.projectId},CN=${clientId}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
await kmipClientCertificateDAL.create({
kmipClientId: clientId,
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber
});
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain,
projectId: kmipClient.projectId
};
};
return {
createKmipClient,
updateKmipClient,
deleteKmipClient,
getKmipClient,
listKmipClientsByProjectId,
createKmipClientCertificate
};
};
@@ -1,7 +1,14 @@
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { KmipPermission } from "./kmip-enum";
export type TCreateKmipClientCertificateDTO = {
clientId: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateKmipClientDTO = {
name: string;
description?: string;
@@ -48,7 +48,8 @@ export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
DeleteClients = "delete-clients",
ReadClients = "read-clients"
ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
}
export enum ProjectPermissionSub {
@@ -596,7 +597,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
+6 -1
View File
@@ -35,6 +35,7 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal";
import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal";
@@ -385,6 +386,7 @@ export const registerRoutes = async (
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const kmipClientDAL = kmipClientDALFactory(db);
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db);
const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db);
@@ -1429,7 +1431,10 @@ export const registerRoutes = async (
const kmipService = kmipServiceFactory({
kmipClientDAL,
permissionService
permissionService,
kmipClientCertificateDAL,
kmipInstanceConfigDAL,
kmsService
});
await superAdminService.initServerCfg();
@@ -11,7 +11,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip";
import { TAuthLoginFactory } from "../auth/auth-login-service";
@@ -562,7 +562,7 @@ export const superAdminServiceFactory = ({
}: TGenerateInstanceKmipServerCertificateDTO) => {
const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!kmipInstanceConfig) {
throw new BadRequestError({
throw new InternalServerError({
message: "KMIP has not been configured for the instance"
});
}