diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8d66fbde3..b12d99d43 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -56,9 +56,6 @@ import { TCertificateBodies, TCertificateBodiesInsert, TCertificateBodiesUpdate, - TCertificateProfiles, - TCertificateProfilesInsert, - TCertificateProfilesUpdate, TCertificates, TCertificateSecrets, TCertificateSecretsInsert, @@ -71,9 +68,6 @@ import { TCertificateTemplates, TCertificateTemplatesInsert, TCertificateTemplatesUpdate, - TCertificateTemplatesV2, - TCertificateTemplatesV2Insert, - TCertificateTemplatesV2Update, TDynamicSecretLeases, TDynamicSecretLeasesInsert, TDynamicSecretLeasesUpdate, @@ -275,6 +269,12 @@ import { TPkiApiEnrollmentConfigs, TPkiApiEnrollmentConfigsInsert, TPkiApiEnrollmentConfigsUpdate, + TPkiCertificateProfiles, + TPkiCertificateProfilesInsert, + TPkiCertificateProfilesUpdate, + TPkiCertificateTemplatesV2, + TPkiCertificateTemplatesV2Insert, + TPkiCertificateTemplatesV2Update, TPkiCollectionItems, TPkiCollectionItemsInsert, TPkiCollectionItemsUpdate, @@ -683,15 +683,15 @@ declare module "knex/types/tables" { TCertificateTemplatesInsert, TCertificateTemplatesUpdate >; - [TableName.CertificateTemplateV2]: KnexOriginal.CompositeTableType< - TCertificateTemplatesV2, - TCertificateTemplatesV2Insert, - TCertificateTemplatesV2Update + [TableName.PkiCertificateTemplateV2]: KnexOriginal.CompositeTableType< + TPkiCertificateTemplatesV2, + TPkiCertificateTemplatesV2Insert, + TPkiCertificateTemplatesV2Update >; - [TableName.CertificateProfile]: KnexOriginal.CompositeTableType< - TCertificateProfiles, - TCertificateProfilesInsert, - TCertificateProfilesUpdate + [TableName.PkiCertificateProfile]: KnexOriginal.CompositeTableType< + TPkiCertificateProfiles, + TPkiCertificateProfilesInsert, + TPkiCertificateProfilesUpdate >; [TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType< TPkiEstEnrollmentConfigs, diff --git a/backend/src/db/migrations/20251007133321_pki-v3-tables.ts b/backend/src/db/migrations/20251007133321_pki-v3-tables.ts index d1ec020ce..1564ff0b4 100644 --- a/backend/src/db/migrations/20251007133321_pki-v3-tables.ts +++ b/backend/src/db/migrations/20251007133321_pki-v3-tables.ts @@ -4,8 +4,8 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasTable(TableName.CertificateTemplateV2))) { - await knex.schema.createTable(TableName.CertificateTemplateV2, (t) => { + if (!(await knex.schema.hasTable(TableName.PkiCertificateTemplateV2))) { + await knex.schema.createTable(TableName.PkiCertificateTemplateV2, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("projectId").notNullable(); t.foreign("projectId").references("id").inTable(TableName.Project); @@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise { t.unique(["name", "projectId"]); }); - await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2); + await createOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2); } if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) { @@ -55,17 +55,17 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); } - if (!(await knex.schema.hasTable(TableName.CertificateProfile))) { - await knex.schema.createTable(TableName.CertificateProfile, (t) => { + if (!(await knex.schema.hasTable(TableName.PkiCertificateProfile))) { + await knex.schema.createTable(TableName.PkiCertificateProfile, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("projectId").notNullable(); - t.foreign("projectId").references("id").inTable(TableName.Project); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.uuid("caId").notNullable(); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority); t.uuid("certificateTemplateId").notNullable(); - t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2); + t.foreign("certificateTemplateId").references("id").inTable(TableName.PkiCertificateTemplateV2); t.string("slug").notNullable(); t.string("description"); @@ -82,13 +82,13 @@ export async function up(knex: Knex): Promise { t.unique(["slug", "projectId"]); }); - await createOnUpdateTrigger(knex, TableName.CertificateProfile); + await createOnUpdateTrigger(knex, TableName.PkiCertificateProfile); } if (!(await knex.schema.hasColumn(TableName.Certificate, "profileId"))) { await knex.schema.alterTable(TableName.Certificate, (t) => { t.uuid("profileId"); - t.foreign("profileId").references("id").inTable(TableName.CertificateProfile).onDelete("SET NULL"); + t.foreign("profileId").references("id").inTable(TableName.PkiCertificateProfile).onDelete("SET NULL"); t.index("profileId"); }); } @@ -103,8 +103,8 @@ export async function down(knex: Knex): Promise { }); } - await knex.schema.dropTableIfExists(TableName.CertificateProfile); - await dropOnUpdateTrigger(knex, TableName.CertificateProfile); + await knex.schema.dropTableIfExists(TableName.PkiCertificateProfile); + await dropOnUpdateTrigger(knex, TableName.PkiCertificateProfile); await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig); await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); @@ -112,6 +112,6 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig); await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig); - await knex.schema.dropTableIfExists(TableName.CertificateTemplateV2); - await dropOnUpdateTrigger(knex, TableName.CertificateTemplateV2); + await knex.schema.dropTableIfExists(TableName.PkiCertificateTemplateV2); + await dropOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2); } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7f05f4f3f..e40a275a4 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -16,11 +16,9 @@ export * from "./certificate-authority-certs"; export * from "./certificate-authority-crl"; export * from "./certificate-authority-secret"; export * from "./certificate-bodies"; -export * from "./certificate-profiles"; export * from "./certificate-secrets"; export * from "./certificate-template-est-configs"; export * from "./certificate-templates"; -export * from "./certificate-templates-v2"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; @@ -95,6 +93,8 @@ export * from "./pam-resources"; export * from "./pam-sessions"; export * from "./pki-alerts"; export * from "./pki-api-enrollment-configs"; +export * from "./pki-certificate-profiles"; +export * from "./pki-certificate-templates-v2"; export * from "./pki-collection-items"; export * from "./pki-collections"; export * from "./pki-est-enrollment-configs"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index edd7bb95d..cfd29311b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -23,8 +23,8 @@ export enum TableName { CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", CertificateTemplate = "certificate_templates", - CertificateTemplateV2 = "certificate_templates_v2", - CertificateProfile = "certificate_profiles", + PkiCertificateTemplateV2 = "pki_certificate_templates_v2", + PkiCertificateProfile = "pki_certificate_profiles", PkiEstEnrollmentConfig = "pki_est_enrollment_configs", PkiApiEnrollmentConfig = "pki_api_enrollment_configs", PkiSubscriber = "pki_subscribers", diff --git a/backend/src/db/schemas/certificate-profiles.ts b/backend/src/db/schemas/pki-certificate-profiles.ts similarity index 62% rename from backend/src/db/schemas/certificate-profiles.ts rename to backend/src/db/schemas/pki-certificate-profiles.ts index 9e91706ed..368770c3e 100644 --- a/backend/src/db/schemas/certificate-profiles.ts +++ b/backend/src/db/schemas/pki-certificate-profiles.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { TImmutableDBKeys } from "./models"; -export const CertificateProfilesSchema = z.object({ +export const PkiCertificateProfilesSchema = z.object({ id: z.string().uuid(), projectId: z.string(), caId: z.string().uuid(), @@ -21,6 +21,8 @@ export const CertificateProfilesSchema = z.object({ updatedAt: z.date() }); -export type TCertificateProfiles = z.infer; -export type TCertificateProfilesInsert = Omit, TImmutableDBKeys>; -export type TCertificateProfilesUpdate = Partial, TImmutableDBKeys>>; +export type TPkiCertificateProfiles = z.infer; +export type TPkiCertificateProfilesInsert = Omit, TImmutableDBKeys>; +export type TPkiCertificateProfilesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/certificate-templates-v2.ts b/backend/src/db/schemas/pki-certificate-templates-v2.ts similarity index 64% rename from backend/src/db/schemas/certificate-templates-v2.ts rename to backend/src/db/schemas/pki-certificate-templates-v2.ts index 08977988a..de4603887 100644 --- a/backend/src/db/schemas/certificate-templates-v2.ts +++ b/backend/src/db/schemas/pki-certificate-templates-v2.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { TImmutableDBKeys } from "./models"; -export const CertificateTemplatesV2Schema = z.object({ +export const PkiCertificateTemplatesV2Schema = z.object({ id: z.string().uuid(), projectId: z.string(), name: z.string(), @@ -22,8 +22,8 @@ export const CertificateTemplatesV2Schema = z.object({ updatedAt: z.date() }); -export type TCertificateTemplatesV2 = z.infer; -export type TCertificateTemplatesV2Insert = Omit, TImmutableDBKeys>; -export type TCertificateTemplatesV2Update = Partial< - Omit, TImmutableDBKeys> +export type TPkiCertificateTemplatesV2 = z.infer; +export type TPkiCertificateTemplatesV2Insert = Omit, TImmutableDBKeys>; +export type TPkiCertificateTemplatesV2Update = Partial< + Omit, TImmutableDBKeys> >; diff --git a/backend/src/ee/routes/est/certificate-est-router.ts b/backend/src/ee/routes/est/certificate-est-router.ts index be6c552ec..2f9c6ca51 100644 --- a/backend/src/ee/routes/est/certificate-est-router.ts +++ b/backend/src/ee/routes/est/certificate-est-router.ts @@ -4,24 +4,53 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; export const registerCertificateEstRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); - const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => { + const getIdentifierType = async ({ + identifier, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: { + identifier: string; + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string; + }): Promise<"template" | "profile" | null> => { try { await server.services.certificateProfile.getEstConfigurationByProfile({ + actor, + actorId, + actorAuthMethod, + actorOrgId, profileId: identifier }); return "profile"; - } catch { + } catch (profileError) { try { await server.services.certificateTemplate.getEstConfiguration({ - isInternal: true, + isInternal: false, + actor, + actorId, + actorAuthMethod, + actorOrgId, certificateTemplateId: identifier }); return "template"; - } catch { + } catch (templateError) { + server.log.debug( + { + identifier, + profileError: profileError instanceof Error ? profileError.message : "Unknown error", + templateError: templateError instanceof Error ? templateError.message : "Unknown error" + }, + "EST identifier not found as profile or template" + ); return null; } } @@ -80,7 +109,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = const identifier = urlFragments.slice(-2)[0]; - const identifierType = await getIdentifierType(identifier); + const identifierType = await getIdentifierType({ + identifier, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); if (!identifierType) { res.raw.statusCode = 404; res.raw.setHeader("Content-Type", "text/plain"); @@ -92,6 +127,10 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = let estConfig; if (identifierType === "profile") { estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, profileId: identifier }); } else { @@ -149,7 +188,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = void res.header("Content-Transfer-Encoding", "base64"); const { identifier } = req.params; - const identifierType = await getIdentifierType(identifier); + const identifierType = await getIdentifierType({ + identifier, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); if (!identifierType) { throw new BadRequestError({ message: "Certificate template or profile not found" }); @@ -190,7 +235,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = void res.header("Content-Transfer-Encoding", "base64"); const { identifier } = req.params; - const identifierType = await getIdentifierType(identifier); + const identifierType = await getIdentifierType({ + identifier, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); if (!identifierType) { throw new BadRequestError({ message: "Certificate template or profile not found" }); @@ -230,7 +281,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = void res.header("Content-Transfer-Encoding", "base64"); const { identifier } = req.params; - const identifierType = await getIdentifierType(identifier); + const identifierType = await getIdentifierType({ + identifier, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); if (!identifierType) { throw new BadRequestError({ message: "Certificate template or profile not found" }); diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index be36390e7..6d9bdcc66 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -1,7 +1,7 @@ import RE2 from "re2"; import { z } from "zod"; -import { CertificateProfilesSchema } from "@app/db/schemas"; +import { PkiCertificateProfilesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -72,7 +72,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid ), response: { 200: z.object({ - certificateProfile: CertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema }) } }, @@ -126,7 +126,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfiles: CertificateProfilesSchema.extend({ + certificateProfiles: PkiCertificateProfilesSchema.extend({ metrics: z .object({ profileId: z.string(), @@ -177,7 +177,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], params: z.object({ - id: z.string().min(1) + id: z.string().uuid() }), querystring: z.object({ includeMetrics: z.coerce.boolean().optional().default(false), @@ -185,7 +185,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfile: CertificateProfilesSchema.extend({ + certificateProfile: PkiCertificateProfilesSchema.extend({ certificateAuthority: z .object({ id: z.string(), @@ -207,7 +207,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid id: z.string(), disableBootstrapCaValidation: z.boolean(), hashedPassphrase: z.string(), - encryptedCaChain: z.any() + encryptedCaChain: z.string() }) .optional(), apiConfig: z @@ -288,7 +288,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfile: CertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema }) } }, @@ -317,7 +317,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], params: z.object({ - id: z.string().min(1) + id: z.string().uuid() }), body: z .object({ @@ -363,7 +363,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid ), response: { 200: z.object({ - certificateProfile: CertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema }) } }, @@ -408,7 +408,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfile: CertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema }) } }, @@ -448,11 +448,11 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], params: z.object({ - id: z.string().min(1) + id: z.string().uuid() }), querystring: z.object({ - offset: z.number().min(0).default(0), - limit: z.number().min(1).max(100).default(20), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(20), status: z.enum(["active", "expired", "revoked"]).optional(), search: z.string().optional() }), diff --git a/backend/src/server/routes/v2/certificate-templates-v2-router.ts b/backend/src/server/routes/v2/certificate-templates-v2-router.ts index 889961776..8a7189727 100644 --- a/backend/src/server/routes/v2/certificate-templates-v2-router.ts +++ b/backend/src/server/routes/v2/certificate-templates-v2-router.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; @@ -93,9 +94,19 @@ const templateV2SanSchema = z const templateV2ValiditySchema = z.object({ max: z .string() - .regex(/^\d+[dhmy]$/, { - message: "Max validity must be in format like '365d', '12m', '1y', or '24h'" - }) + .refine( + (val) => { + if (!val) return true; + if (val.length < 2) return false; + const unit = val.slice(-1); + const number = val.slice(0, -1); + const digitRegex = new RE2("^\\d+$"); + return ["d", "h", "m", "y"].includes(unit) && digitRegex.test(number); + }, + { + message: "Max validity must be in format like '365d', '12m', '1y', or '24h'" + } + ) .optional() }); diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index d5c1a7a90..7e50eeee5 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; @@ -90,24 +91,28 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => keyUsages: req.body.keyUsages, extendedKeyUsages: req.body.extendedKeyUsages, altNames: req.body.subjectAltNames - ? req.body.subjectAltNames - .split(", ") - .map((name) => name.trim()) - .map((name) => { - const mappedType = validateAndMapAltNameType(name); - if (!mappedType) return null; - const typeMapping = { - dns: "dns_name", - ip: "ip_address", - email: "email", - url: "uri" - } as const; - return { - type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType, - value: mappedType.value - }; - }) - .filter((item): item is NonNullable => item !== null) + ? (() => { + const splitRegex = new RE2("[,;]+"); + return req.body.subjectAltNames + .split(splitRegex) + .map((name) => name.trim()) + .filter((name) => name.length > 0) + .map((name) => { + const mappedType = validateAndMapAltNameType(name); + if (!mappedType) return null; + const typeMapping = { + dns: "dns_name", + ip: "ip_address", + email: "email", + url: "uri" + } as const; + return { + type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType, + value: mappedType.value + }; + }) + .filter((item): item is NonNullable => item !== null); + })() : undefined, validity: { ttl: req.body.ttl @@ -223,7 +228,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => certificateProfileId: req.body.profileId, certificateId: data.certificateId, profileName: data.profileName, - commonName: req.body.csr || "" + commonName: "" } } }); diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.test.ts b/backend/src/services/certificate-authority/certificate-authority-fns.test.ts index 676d89fcd..6006cded4 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.test.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.test.ts @@ -136,7 +136,7 @@ describe("signatureAlgorithmToAlgCfg", () => { const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_secp521r1"); expect(result.name).toBe("ECDSA"); - expect(result.namedCurve).toBe("P-256"); + expect(result.namedCurve).toBe("P-521"); expect(result.hash).toBe("SHA-256"); }); diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 5f6ee7678..24b2aa2cf 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -101,8 +101,16 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgorithm: CertKeyAlgorithm | string) => { // Parse signature algorithm like "RSA-SHA256", "ECDSA-SHA256" etc. + if (!signatureAlgorithm || typeof signatureAlgorithm !== "string" || !signatureAlgorithm.includes("-")) { + throw new Error(`Invalid signature algorithm format: ${signatureAlgorithm}`); + } + const [keyType, hashType] = signatureAlgorithm.split("-"); + if (!keyType || !hashType) { + throw new Error(`Malformed signature algorithm: ${signatureAlgorithm}`); + } + const normalizeHashType = (hash: string) => { const upperHash = hash.toUpperCase(); @@ -118,7 +126,7 @@ export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgori if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384"; if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512"; - return hash; + throw new Error(`Unsupported hash algorithm: ${hash}`); }; const normalizedHash = hashType ? normalizeHashType(hashType) : undefined; @@ -136,7 +144,16 @@ export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgori const is384Curve = keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384"; // eslint-disable-next-line no-case-declarations - const namedCurve = is384Curve ? "P-384" : "P-256"; + const is521Curve = keyAlgorithm === "EC_secp521r1" || keyAlgorithm === "EC_P521"; + // eslint-disable-next-line no-case-declarations + let namedCurve: string; + if (is521Curve) { + namedCurve = "P-521"; + } else if (is384Curve) { + namedCurve = "P-384"; + } else { + namedCurve = "P-256"; + } return { name: "ECDSA", namedCurve, diff --git a/backend/src/services/certificate-authority/certificate-authority-schemas.ts b/backend/src/services/certificate-authority/certificate-authority-schemas.ts index 61d620156..5ecc50a4b 100644 --- a/backend/src/services/certificate-authority/certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/certificate-authority-schemas.ts @@ -18,7 +18,7 @@ export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick( export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), - projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId), + projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId), enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) }); @@ -26,7 +26,7 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), - projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId), + projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId), enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) }); diff --git a/backend/src/services/certificate-common/certificate-constants.ts b/backend/src/services/certificate-common/certificate-constants.ts index d7ac82c16..937be6cad 100644 --- a/backend/src/services/certificate-common/certificate-constants.ts +++ b/backend/src/services/certificate-common/certificate-constants.ts @@ -191,6 +191,23 @@ export const mapLegacyExtendedKeyUsageToStandard = (usage: string): CertExtended } }; +export enum CertKeyAlgorithm { + RSA_2048 = "RSA_2048", + RSA_3072 = "RSA_3072", + RSA_4096 = "RSA_4096", + ECDSA_P256 = "EC_prime256v1", + ECDSA_P384 = "EC_secp384r1" +} + +export enum CertSignatureAlgorithm { + RSA_SHA256 = "RSA-SHA256", + RSA_SHA384 = "RSA-SHA384", + RSA_SHA512 = "RSA-SHA512", + ECDSA_SHA256 = "ECDSA-SHA256", + ECDSA_SHA384 = "ECDSA-SHA384", + ECDSA_SHA512 = "ECDSA-SHA512" +} + export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType); export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType); export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType); @@ -199,3 +216,5 @@ export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit); export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType); export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule); export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect); +export const KEY_ALGORITHM_OPTIONS = Object.values(CertKeyAlgorithm); +export const SIGNATURE_ALGORITHM_OPTIONS = Object.values(CertSignatureAlgorithm); diff --git a/backend/src/services/certificate-common/certificate-utils.ts b/backend/src/services/certificate-common/certificate-utils.ts index 231216b86..b88f183db 100644 --- a/backend/src/services/certificate-common/certificate-utils.ts +++ b/backend/src/services/certificate-common/certificate-utils.ts @@ -1,3 +1,5 @@ +import RE2 from "re2"; + import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types"; import { CertExtendedKeyUsageType, @@ -78,6 +80,43 @@ export const buildCertificateSubjectFromTemplate = ( return subject; }; +const isWildcardPattern = (value: string): boolean => { + return value.includes("*"); +}; + +const createWildcardRegex = (pattern: string): RE2 => { + const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g); + const escaped = pattern.replace(escapeRegex, "\\$&"); + const wildcardRegex = new RE2(/\*/g); + const regexPattern = escaped.replace(wildcardRegex, ".*"); + return new RE2(`^${regexPattern}$`); +}; + +const validateValueAgainstPatterns = (value: string, patterns: string[]): boolean => { + if (!patterns || patterns.length === 0) { + return false; + } + + for (const pattern of patterns) { + if (isWildcardPattern(pattern)) { + try { + const regex = createWildcardRegex(pattern); + if (regex.test(value)) { + return true; + } + } catch { + if (pattern === value) { + return true; + } + } + } else if (pattern === value) { + return true; + } + } + + return false; +}; + export const buildSubjectAlternativeNamesFromTemplate = ( request: { subjectAlternativeNames?: Array<{ type: string; value: string }> }, templateSans?: Array<{ @@ -98,7 +137,25 @@ export const buildSubjectAlternativeNamesFromTemplate = ( const allowedSans: string[] = []; request.subjectAlternativeNames.forEach((san) => { - allowedSans.push(san.value); + const templateSan = templateSans.find((template) => template.type === san.type); + + if (!templateSan) { + allowedSans.push(san.value); + return; + } + + if (templateSan.denied && validateValueAgainstPatterns(san.value, templateSan.denied)) { + throw new Error(`SAN value '${san.value}' is explicitly denied for type '${san.type}'`); + } + + const isRequired = templateSan.required && validateValueAgainstPatterns(san.value, templateSan.required); + const isAllowed = templateSan.allowed && validateValueAgainstPatterns(san.value, templateSan.allowed); + + if (isRequired || isAllowed || (!templateSan.allowed && !templateSan.required)) { + allowedSans.push(san.value); + } else { + throw new Error(`SAN value '${san.value}' is not allowed for type '${san.type}'`); + } }); return allowedSans.join(","); diff --git a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts index c517b927b..a22cef37b 100644 --- a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts +++ b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts @@ -173,6 +173,11 @@ export const certificateEstV3ServiceFactory = ({ } const certTemplate = await certificateTemplateDAL.findById(profile.certificateTemplateId); + if (!certTemplate) { + throw new NotFoundError({ + message: `Certificate template with ID '${profile.certificateTemplateId}' not found` + }); + } const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0]; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 6b2852c6b..f52667b70 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -7,118 +7,188 @@ import { ormify, selectAllTableCols } from "@app/lib/knex"; import { EnrollmentType, + TCertificateProfile, TCertificateProfileCertificate, TCertificateProfileInsert, TCertificateProfileMetrics, - TCertificateProfileUpdate + TCertificateProfileUpdate, + TCertificateProfileWithConfigs, + TCertificateProfileWithRawMetrics } from "./certificate-profile-types"; export type TCertificateProfileDALFactory = ReturnType; export const certificateProfileDALFactory = (db: TDbClient) => { - const certificateProfileOrm = ormify(db, TableName.CertificateProfile); + const certificateProfileOrm = ormify(db, TableName.PkiCertificateProfile); - const create = async (data: TCertificateProfileInsert, tx?: Knex) => { + const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise => { try { - const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).insert(data).returning("*"); + const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile).insert(data).returning("*")) as [ + TCertificateProfile + ]; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Create certificate profile" }); } }; - const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex) => { + const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise => { try { - const [certificateProfile] = await (tx || db)(TableName.CertificateProfile) + const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile) .where({ id }) .update(data) - .returning("*"); + .returning("*")) as [TCertificateProfile]; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Update certificate profile" }); } }; - const deleteById = async (id: string, tx?: Knex) => { + const deleteById = async (id: string, tx?: Knex): Promise => { try { - const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).where({ id }).del().returning("*"); + const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile) + .where({ id }) + .del() + .returning("*")) as [TCertificateProfile]; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Delete certificate profile" }); } }; - const findById = async (id: string, tx?: Knex) => { + const findById = async (id: string, tx?: Knex): Promise => { try { - const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ id }).first(); + const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first()) as + | TCertificateProfile + | undefined; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by id" }); } }; - const findByIdWithConfigs = async (id: string, tx?: Knex) => { + const findByIdWithConfigs = async (id: string, tx?: Knex): Promise => { try { - const result = await (tx || db)(TableName.CertificateProfile) - .select( - selectAllTableCols(TableName.CertificateProfile), - (tx || db).ref("id").withSchema(TableName.CertificateAuthority).as("caId"), - (tx || db).ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), - (tx || db).ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), - (tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"), - (tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"), - (tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"), - (tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"), - (tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"), - (tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"), - (tx || db) - .ref("disableBootstrapCaValidation") - .withSchema(TableName.PkiEstEnrollmentConfig) - .as("estConfigDisableBootstrapCaValidation"), - (tx || db) - .ref("hashedPassphrase") - .withSchema(TableName.PkiEstEnrollmentConfig) - .as("estConfigHashedPassphrase"), - (tx || db) - .ref("encryptedCaChain") - .withSchema(TableName.PkiEstEnrollmentConfig) - .as("estConfigEncryptedCaChain"), - (tx || db).ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"), - (tx || db).ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), - (tx || db).ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays") - ) + const query = (tx || db)(TableName.PkiCertificateProfile) .leftJoin( TableName.CertificateAuthority, - `${TableName.CertificateProfile}.caId`, + `${TableName.PkiCertificateProfile}.caId`, `${TableName.CertificateAuthority}.id` ) .leftJoin( - TableName.CertificateTemplateV2, - `${TableName.CertificateProfile}.certificateTemplateId`, - `${TableName.CertificateTemplateV2}.id` + TableName.PkiCertificateTemplateV2, + `${TableName.PkiCertificateProfile}.certificateTemplateId`, + `${TableName.PkiCertificateTemplateV2}.id` ) .leftJoin( TableName.PkiEstEnrollmentConfig, - `${TableName.CertificateProfile}.estConfigId`, + `${TableName.PkiCertificateProfile}.estConfigId`, `${TableName.PkiEstEnrollmentConfig}.id` ) .leftJoin( TableName.PkiApiEnrollmentConfig, - `${TableName.CertificateProfile}.apiConfigId`, + `${TableName.PkiCertificateProfile}.apiConfigId`, `${TableName.PkiApiEnrollmentConfig}.id` ) - .where(`${TableName.CertificateProfile}.id`, id) + .select(selectAllTableCols(TableName.PkiCertificateProfile)) + .select( + db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), + db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"), + db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), + db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"), + db.ref("id").withSchema(TableName.PkiCertificateTemplateV2).as("templateId"), + db.ref("projectId").withSchema(TableName.PkiCertificateTemplateV2).as("templateProjectId"), + db.ref("name").withSchema(TableName.PkiCertificateTemplateV2).as("templateName"), + db.ref("description").withSchema(TableName.PkiCertificateTemplateV2).as("templateDescription"), + db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"), + db + .ref("disableBootstrapCaValidation") + .withSchema(TableName.PkiEstEnrollmentConfig) + .as("estConfigDisableBootstrapCaValidation"), + db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigHashedPassphrase"), + db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"), + db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"), + db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), + db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays") + ) + .where(`${TableName.PkiCertificateProfile}.id`, id) .first(); - return result; + const result = await query; + + if (!result) return undefined; + + const estConfig = + result.estConfigEncryptedCaChain && result.estConfigId && result.estConfigHashedPassphrase + ? ({ + id: result.estConfigId, + disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation, + hashedPassphrase: result.estConfigHashedPassphrase, + encryptedCaChain: result.estConfigEncryptedCaChain.toString("base64") + } as TCertificateProfileWithConfigs["estConfig"]) + : undefined; + + const apiConfig = result.apiConfigId + ? ({ + id: result.apiConfigId, + autoRenew: !!result.apiConfigAutoRenew, + autoRenewDays: result.apiConfigAutoRenewDays || undefined + } as TCertificateProfileWithConfigs["apiConfig"]) + : undefined; + + const certificateAuthority = + result.caId && result.caProjectId && result.caStatus && result.caName + ? ({ + id: result.caId, + projectId: result.caProjectId, + status: result.caStatus, + name: result.caName + } as TCertificateProfileWithConfigs["certificateAuthority"]) + : undefined; + + const certificateTemplate = + result.templateId && result.templateProjectId && result.templateName + ? ({ + id: result.templateId, + projectId: result.templateProjectId, + name: result.templateName, + description: result.templateDescription || undefined + } as TCertificateProfileWithConfigs["certificateTemplate"]) + : undefined; + + const transformedResult: TCertificateProfileWithConfigs = { + id: result.id, + projectId: result.projectId, + caId: result.caId, + certificateTemplateId: result.certificateTemplateId, + slug: result.slug, + description: result.description, + enrollmentType: result.enrollmentType as EnrollmentType, + estConfigId: result.estConfigId, + apiConfigId: result.apiConfigId, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + estConfig, + apiConfig, + certificateAuthority, + certificateTemplate + }; + + return transformedResult; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by id with configs" }); } }; - const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => { + const findBySlugAndProjectId = async ( + slug: string, + projectId: string, + tx?: Knex + ): Promise => { try { - const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ slug, projectId }).first(); + const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile) + .where({ slug, projectId }) + .first()) as TCertificateProfile | undefined; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by slug and project id" }); @@ -137,7 +207,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { expiringDays?: number; } = {}, tx?: Knex - ) => { + ): Promise => { try { const { offset = 0, @@ -149,25 +219,27 @@ export const certificateProfileDALFactory = (db: TDbClient) => { expiringDays = 7 } = options; - let query = (tx || db)(TableName.CertificateProfile).where( - `${TableName.CertificateProfile}.projectId`, + let query = (tx || db)(TableName.PkiCertificateProfile).where( + `${TableName.PkiCertificateProfile}.projectId`, projectId ); if (search) { query = query.where((builder) => { - void builder - .whereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`) - .orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`); + void builder.where((qb) => { + void qb + .whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`) + .orWhereILike(`${TableName.PkiCertificateProfile}.description`, `%${search}%`); + }); }); } if (enrollmentType) { - query = query.where(`${TableName.CertificateProfile}.enrollmentType`, enrollmentType); + query = query.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType); } if (caId) { - query = query.where(`${TableName.CertificateProfile}.caId`, caId); + query = query.where(`${TableName.PkiCertificateProfile}.caId`, caId); } if (includeMetrics) { @@ -176,9 +248,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => { expiringDate.setDate(now.getDate() + expiringDays); const certificateProfiles = await query - .leftJoin(TableName.Certificate, `${TableName.CertificateProfile}.id`, `${TableName.Certificate}.profileId`) + .leftJoin( + TableName.Certificate, + `${TableName.PkiCertificateProfile}.id`, + `${TableName.Certificate}.profileId` + ) .select( - selectAllTableCols(TableName.CertificateProfile), + selectAllTableCols(TableName.PkiCertificateProfile), db.raw("COUNT(certificates.id) as total_certificates"), db.raw( 'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates', @@ -194,21 +270,21 @@ export const certificateProfileDALFactory = (db: TDbClient) => { ), db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates') ) - .groupBy(`${TableName.CertificateProfile}.id`) - .orderBy(`${TableName.CertificateProfile}.createdAt`, "desc") + .groupBy(`${TableName.PkiCertificateProfile}.id`) + .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc") .offset(offset) .limit(limit); - return certificateProfiles; + return certificateProfiles as TCertificateProfileWithRawMetrics[]; } const certificateProfiles = await query - .select(selectAllTableCols(TableName.CertificateProfile)) - .orderBy(`${TableName.CertificateProfile}.createdAt`, "desc") + .select(selectAllTableCols(TableName.PkiCertificateProfile)) + .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc") .offset(offset) .limit(limit); - return certificateProfiles; + return certificateProfiles as TCertificateProfile[]; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profiles by project id" }); } @@ -222,15 +298,17 @@ export const certificateProfileDALFactory = (db: TDbClient) => { caId?: string; } = {}, tx?: Knex - ) => { + ): Promise => { try { const { search, enrollmentType, caId } = options; - let query = (tx || db)(TableName.CertificateProfile).where({ projectId }); + let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId }); if (search) { query = query.where((builder) => { - void builder.orWhereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`); + void builder.where((qb) => { + void qb.whereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`); + }); }); } @@ -249,11 +327,15 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } }; - const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { + const findByNameAndProjectId = async ( + name: string, + projectId: string, + tx?: Knex + ): Promise => { try { - const certificateProfile = await (tx || db)(TableName.CertificateProfile) + const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile) .where({ slug: name, projectId }) - .first(); + .first()) as TCertificateProfile | undefined; return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by name and project id" }); @@ -278,7 +360,9 @@ export const certificateProfileDALFactory = (db: TDbClient) => { if (search) { query = query.where((builder) => { - void builder.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`); + void builder.where((qb) => { + void qb.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`); + }); }); } @@ -311,7 +395,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => { .offset(offset) .limit(limit); - return certificates; + return certificates.map((cert) => ({ + ...cert, + revokedAt: cert.revokedAt ?? null + })); } catch (error) { throw new DatabaseError({ error, name: "Get certificates by profile" }); } diff --git a/backend/src/services/certificate-profile/certificate-profile-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-schemas.ts index d2a45f8c9..7b3e2cc57 100644 --- a/backend/src/services/certificate-profile/certificate-profile-schemas.ts +++ b/backend/src/services/certificate-profile/certificate-profile-schemas.ts @@ -5,7 +5,7 @@ import { EnrollmentType } from "./certificate-profile-types"; export const createCertificateProfileSchema = z .object({ - projectId: z.string().min(1), + projectId: z.string().uuid("Project ID must be valid"), caId: z.string().uuid(), certificateTemplateId: z.string().uuid(), slug: z @@ -103,12 +103,12 @@ export const getCertificateProfileByIdSchema = z.object({ }); export const getCertificateProfileBySlugSchema = z.object({ - projectId: z.string().min(1), + projectId: z.string().uuid("Project ID must be valid"), slug: z.string().min(1) }); export const listCertificateProfilesSchema = z.object({ - projectId: z.string().min(1), + projectId: z.string().uuid("Project ID must be valid"), offset: z.coerce.number().min(0).default(0), limit: z.coerce.number().min(1).max(100).default(20), search: z.string().optional(), diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index e8cb0a41f..ff5e918a2 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -712,7 +712,7 @@ describe("CertificateProfileService", () => { estConfig: { disableBootstrapCaValidation: false, passphrase: "secret-passphrase", - encryptedCaChain: "encrypted-ca-chain-data" + encryptedCaChain: Buffer.from("test-ca-chain-data").toString("base64") } }; @@ -1117,14 +1117,17 @@ describe("CertificateProfileService", () => { ...sampleProfileWithConfigs, id: profileId, enrollmentType: EnrollmentType.EST, - estConfigEncryptedCaChain: Buffer.from("mock-ca-chain"), - estConfigDisableBootstrapCaValidation: false, - estConfigHashedPassphrase: "hashed-passphrase" + estConfig: { + id: "est-config-123", + disableBootstrapCaValidation: false, + hashedPassphrase: "hashed-passphrase", + encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64") + } } as TCertificateProfileWithConfigs; (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); - const result = await service.getEstConfigurationByProfile({ profileId }); + const result = await service.getEstConfigurationByProfile({ ...mockActor, profileId }); expect(result).toEqual({ orgId: "project-123", @@ -1139,7 +1142,7 @@ describe("CertificateProfileService", () => { const profileId = "non-existent-profile"; (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(null); - await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(NotFoundError); + await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(NotFoundError); }); it("should throw ForbiddenRequestError when profile is not configured for EST enrollment", async () => { @@ -1148,15 +1151,20 @@ describe("CertificateProfileService", () => { ...sampleProfileWithConfigs, id: profileId, enrollmentType: EnrollmentType.API, // Wrong enrollment type - estConfigEncryptedCaChain: Buffer.from("mock-ca-chain"), - estConfigDisableBootstrapCaValidation: false, - estConfigHashedPassphrase: "hashed-passphrase" + estConfig: { + id: "est-config-123", + disableBootstrapCaValidation: false, + hashedPassphrase: "hashed-passphrase", + encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64") + } } as TCertificateProfileWithConfigs; (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); - await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(ForbiddenRequestError); - await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow( + await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow( + ForbiddenRequestError + ); + await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow( "Profile is not configured for EST enrollment" ); }); @@ -1167,15 +1175,13 @@ describe("CertificateProfileService", () => { ...sampleProfileWithConfigs, id: profileId, enrollmentType: EnrollmentType.EST, - estConfigEncryptedCaChain: null, // Missing EST config - estConfigDisableBootstrapCaValidation: false, - estConfigHashedPassphrase: "hashed-passphrase" + estConfig: undefined // Missing EST config } as TCertificateProfileWithConfigs; (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); - await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(NotFoundError); - await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow( + await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(NotFoundError); + await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow( "EST configuration not found for this profile" ); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index cd50187ca..037775916 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -8,7 +8,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; @@ -127,11 +127,24 @@ export const certificateProfileServiceFactory = ({ // Hash the passphrase const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS); + let encryptedCaChainBuffer: Buffer; + try { + if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") { + throw new BadRequestError({ message: "Invalid or missing CA chain data" }); + } + encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64"); + if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) { + throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" }); + } + } catch (error) { + throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" }); + } + const estConfig = await estEnrollmentConfigDAL.create( { disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation, hashedPassphrase, - encryptedCaChain: Buffer.from(data.estConfig.encryptedCaChain, "base64") + encryptedCaChain: encryptedCaChainBuffer }, tx ); @@ -233,9 +246,21 @@ export const certificateProfileServiceFactory = ({ ...(estConfig.passphrase && { hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS) }), - ...(estConfig.caChain && { - encryptedCaChain: Buffer.from(estConfig.caChain, "base64") - }) + ...(estConfig.caChain && + (() => { + try { + if (typeof estConfig.caChain !== "string") { + throw new BadRequestError({ message: "CA chain must be a string" }); + } + const buffer = Buffer.from(estConfig.caChain, "base64"); + if (buffer.toString("base64") !== estConfig.caChain) { + throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" }); + } + return { encryptedCaChain: buffer }; + } catch (error) { + throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" }); + } + })()) }, tx ); @@ -596,28 +621,54 @@ export const certificateProfileServiceFactory = ({ return metrics; }; - const getEstConfigurationByProfile = async ({ profileId }: { profileId: string }) => { + const getEstConfigurationByProfile = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + profileId + }: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + profileId: string; + }) => { const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); if (!profile) { throw new NotFoundError({ message: "Certificate profile not found" }); } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); + if (profile.enrollmentType !== EnrollmentType.EST) { throw new ForbiddenRequestError({ message: "Profile is not configured for EST enrollment" }); } - if (!profile.estConfigEncryptedCaChain) { + if (!profile.estConfig) { throw new NotFoundError({ message: "EST configuration not found for this profile" }); } return { orgId: profile.projectId, isEnabled: true, - caChain: profile.estConfigEncryptedCaChain.toString("base64"), - disableBootstrapCertValidation: profile.estConfigDisableBootstrapCaValidation, - hashedPassphrase: profile.estConfigHashedPassphrase + caChain: profile.estConfig.encryptedCaChain, + disableBootstrapCertValidation: profile.estConfig.disableBootstrapCaValidation, + hashedPassphrase: profile.estConfig.hashedPassphrase }; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 6ed601b23..e40a54810 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -1,23 +1,23 @@ import { - TCertificateProfiles, - TCertificateProfilesInsert, - TCertificateProfilesUpdate -} from "@app/db/schemas/certificate-profiles"; + TPkiCertificateProfiles, + TPkiCertificateProfilesInsert, + TPkiCertificateProfilesUpdate +} from "@app/db/schemas/pki-certificate-profiles"; export enum EnrollmentType { API = "api", EST = "est" } -export type TCertificateProfile = Omit & { +export type TCertificateProfile = Omit & { enrollmentType: EnrollmentType; }; -export type TCertificateProfileInsert = Omit & { +export type TCertificateProfileInsert = Omit & { enrollmentType: EnrollmentType; }; -export type TCertificateProfileUpdate = Omit & { +export type TCertificateProfileUpdate = Omit & { enrollmentType?: EnrollmentType; estConfig?: { disableBootstrapCaValidation?: boolean; @@ -47,7 +47,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & { id: string; disableBootstrapCaValidation: boolean; hashedPassphrase: string; - encryptedCaChain: Buffer; + encryptedCaChain: string; }; apiConfig?: { id: string; @@ -73,7 +73,7 @@ export interface TCertificateProfileCertificate { status: string; notBefore: Date; notAfter: Date; - revokedAt: Date | null | undefined; + revokedAt: Date | null; createdAt: Date; } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index 08a98a3b8..3b935f26a 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -2,7 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2"; +import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; @@ -19,7 +19,7 @@ interface CountResult { } export const certificateTemplateV2DALFactory = (db: TDbClient) => { - const certificateTemplateV2Orm = ormify(db, TableName.CertificateTemplateV2); + const certificateTemplateV2Orm = ormify(db, TableName.PkiCertificateTemplateV2); const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => { const serialized = { ...data } as Record; @@ -43,7 +43,17 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { jsonFields.forEach((field) => { const value = raw[field]; if (value !== null && value !== undefined) { - parsed[field] = typeof value === "string" ? JSON.parse(value) : value; + if (typeof value === "string") { + try { + parsed[field] = JSON.parse(value); + } catch (error) { + throw new Error( + `Invalid JSON in field '${field}': ${error instanceof Error ? error.message : "Parse error"}` + ); + } + } else { + parsed[field] = value; + } } else { parsed[field] = undefined; } @@ -55,9 +65,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const create = async (data: TCertificateTemplateV2Insert, tx?: Knex) => { try { const serializedData = serializeJsonFields(data); - const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) - .insert(serializedData as TCertificateTemplatesV2Insert) - .returning("*"); + const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2) + .insert(serializedData as TPkiCertificateTemplatesV2Insert) + .returning("*")) as Record[]; if (!certificateTemplateV2) { throw new Error("Failed to create certificate template v2"); @@ -72,10 +82,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TCertificateTemplateV2Update, tx?: Knex) => { try { const serializedData = serializeJsonFields(data); - const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) + const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2) .where({ id }) .update(serializedData) - .returning("*"); + .returning("*")) as Record[]; if (!certificateTemplateV2) { return null; @@ -89,10 +99,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const deleteById = async (id: string, tx?: Knex) => { try { - const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) + const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2) .where({ id }) .del() - .returning("*"); + .returning("*")) as Record[]; return certificateTemplateV2; } catch (error) { @@ -102,7 +112,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2).where({ id }).first(); + const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2).where({ id }).first()) as + | Record + | undefined; if (!certificateTemplateV2) { return null; @@ -126,7 +138,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { try { const { offset = 0, limit = 20, search } = options; - let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId }); + let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId }); if (search) { query = query.where((builder) => { @@ -152,7 +164,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { try { const { search } = options; - let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId }); + let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId }); if (search) { query = query.where((builder) => { @@ -169,9 +181,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { try { - const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2) + const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2) .where({ name, projectId }) - .first(); + .first()) as Record | undefined; if (!certificateTemplateV2) { return null; @@ -185,7 +197,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const isTemplateInUse = async (templateId: string, tx?: Knex) => { try { - const profileCount = await (tx || db)(TableName.CertificateProfile) + const profileCount = await (tx || db)(TableName.PkiCertificateProfile) .where({ certificateTemplateId: templateId }) .count("*") .first(); @@ -207,11 +219,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { const getProfilesUsingTemplate = async (templateId: string, tx?: Knex) => { try { - const profiles = await (tx || db)(TableName.CertificateProfile) + const profiles = await (tx || db)(TableName.PkiCertificateProfile) .select("id", "slug", "description") .where({ certificateTemplateId: templateId }); - return profiles; + return profiles as Array<{ id: string; slug: string; description?: string }>; } catch (error) { throw new DatabaseError({ error, name: "Get profiles using certificate template v2" }); } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts index 29432620e..5e653f626 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts @@ -100,7 +100,7 @@ const templateV2AlgorithmsSchema = z.object({ export const certificateTemplateV2ResponseSchema = z.object({ id: z.string().uuid(), - projectId: z.string(), + projectId: z.string().uuid("Project ID must be valid"), name: z.string(), description: z.string().nullable().optional(), subject: z.array(templateV2SubjectSchema).optional(), @@ -116,7 +116,6 @@ export const certificateTemplateV2ResponseSchema = z.object({ export const certificateRequestSchema = z.object({ commonName: z.string().optional(), organization: z.string().optional(), - organizationName: z.string().optional(), country: z.string().optional(), keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(), extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(), diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index 351c05830..daacc5dde 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -581,18 +581,20 @@ describe("CertificateTemplateV2Service", () => { { ttl: "invalid", shouldThrow: true } ]; - for (const testCase of testCases) { - const request = { ...validRequest, validity: { ttl: testCase.ttl } }; + await Promise.all( + testCases.map(async (testCase) => { + const request = { ...validRequest, validity: { ttl: testCase.ttl } }; - if (testCase.shouldThrow) { - await expect(service.validateCertificateRequest("template-123", request)).rejects.toThrow( - `Invalid TTL format: ${testCase.ttl}` - ); - } else { - const result = await service.validateCertificateRequest("template-123", request); - expect(result.isValid).toBe(testCase.shouldBeValid); - } - } + if (testCase.shouldThrow) { + await expect(service.validateCertificateRequest("template-123", request)).rejects.toThrow( + `Invalid TTL format: ${testCase.ttl}` + ); + } else { + const result = await service.validateCertificateRequest("template-123", request); + expect(result.isValid).toBe(testCase.shouldBeValid); + } + }) + ); }); it("should allow optional key usages and extended key usages", async () => { @@ -1108,21 +1110,23 @@ describe("CertificateTemplateV2Service", () => { { ttl: "1y", shouldBeValid: true, description: "exactly 365 days in years" } ]; - for (const testCase of testCases) { - const request = { - commonName: "example.com", - keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT], - extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH], - validity: { ttl: testCase.ttl } - }; + await Promise.all( + testCases.map(async (testCase) => { + const request = { + commonName: "example.com", + keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT], + extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH], + validity: { ttl: testCase.ttl } + }; - const result = await service.validateCertificateRequest("template-123", request); - expect(result.isValid).toBe(testCase.shouldBeValid); + const result = await service.validateCertificateRequest("template-123", request); + expect(result.isValid).toBe(testCase.shouldBeValid); - if (!testCase.shouldBeValid) { - expect(result.errors.length).toBeGreaterThan(0); - } - } + if (!testCase.shouldBeValid) { + expect(result.errors.length).toBeGreaterThan(0); + } + }) + ); }); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 61644215f..4ea0a195d 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -290,8 +290,8 @@ export const certificateTemplateV2ServiceFactory = ({ const subjectPolicies = template.subject; const requestAttributes = new Map(); if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName); - if (request.organization || request.organizationName) { - requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization || request.organizationName!); + if (request.organization) { + requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization); } if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts index a034f1937..de2691d9a 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts @@ -1,4 +1,7 @@ -import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2"; +import { + TPkiCertificateTemplatesV2, + TPkiCertificateTemplatesV2Insert +} from "@app/db/schemas/pki-certificate-templates-v2"; import { CertExtendedKeyUsageType, CertKeyUsageType, @@ -38,7 +41,7 @@ export interface TTemplateV2Policy { }; } -export type TCertificateTemplateV2 = TCertificateTemplatesV2 & { +export type TCertificateTemplateV2 = TPkiCertificateTemplatesV2 & { subject?: TTemplateV2Policy["subject"]; sans?: TTemplateV2Policy["sans"]; keyUsages?: TTemplateV2Policy["keyUsages"]; @@ -47,7 +50,7 @@ export type TCertificateTemplateV2 = TCertificateTemplatesV2 & { validity?: TTemplateV2Policy["validity"]; }; -export type TCertificateTemplateV2Insert = TCertificateTemplatesV2Insert & { +export type TCertificateTemplateV2Insert = TPkiCertificateTemplatesV2Insert & { subject?: TTemplateV2Policy["subject"]; sans?: TTemplateV2Policy["sans"]; keyUsages?: TTemplateV2Policy["keyUsages"]; @@ -66,7 +69,6 @@ export type TCertificateTemplateV2Update = Partial< export interface TCertificateRequest { commonName?: string; organization?: string; - organizationName?: string; organizationUnit?: string; locality?: string; state?: string; diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 20c061bf7..20c0ffd88 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -420,7 +420,7 @@ export const certificateTemplateServiceFactory = ({ }; const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => { - const { certificateTemplateId } = dto; + const { certificateTemplateId, isInternal } = dto; const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certTemplate) { @@ -429,7 +429,7 @@ export const certificateTemplateServiceFactory = ({ }); } - if (!dto.isInternal) { + if (!isInternal) { const { permission } = await permissionService.getProjectPermission({ actor: dto.actor, actorId: dto.actorId, @@ -440,7 +440,7 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPkiTemplateActions.Edit, + ProjectPermissionPkiTemplateActions.Read, subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); } diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 3540a2b71..95f9a5077 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -1,20 +1,26 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { ACMESANType, CertificateOrderStatus } from "@app/services/certificate/certificate-types"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { CertExtendedKeyUsageType, CertIncludeType, CertKeyUsageType, CertSubjectAttributeType } from "@app/services/certificate-common/certificate-constants"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { ActorType, AuthMethod } from "../auth/auth-type"; import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service"; @@ -22,49 +28,73 @@ import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./cer describe("CertificateV3Service", () => { let service: TCertificateV3ServiceFactory; - const mockCertificateDAL = { + const mockCertificateDAL: Pick = { findOne: vi.fn(), updateById: vi.fn() - } as any; + }; - const mockCertificateAuthorityDAL = { + const mockCertificateAuthorityDAL: Pick = { findByIdWithAssociatedCa: vi.fn() - } as any; + }; - const mockCertificateProfileDAL = { + const mockCertificateProfileDAL: Pick = { findByIdWithConfigs: vi.fn() - } as any; + }; - const mockCertificateTemplateV2Service = { + const mockCertificateTemplateV2Service: Pick< + TCertificateTemplateV2ServiceFactory, + "validateCertificateRequest" | "getTemplateV2ById" + > = { validateCertificateRequest: vi.fn(), getTemplateV2ById: vi.fn() - } as any; + }; - const mockInternalCaService = { - signCertFromCa: vi.fn(), - issueCertFromCa: vi.fn() - } as any; + const mockInternalCaService: Pick = + { + signCertFromCa: vi.fn(), + issueCertFromCa: vi.fn() + }; - const mockPermissionService = { + const mockPermissionService: Pick = { getProjectPermission: vi.fn().mockResolvedValue({ permission: { - throwUnlessCan: vi.fn() + throwUnlessCan: vi.fn(), + can: vi.fn().mockReturnValue(true), + cannot: vi.fn().mockReturnValue(false), + relevantRuleFor: vi.fn(), + rules: [] } }) - } as any; + }; const mockActor = { actor: ActorType.USER, actorId: "user-123", - actorAuthMethod: AuthMethod.EMAIL as any, + actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "org-123" }; beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + + // Mock ForbiddenError.from static method vi.spyOn(ForbiddenError, "from").mockReturnValue({ throwUnlessCan: vi.fn() } as any); + // Ensure the permission service mock is properly set up + (mockPermissionService.getProjectPermission as any).mockResolvedValue({ + permission: { + throwUnlessCan: vi.fn(), + can: vi.fn().mockReturnValue(true), + cannot: vi.fn().mockReturnValue(false), + relevantRuleFor: vi.fn(), + rules: [], + detectSubjectType: vi.fn() + } + }); + service = certificateV3ServiceFactory({ certificateDAL: mockCertificateDAL, certificateAuthorityDAL: mockCertificateAuthorityDAL, @@ -77,6 +107,7 @@ describe("CertificateV3Service", () => { afterEach(() => { vi.clearAllMocks(); + vi.restoreAllMocks(); // Ensure static method mocks are properly restored }); describe("issueCertificateFromProfile", () => { @@ -96,16 +127,51 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.API, caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile", + description: "Test profile" }; const mockCA = { id: "ca-123", - externalCa: null + projectId: "project-123", + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-123" + }, + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true }; const mockTemplate = { id: "template-123", + name: "Test Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + description: "Test template", signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, keyAlgorithm: { defaultKeyType: "RSA_2048" }, attributes: [ @@ -118,29 +184,71 @@ describe("CertificateV3Service", () => { }; const mockCertificateResult = { - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("issuing-ca"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "issuing-ca", + privateKey: "key", + serialNumber: "123456", + ca: { + id: "ca-123", + projectId: "project-123", + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: null, + notAfter: null, + activeCaCertId: "cert-123", + caId: "ca-123" + } + } }; const mockCertRecord = { id: "cert-123", - serialNumber: "123456" + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + commonName: "test.example.com", + friendlyName: "Test Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-123", + certificateTemplateId: "template-123", + revokedAt: null, + revokedBy: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ isValid: true, errors: [], warnings: [] }); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(mockCA); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(mockTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue(mockCertificateResult); - mockCertificateDAL.findOne.mockResolvedValue(mockCertRecord); - mockCertificateDAL.updateById.mockResolvedValue({}); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); const result = await service.issueCertificateFromProfile({ profileId, @@ -163,16 +271,53 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.API, caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-camel", + description: "Test camelCase profile", + estConfigId: null, + apiConfigId: null }; const mockCA = { id: "ca-123", - externalCa: null + projectId: "project-123", + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-123" + }, + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true }; const mockTemplate = { id: "template-123", + name: "Test Template for CamelCase", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + description: "Test template for camelCase validation", signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, keyAlgorithm: { defaultKeyType: "RSA_2048" }, attributes: [ @@ -181,20 +326,36 @@ describe("CertificateV3Service", () => { include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] - }; - - const mockCertificateResult = { - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("issuing-ca"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; const mockCertRecord = { id: "cert-123", - serialNumber: "123456" + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + commonName: "test.example.com", + friendlyName: "Test Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-123", + certificateTemplateId: "template-123", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }; const camelCaseRequest = { @@ -215,17 +376,55 @@ describe("CertificateV3Service", () => { validity: { ttl: "10d" } }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ + const mockCertificateResultWithCa = { + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "issuing-ca", + privateKey: "key", + serialNumber: "123456", + ca: { + id: "ca-123", + projectId: "project-123", + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: null, + notAfter: null, + activeCaCertId: "cert-123", + caId: "ca-123" + } + } + }; + + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ isValid: true, errors: [], warnings: [] }); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(mockCA); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(mockTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue(mockCertificateResult); - mockCertificateDAL.findOne.mockResolvedValue(mockCertRecord); - mockCertificateDAL.updateById.mockResolvedValue({}); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); await service.issueCertificateFromProfile({ profileId, @@ -261,10 +460,16 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-est", + description: "Test EST profile", + estConfigId: null, + apiConfigId: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); await expect( service.issueCertificateFromProfile({ @@ -285,7 +490,7 @@ describe("CertificateV3Service", () => { it("should throw NotFoundError when profile doesn't exist", async () => { const profileId = "non-existent-profile"; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(null); + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(undefined); await expect( service.issueCertificateFromProfile({ @@ -308,31 +513,137 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.API, caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-sign", + description: "Test signing profile", + estConfigId: null, + apiConfigId: null }; const mockCA = { id: "ca-123", - externalCa: null + projectId: "project-123", + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-123" + }, + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true }; const mockSignResult = { - certificate: Buffer.from("signed-cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("issuing-ca"), - serialNumber: "789012" + certificate: "signed-cert", + certificateChain: "chain", + issuingCaCertificate: "issuing-ca", + serialNumber: "789012", + commonName: "test.example.com", + ca: { + id: "ca-123", + projectId: "project-123", + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: null, + notAfter: null, + activeCaCertId: "cert-123", + caId: "ca-123" + } + } }; const mockCertRecord = { id: "cert-456", - serialNumber: "789012" + serialNumber: "789012", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + commonName: "test.example.com", + friendlyName: "Test Signing Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-123", + certificateTemplateId: "template-123", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(mockCA); - mockInternalCaService.signCertFromCa.mockResolvedValue(mockSignResult); - mockCertificateDAL.findOne.mockResolvedValue(mockCertRecord); - mockCertificateDAL.updateById.mockResolvedValue({}); + const mockTemplate = { + id: "template-123", + name: "Test Signing Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + description: "Test template for signing certificates", + signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, + keyAlgorithm: { defaultKeyType: "RSA_2048" }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertIncludeType.OPTIONAL, + value: ["example.com"] + } + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined + }; + + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); + vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); const result = await service.signCertificateFromProfile({ profileId, @@ -356,10 +667,16 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-est-sign", + description: "Test EST signing profile", + estConfigId: null, + apiConfigId: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); await expect( service.signCertificateFromProfile({ @@ -399,16 +716,53 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.API, caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-order", + description: "Test order profile", + estConfigId: null, + apiConfigId: null }; const mockCA = { id: "ca-123", - externalCa: null + projectId: "project-123", + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-123" + }, + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true }; const mockTemplate = { id: "template-123", + name: "Test Order Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + description: "Test template for ordering certificates", signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, keyAlgorithm: { defaultKeyType: "RSA_2048" }, attributes: [ @@ -417,33 +771,87 @@ describe("CertificateV3Service", () => { include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; const mockCertificateResult = { - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("issuing-ca"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "issuing-ca", + privateKey: "key", + serialNumber: "123456", + ca: { + id: "ca-123", + projectId: "project-123", + name: "Test CA", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, + internalCa: { + id: "internal-ca-123", + parentCaId: null, + type: "ROOT", + friendlyName: "Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "Test CA", + dn: "CN=Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: null, + notAfter: null, + activeCaCertId: "cert-123", + caId: "ca-123" + } + } }; const mockCertRecord = { id: "cert-123", - serialNumber: "123456" + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-123", + commonName: "example.com", + friendlyName: "Test Order Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-123", + certificateTemplateId: "template-123", + revokedAt: null, + altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]), + caCertId: null, + keyUsages: ["DIGITAL_SIGNATURE"], + extendedKeyUsages: ["SERVER_AUTH"], + revocationReason: null, + pkiSubscriberId: null, + profileId: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ isValid: true, errors: [], warnings: [] }); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(mockCA); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(mockTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue(mockCertificateResult); - mockCertificateDAL.findOne.mockResolvedValue(mockCertRecord); - mockCertificateDAL.updateById.mockResolvedValue({}); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); const result = await service.orderCertificateFromProfile({ profileId, @@ -469,10 +877,16 @@ describe("CertificateV3Service", () => { projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type caId: "ca-123", - certificateTemplateId: "template-123" + certificateTemplateId: "template-123", + createdAt: new Date(), + updatedAt: new Date(), + slug: "test-profile-est-order", + description: "Test EST order profile", + estConfigId: null, + apiConfigId: null }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); await expect( service.orderCertificateFromProfile({ @@ -499,7 +913,12 @@ describe("CertificateV3Service", () => { projectId: "project-1", caId: "ca-1", certificateTemplateId: "template-1", - enrollmentType: EnrollmentType.API + enrollmentType: EnrollmentType.API, + createdAt: new Date(), + updatedAt: new Date(), + description: "Test profile for algorithm compatibility", + estConfigId: null, + apiConfigId: null }; const mockCertificateRequest = { @@ -518,38 +937,119 @@ describe("CertificateV3Service", () => { id: "ca-1", projectId: "project-1", status: "active", + name: "RSA Test CA", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, internalCa: { - keyAlgorithm: "RSA_2048" + id: "internal-ca-1", + parentCaId: null, + type: "ROOT", + friendlyName: "RSA Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "RSA Test CA", + dn: "CN=RSA Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_2048", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-1" } }; const rsaTemplate = { id: "template-1", + name: "RSA Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + description: "RSA template for algorithm compatibility", signatureAlgorithm: { allowedAlgorithms: ["SHA256-RSA", "SHA384-RSA"] }, + keyAlgorithm: null, attributes: [ { type: CertSubjectAttributeType.COMMON_NAME, include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsaCa); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] }); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue({ - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("ca-cert"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(rsaCa); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ + isValid: true, + errors: [], + warnings: [] + }); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(rsaTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({ + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "ca-cert", + privateKey: "key", + serialNumber: "123456", + ca: rsaCa as any + }); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }); - mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" }); - mockCertificateDAL.updateById.mockResolvedValue(undefined); // Should not throw - RSA CA is compatible with RSA signature algorithms await expect( @@ -569,38 +1069,119 @@ describe("CertificateV3Service", () => { id: "ca-1", projectId: "project-1", status: "active", + name: "EC Test CA", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, internalCa: { - keyAlgorithm: "EC_prime256v1" + id: "internal-ca-1", + parentCaId: null, + type: "ROOT", + friendlyName: "EC Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "EC Test CA", + dn: "CN=EC Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "EC_prime256v1", + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-1" } }; const ecdsaTemplate = { id: "template-1", + name: "ECDSA Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + description: "ECDSA template for algorithm compatibility", signatureAlgorithm: { allowedAlgorithms: ["SHA256-ECDSA", "SHA384-ECDSA"] }, + keyAlgorithm: null, attributes: [ { type: CertSubjectAttributeType.COMMON_NAME, include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(ecCa); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] }); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue({ - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("ca-cert"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(ecCa); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ + isValid: true, + errors: [], + warnings: [] + }); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(ecdsaTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({ + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "ca-cert", + privateKey: "key", + serialNumber: "123456", + ca: ecCa as any + }); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }); - mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" }); - mockCertificateDAL.updateById.mockResolvedValue(undefined); // Should not throw - EC CA is compatible with ECDSA signature algorithms await expect( @@ -620,38 +1201,119 @@ describe("CertificateV3Service", () => { id: "ca-1", projectId: "project-1", status: "active", + name: "RSA 8192 Test CA", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, internalCa: { - keyAlgorithm: "RSA_8192" // Future RSA key size + id: "internal-ca-1", + parentCaId: null, + type: "ROOT", + friendlyName: "RSA 8192 Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "RSA 8192 Test CA", + dn: "CN=RSA 8192 Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "RSA_8192", // Future RSA key size + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-1" } }; const rsaTemplate = { id: "template-1", + name: "RSA 8192 Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + description: "RSA 8192 template for future key sizes", signatureAlgorithm: { allowedAlgorithms: ["SHA256-RSA"] }, + keyAlgorithm: null, attributes: [ { type: CertSubjectAttributeType.COMMON_NAME, include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsa8192Ca); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] }); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue({ - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("ca-cert"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(rsa8192Ca); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ + isValid: true, + errors: [], + warnings: [] + }); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(rsaTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({ + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "ca-cert", + privateKey: "key", + serialNumber: "123456", + ca: rsa8192Ca as any + }); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }); - mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" }); - mockCertificateDAL.updateById.mockResolvedValue(undefined); // Should not throw - dynamic check supports new RSA key sizes await expect( @@ -671,38 +1333,119 @@ describe("CertificateV3Service", () => { id: "ca-1", projectId: "project-1", status: "active", + name: "EC secp521r1 Test CA", + createdAt: new Date(), + updatedAt: new Date(), + enableDirectIssuance: true, + externalCa: undefined, internalCa: { - keyAlgorithm: "EC_secp521r1" // Future EC curve + id: "internal-ca-1", + parentCaId: null, + type: "ROOT", + friendlyName: "EC secp521r1 Test CA", + organization: "Test Org", + ou: "Test OU", + country: "US", + province: "CA", + locality: "SF", + commonName: "EC secp521r1 Test CA", + dn: "CN=EC secp521r1 Test CA", + serialNumber: "123", + maxPathLength: null, + keyAlgorithm: "EC_secp521r1", // Future EC curve + notBefore: undefined, + notAfter: undefined, + activeCaCertId: "cert-123", + caId: "ca-1" } }; const ecdsaTemplate = { id: "template-1", + name: "ECDSA secp521r1 Template", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + description: "ECDSA secp521r1 template for future EC curves", signatureAlgorithm: { allowedAlgorithms: ["SHA384-ECDSA"] }, + keyAlgorithm: null, attributes: [ { type: CertSubjectAttributeType.COMMON_NAME, include: CertIncludeType.OPTIONAL, value: ["example.com"] } - ] + ], + subject: undefined, + sans: undefined, + keyUsages: undefined, + extendedKeyUsages: undefined, + algorithms: undefined, + validity: undefined }; - mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile); - mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(newEcCa); - mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] }); - mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate); - mockInternalCaService.issueCertFromCa.mockResolvedValue({ - certificate: Buffer.from("cert"), - certificateChain: Buffer.from("chain"), - issuingCaCertificate: Buffer.from("ca-cert"), - privateKey: Buffer.from("key"), - serialNumber: "123456" + vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); + vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(newEcCa); + vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ + isValid: true, + errors: [], + warnings: [] + }); + vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(ecdsaTemplate); + vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({ + certificate: "cert", + certificateChain: "chain", + issuingCaCertificate: "ca-cert", + privateKey: "key", + serialNumber: "123456", + ca: newEcCa as any + }); + vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.updateById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null }); - mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" }); - mockCertificateDAL.updateById.mockResolvedValue(undefined); // Should not throw - dynamic check supports new EC curves await expect( diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 69f3dce8d..a3644fbba 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -116,24 +116,25 @@ const validateAlgorithmCompatibility = ( throw new BadRequestError({ message: "CA key algorithm not found" }); } - const compatibleAlgorithms = template.algorithms.signature.filter((sigAlg: string) => { - const parts = sigAlg.split("-"); - const keyType = parts[parts.length - 1]; + const compatibleAlgorithms = + template.algorithms?.signature?.filter((sigAlg: string) => { + const parts = sigAlg.split("-"); + const keyType = parts[parts.length - 1]; - if (caKeyAlgorithm.startsWith("RSA")) { - return keyType === "RSA"; - } + if (caKeyAlgorithm.startsWith("RSA")) { + return keyType === "RSA"; + } - if (caKeyAlgorithm.startsWith("EC")) { - return keyType === "ECDSA"; - } + if (caKeyAlgorithm.startsWith("EC")) { + return keyType === "ECDSA"; + } - return false; - }); + return false; + }) || []; if (compatibleAlgorithms.length === 0) { throw new BadRequestError({ - message: `Template signature algorithms (${template.algorithms.signature.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})` + message: `Template signature algorithms (${template.algorithms?.signature?.join(", ") || "none"}) are not compatible with CA key algorithm (${caKeyAlgorithm})` }); } }; diff --git a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts index d5b7f9c9c..1768c76e9 100644 --- a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts +++ b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts @@ -54,28 +54,26 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, tx?: Knex) => { try { - const now = new Date(); - const renewalDate = new Date(); - renewalDate.setDate(now.getDate() + renewalThresholdDays); - - const profiles = await (tx || db)(TableName.CertificateProfile) + const profiles = await (tx || db)(TableName.PkiCertificateProfile) .join( TableName.PkiApiEnrollmentConfig, - `${TableName.CertificateProfile}.apiConfigId`, + `${TableName.PkiCertificateProfile}.apiConfigId`, `${TableName.PkiApiEnrollmentConfig}.id` ) .where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true) .where((query) => { - void query - .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) - .orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); + void query.where((qb) => { + void qb + .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) + .orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); + }); }) - .select((tx || db).ref("id").withSchema(TableName.CertificateProfile)) - .select((tx || db).ref("name").withSchema(TableName.CertificateProfile)) - .select((tx || db).ref("projectId").withSchema(TableName.CertificateProfile)) - .select((tx || db).ref("autoRenewDays").withSchema(TableName.CertificateProfile)); + .select((tx || db).ref("id").withSchema(TableName.PkiCertificateProfile)) + .select((tx || db).ref("name").withSchema(TableName.PkiCertificateProfile)) + .select((tx || db).ref("projectId").withSchema(TableName.PkiCertificateProfile)) + .select((tx || db).ref("autoRenewDays").withSchema(TableName.PkiCertificateProfile)); - return profiles; + return profiles as Array<{ id: string; name: string; projectId: string; autoRenewDays?: number }>; } catch (error) { throw new DatabaseError({ error, name: "Find profiles for auto renewal" }); } @@ -83,9 +81,9 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const isConfigInUse = async (configId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.CertificateProfile).where({ apiConfigId: configId }).count("*").first(); + const doc = await (tx || db)(TableName.PkiCertificateProfile).where({ apiConfigId: configId }).count("*").first(); - return parseInt(doc || "0", 10); + return parseInt((doc as { count?: string })?.count || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "Check if API enrollment config is in use" }); } diff --git a/backend/src/services/enrollment-config/est-enrollment-config-dal.ts b/backend/src/services/enrollment-config/est-enrollment-config-dal.ts index c13818270..b7520f290 100644 --- a/backend/src/services/enrollment-config/est-enrollment-config-dal.ts +++ b/backend/src/services/enrollment-config/est-enrollment-config-dal.ts @@ -14,7 +14,12 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => { try { - const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*"); + const result = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*"); + const [estConfig] = result; + + if (!estConfig) { + throw new Error("Failed to create EST enrollment config"); + } return estConfig; } catch (error) { @@ -24,7 +29,12 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => { try { - const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*"); + const result = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*"); + const [estConfig] = result; + + if (!estConfig) { + return null; + } return estConfig; } catch (error) { @@ -36,7 +46,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { try { const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first(); - return estConfig; + return estConfig || null; } catch (error) { throw new DatabaseError({ error, name: "Find EST enrollment config by id" }); } diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index d782b78e9..d4a62a710 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -235,7 +235,7 @@ export type TOrderCertificateResponse = { url: string; token: string; validated?: string; - error?: any; + error?: string | Error; }>; }>; certificate?: string; diff --git a/frontend/src/hooks/api/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index e49a89cc7..75b7e6a82 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -30,7 +30,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & { id: string; disableBootstrapCaValidation: boolean; hashedPassphrase: string; - encryptedCaChain: any; + encryptedCaChain: string; }; apiConfig?: { id: string; @@ -108,10 +108,10 @@ export type TProfileCertificate = { serialNumber: string; cn: string; status: string; - notBefore: Date; - notAfter: Date; + notBefore: string; + notAfter: string; isRevoked: boolean; - createdAt: Date; + createdAt: string; }; export type TGetProfileCertificatesDTO = { diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index 2768be2eb..08fc2f53e 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -146,8 +146,10 @@ export type TCertificateTemplateV2Policy = { denied?: string[]; }; algorithms?: { - signature?: string[]; - keyAlgorithm?: string[]; + signature?: Array< + "SHA256-RSA" | "SHA384-RSA" | "SHA512-RSA" | "SHA256-ECDSA" | "SHA384-ECDSA" | "SHA512-ECDSA" + >; + keyAlgorithm?: Array<"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384">; }; validity?: { max?: string; diff --git a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx index 9c8852908..66c73abcb 100644 --- a/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx +++ b/frontend/src/layouts/PkiManagerLayout/PkiManagerLayout.tsx @@ -109,7 +109,7 @@ export const PkiManagerLayout = () => {
- Certificates Authorities + Certificate Authorities )} diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index 24009c06e..e726f2fce 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -60,12 +60,7 @@ const schema = z commonName: z.string(), notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }), maxPathLength: z.string(), - keyAlgorithm: z.enum([ - CertKeyAlgorithm.RSA_2048, - CertKeyAlgorithm.RSA_4096, - CertKeyAlgorithm.ECDSA_P256, - CertKeyAlgorithm.ECDSA_P384 - ]) + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm) }) .required() }) @@ -145,13 +140,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { maxPathLength: ca.configuration.maxPathLength ? String(ca.configuration.maxPathLength) : "", - keyAlgorithm: - ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_2048 || - ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_4096 || - ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P256 || - ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 - ? ca.configuration.keyAlgorithm - : CertKeyAlgorithm.RSA_2048 + keyAlgorithm: (Object.values(CertKeyAlgorithm) as string[]).includes( + ca.configuration.keyAlgorithm + ) + ? ca.configuration.keyAlgorithm + : CertKeyAlgorithm.RSA_2048 } }); } else { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index ca5eb9659..6e3cf4ae8 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -112,9 +112,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState([]); const { currentProject } = useProject(); - const { data: cert } = useGetCert( - (popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "" - ); + const inputSerialNumber = + (popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""; + const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, ""); + + const { data: cert } = useGetCert(sanitizedSerialNumber); const { data: profilesData } = useListCertificateProfiles({ projectId: currentProject?.id || "", @@ -263,15 +265,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } useEffect(() => { if (cert) { - const subjectAttrs: Array<{ type: string; value: string }> = []; - if (cert.commonName) subjectAttrs.push({ type: "common_name", value: cert.commonName }); + const subjectAttrs: Array<{ type: "common_name"; value: string }> = []; + if (cert.commonName) + subjectAttrs.push({ type: "common_name" as const, value: cert.commonName }); reset({ profileId: "", subjectAttributes: - subjectAttrs.length > 0 - ? (subjectAttrs as any) - : [{ type: "common_name" as const, value: "" }], + subjectAttrs.length > 0 ? subjectAttrs : [{ type: "common_name" as const, value: "" }], subjectAltNames: cert.subjectAltNames ? cert.subjectAltNames.split(",").map((name) => { const trimmed = name.trim(); diff --git a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx index 5b0d1c98e..d2dfc414d 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx @@ -2,15 +2,16 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { useProject } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab"; enum TabSections { - CertificateTemplatesV2 = "templates-v2", - CertificateProfiles = "profiles" + CertificateProfiles = "profiles", + CertificateTemplatesV2 = "templates-v2" } export const PoliciesPage = () => { @@ -23,34 +24,53 @@ export const PoliciesPage = () => { } return ( -
- - {t("common.head-title", { title: "Certificate Policies" })} - -
- - - setActiveTab(value as TabSections)}> - -
- Certificate Profiles - Certificate Templates + + {(isAllowed) => { + if (!isAllowed) { + return ( +
+
+

You don't have permission to access certificate policies.

+
- + ); + } - - - + return ( +
+ + {t("common.head-title", { title: "Certificate Policies" })} + +
+ - - - - -
-
+ setActiveTab(value as TabSections)}> + +
+ Certificate Profiles + Certificate Templates +
+
+ + + + + + + + +
+
+
+ ); + }} + ); }; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 64a8d940d..4334fc2c5 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -60,7 +60,10 @@ export const CertificateProfilesTab = () => { type: "success" }); } catch (error) { - console.error("Failed to delete profile:", error); + console.error( + `Failed to delete profile "${selectedProfile.slug}" (ID: ${selectedProfile.id}):`, + error + ); } }; @@ -78,7 +81,7 @@ export const CertificateProfilesTab = () => { {canCreateProfile && (