diff --git a/backend/src/db/migrations/20240522010055_certificate-authority.ts b/backend/src/db/migrations/20240530163136_certificate-mgmt.ts similarity index 99% rename from backend/src/db/migrations/20240522010055_certificate-authority.ts rename to backend/src/db/migrations/20240530163136_certificate-mgmt.ts index b8e3eb171..56fdff775 100644 --- a/backend/src/db/migrations/20240522010055_certificate-authority.ts +++ b/backend/src/db/migrations/20240530163136_certificate-mgmt.ts @@ -25,6 +25,7 @@ export async function up(knex: Knex): Promise { t.unique(["dn", "projectId"]); t.string("serialNumber").nullable().unique(); t.integer("maxPathLength").nullable(); + t.string("keyAlgorithm").notNullable(); t.datetime("notBefore").nullable(); t.datetime("notAfter").nullable(); }); diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 7e2d147f4..9f0e7f677 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -24,6 +24,7 @@ export const CertificateAuthoritiesSchema = z.object({ dn: z.string(), serialNumber: z.string().nullable().optional(), maxPathLength: z.number().nullable().optional(), + keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional() }); diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index c10af4ba4..9e0b9a3b5 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -23,8 +23,8 @@ export const UsersSchema = z.object({ isGhost: z.boolean().default(false), username: z.string(), isEmailVerified: z.boolean().default(false).nullable().optional(), - consecutiveFailedMfaAttempts: z.number().optional(), - isLocked: z.boolean().optional(), + consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(), + isLocked: z.boolean().default(false).nullable().optional(), temporaryLockDateEnd: z.date().nullable().optional() }); diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index facd70a54..d52e0f7f4 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -4,7 +4,7 @@ import { CertificateAuthoritiesSchema } from "@app/db/schemas"; 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 { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; +import { CaStatus, CaType, CertKeyAlgorithm } from "@app/services/certificate-authority/certificate-authority-types"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; export const registerCaRouter = async (server: FastifyZodProvider) => { @@ -30,7 +30,15 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { // format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format notBefore: validateCaDateField.optional(), notAfter: validateCaDateField.optional(), - maxPathLength: z.number().min(-1).optional() + maxPathLength: z.number().min(-1).default(-1), + keyAlgorithm: z + .enum([ + CertKeyAlgorithm.RSA_2048, + CertKeyAlgorithm.RSA_4096, + CertKeyAlgorithm.ECDSA_P256, + CertKeyAlgorithm.ECDSA_P384 + ]) + .default(CertKeyAlgorithm.RSA_2048) }) .refine( (data) => { diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index f7157925b..7b906a3f9 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -1,4 +1,4 @@ -import { TDNParts } from "./certificate-authority-types"; +import { CertKeyAlgorithm, TDNParts } from "./certificate-authority-types"; export const createDistinguishedName = (parts: TDNParts) => { const dnParts = []; @@ -10,3 +10,36 @@ export const createDistinguishedName = (parts: TDNParts) => { if (parts.locality) dnParts.push(`L=${parts.locality}`); return dnParts.join(", "); }; + +export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { + switch (keyAlgorithm) { + case CertKeyAlgorithm.RSA_4096: + return { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 4096 + }; + case CertKeyAlgorithm.ECDSA_P256: + return { + name: "ECDSA", + namedCurve: "P-256", + hash: "SHA-256" + }; + case CertKeyAlgorithm.ECDSA_P384: + return { + name: "ECDSA", + namedCurve: "P-384", + hash: "SHA-384" + }; + default: { + // RSA_2048 + return { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048 + }; + } + } +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 1fdfbfdb5..a3288fe22 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -13,11 +13,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TCertStatus } from "../certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; -import { createDistinguishedName } from "./certificate-authority-fns"; +import { createDistinguishedName, keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal"; import { CaStatus, CaType, + CertKeyAlgorithm, TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, @@ -68,6 +69,7 @@ export const certificateAuthorityServiceFactory = ({ notBefore, notAfter, maxPathLength, + keyAlgorithm, actorId, actorAuthMethod, actor, @@ -98,12 +100,7 @@ export const certificateAuthorityServiceFactory = ({ locality }); - const alg = { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - publicExponent: new Uint8Array([1, 0, 1]), - modulusLength: 2048 - }; + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey @@ -133,7 +130,13 @@ export const certificateAuthorityServiceFactory = ({ commonName, status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, dn, - ...(type === CaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber }) + keyAlgorithm, + ...(type === CaType.ROOT && { + maxPathLength, + notBefore: notBeforeDate, + notAfter: notAfterDate, + serialNumber + }) }, tx ); @@ -272,12 +275,7 @@ export const certificateAuthorityServiceFactory = ({ const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); - const alg = { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - publicExponent: new Uint8Array([1, 0, 1]), - modulusLength: 2048 - }; + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" }); const pkObj = crypto.createPublicKey({ key: caKeys.pk, format: "pem", type: "spki" }); @@ -371,12 +369,7 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); - const alg = { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - publicExponent: new Uint8Array([1, 0, 1]), - modulusLength: 2048 - }; + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); @@ -566,12 +559,7 @@ export const certificateAuthorityServiceFactory = ({ const caCertObj = new x509.X509Certificate(caCert.certificate); - const alg = { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - publicExponent: new Uint8Array([1, 0, 1]), - modulusLength: 2048 - }; + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index dbb0c52c9..6764000d8 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -11,6 +11,13 @@ export enum CaStatus { PENDING_CERTIFICATE = "pending-certificate" } +export enum CertKeyAlgorithm { + RSA_2048 = "RSA_2048", + RSA_4096 = "RSA_4096", + ECDSA_P256 = "EC_prime256v1", + ECDSA_P384 = "EC_secp384r1" +} + export type TCreateCaDTO = { projectSlug: string; type: CaType; @@ -22,7 +29,8 @@ export type TCreateCaDTO = { locality: string; notBefore?: string; notAfter?: string; - maxPathLength?: number; + maxPathLength: number; + keyAlgorithm: CertKeyAlgorithm; } & Omit; export type TGetCaDTO = { diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index 78963ab5f..e7ff2d6dd 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -55,6 +55,7 @@ consisting of a root CA and an intermediate CA using the Infisical UI. - Valid Until: The date until which the CA is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`. - Path Length: The maximum number of intermediate CAs that can be chained to this CA. A path of `-1` implies no limit; a path of `0` implies no intermediate CAs can be chained. + - Key Algorithm: The type of public key algorithm and size, in bits, of the key pair that the CA creates when it issues a certificate. Supported key algorithms are `RSA 2048`, `RSA 4096`, `ECDSA P-256`, and `ECDSA P-384` with the default being `RSA 2048`. - Organization (O): The organization name. - Country (C): The country code. - State or Province Name: The state or province. @@ -98,9 +99,8 @@ consisting of a root CA and an intermediate CA using the Infisical UI. ## FAQ - - Infisical currently only supports `RSA_2048` and `SHA256WITHRSA` for the - private key and signing algorithm. We are working to add support for more - algorithms in the future. + + Infisical supports `RSA 2048`, `RSA 4096`, `ECDSA P-256`, `ECDSA P-384` key + algorithms specified at the time of creating a CA. diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 02ce1862d..b8ed006cf 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,3 +1,4 @@ +import { CertKeyAlgorithm } from "../certificates/enums"; import { CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { @@ -16,6 +17,7 @@ export type TCertificateAuthority = { maxPathLength?: number; notAfter?: string; notBefore?: string; + keyAlgorithm: CertKeyAlgorithm; createdAt: string; updatedAt: string; }; @@ -31,6 +33,7 @@ export type TCreateCaDTO = { commonName: string; notAfter?: string; maxPathLength: number; + keyAlgorithm: CertKeyAlgorithm; }; export type TUpdateCaDTO = { diff --git a/frontend/src/hooks/api/certificates/constants.tsx b/frontend/src/hooks/api/certificates/constants.tsx index 6188006e6..32cf08886 100644 --- a/frontend/src/hooks/api/certificates/constants.tsx +++ b/frontend/src/hooks/api/certificates/constants.tsx @@ -1,6 +1,20 @@ -import { CertStatus } from "./enums"; +import { CertKeyAlgorithm,CertStatus } from "./enums"; export const certStatusToNameMap: { [K in CertStatus]: string } = { [CertStatus.ACTIVE]: "Active", [CertStatus.REVOKED]: "Revoked" }; + +export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = { + [CertKeyAlgorithm.RSA_2048]: "RSA 2048", + [CertKeyAlgorithm.RSA_4096]: "RSA 4096", + [CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256", + [CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384" +}; + +export const certKeyAlgorithms = [ + { label: "RSA 2048", value: CertKeyAlgorithm.RSA_2048 }, + { label: "RSA 4096", value: CertKeyAlgorithm.RSA_4096 }, + { label: "ECDSA P256", value: CertKeyAlgorithm.ECDSA_P256 }, + { label: "ECDSA P384", value: CertKeyAlgorithm.ECDSA_P384 } +]; diff --git a/frontend/src/hooks/api/certificates/enums.tsx b/frontend/src/hooks/api/certificates/enums.tsx index 5bab7ce1b..f8d8d3851 100644 --- a/frontend/src/hooks/api/certificates/enums.tsx +++ b/frontend/src/hooks/api/certificates/enums.tsx @@ -2,3 +2,10 @@ export enum CertStatus { ACTIVE = "active", REVOKED = "revoked" } + +export enum CertKeyAlgorithm { + RSA_2048 = "RSA_2048", + RSA_4096 = "RSA_4096", + ECDSA_P256 = "EC_prime256v1", + ECDSA_P384 = "EC_secp384r1" +} diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx index 52e37974e..353211c99 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx @@ -17,6 +17,8 @@ import { } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca"; +import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; +import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; const isValidDate = (dateString: string) => { @@ -40,7 +42,13 @@ const schema = z locality: z.string(), commonName: z.string(), notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }), - maxPathLength: z.string() + maxPathLength: z.string(), + keyAlgorithm: z.enum([ + CertKeyAlgorithm.RSA_2048, + CertKeyAlgorithm.RSA_4096, + CertKeyAlgorithm.ECDSA_P256, + CertKeyAlgorithm.ECDSA_P384 + ]) }) .required(); @@ -80,7 +88,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { locality: "", commonName: "", notAfter: getDateTenYearsFromToday(), - maxPathLength: "-1" + maxPathLength: "-1", + keyAlgorithm: CertKeyAlgorithm.RSA_2048 } }); @@ -97,7 +106,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { locality: ca.locality, commonName: ca.commonName, notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "", - maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "" + maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "", + keyAlgorithm: ca.keyAlgorithm }); } else { reset({ @@ -109,7 +119,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { locality: "", commonName: "", notAfter: getDateTenYearsFromToday(), - maxPathLength: "-1" + maxPathLength: "-1", + keyAlgorithm: CertKeyAlgorithm.RSA_2048 }); } }, [ca]); @@ -123,7 +134,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { locality, province, notAfter, - maxPathLength + maxPathLength, + keyAlgorithm }: FormData) => { try { if (!currentWorkspace?.slug) return; @@ -138,7 +150,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { province, locality, notAfter, - maxPathLength: Number(maxPathLength) + maxPathLength: Number(maxPathLength), + keyAlgorithm }); reset(); @@ -269,6 +282,32 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { /> )} + ( + + + + )} + />