mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 12:27:31 +00:00
get certificate private key endpoint + migrations
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.binary("encryptedCertificateChain").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateSecret))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateSecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("certId").notNullable().unique();
|
||||||
|
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||||
|
await knex.schema.dropTable(TableName.CertificateSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.dropColumn("encryptedCertificateChain");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
encryptedCertificate: zodBuffer
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedCertificateChain: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const CertificateSecretsSchema = z.object({
|
export const CertificateSecretsSchema = z.object({
|
||||||
@@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
certId: z.string().uuid(),
|
certId: z.string().uuid(),
|
||||||
pk: z.string(),
|
encryptedPrivateKey: zodBuffer
|
||||||
sk: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const OrganizationsSchema = z.object({
|
|||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional(),
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({
|
|||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
enforceCapitalization: z.boolean().default(false),
|
enforceCapitalization: z.boolean().default(false),
|
||||||
hasDeleteProtection: z.boolean().default(true).nullable().optional()
|
hasDeleteProtection: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -64,6 +64,51 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO(andrey): In the future add support for other formats outside of PEM. Adding a "format" query param may be best.
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/private-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "Get certificate private key",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certPrivateKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/issue-certificate",
|
url: "/issue-certificate",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { CrlReason } from "./certificate-types";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||||
|
import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types";
|
||||||
|
|
||||||
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
switch (crlReason) {
|
switch (crlReason) {
|
||||||
@@ -46,3 +51,44 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
|
|||||||
.map((cert) => cert.toString("pem"))
|
.map((cert) => cert.toString("pem"))
|
||||||
.join("\n")
|
.join("\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the public and private key of certificate
|
||||||
|
* Note: credentials are returned as PEM strings
|
||||||
|
*/
|
||||||
|
export const getCertificateCredentials = async ({
|
||||||
|
certId,
|
||||||
|
projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCertificateCredentialsDTO) => {
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId });
|
||||||
|
if (!certificateSecret)
|
||||||
|
throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: keyId
|
||||||
|
});
|
||||||
|
const decryptedPrivateKey = await kmsDecryptor({
|
||||||
|
cipherTextBlob: certificateSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
|
||||||
|
const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString();
|
||||||
|
|
||||||
|
const pkObj = crypto.createPublicKey(skObj);
|
||||||
|
const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateSecret,
|
||||||
|
certPrivateKey,
|
||||||
|
certPublicKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
|
||||||
|
|
||||||
|
export const certificateSecretDALFactory = (db: TDbClient) => {
|
||||||
|
const caSecretOrm = ormify(db, TableName.CertificateSecret);
|
||||||
|
return caSecretOrm;
|
||||||
|
};
|
||||||
@@ -15,11 +15,13 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||||
import { revocationReasonToCrlCode } from "./certificate-fns";
|
import { getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
||||||
|
|
||||||
type TCertificateServiceFactoryDep = {
|
type TCertificateServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
@@ -34,6 +36,7 @@ export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFac
|
|||||||
|
|
||||||
export const certificateServiceFactory = ({
|
export const certificateServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -67,6 +70,40 @@ export const certificateServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get certificate private key.
|
||||||
|
*/
|
||||||
|
const getCertPrivateKey = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(andrey): Update permission for privateKey fetching. Should be very strict.
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const { certPrivateKey } = await getCertificateCredentials({
|
||||||
|
certId: ca.id,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca,
|
||||||
|
cert,
|
||||||
|
certPrivateKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete certificate with serial number [serialNumber]
|
* Delete certificate with serial number [serialNumber]
|
||||||
*/
|
*/
|
||||||
@@ -203,6 +240,7 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getCert,
|
getCert,
|
||||||
|
getCertPrivateKey,
|
||||||
deleteCert,
|
deleteCert,
|
||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody
|
getCertBody
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509";
|
|||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
||||||
|
|
||||||
export enum CertStatus {
|
export enum CertStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
@@ -73,3 +77,11 @@ export type TRevokeCertDTO = {
|
|||||||
export type TGetCertBodyDTO = {
|
export type TGetCertBodyDTO = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertificateCredentialsDTO = {
|
||||||
|
certId: string;
|
||||||
|
projectId: string;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user