diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 13bad0a6a..24ae75ff5 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,32 +1,38 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ +import type { TAcmeResponse } from "@app/ee/services/pki-acme/pki-acme-types"; +import { FastifyReply } from "fastify"; import { z } from "zod"; import { CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, CreateAcmeOrderResponseSchema, + DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, - DeactivateAcmeAccountSchema, - DownloadAcmeCertificateSchema, - FinalizeAcmeOrderResponseSchema, - FinalizeAcmeOrderSchema, + FinalizeAcmeOrderBodySchema, GetAcmeAuthorizationResponseSchema, - GetAcmeAuthorizationSchema, GetAcmeDirectoryResponseSchema, - GetAcmeDirectorySchema, - GetAcmeNewNonceSchema, GetAcmeOrderResponseSchema, - GetAcmeOrderSchema, ListAcmeOrdersResponseSchema, - ListAcmeOrdersSchema, RawJwsPayloadSchema, - RespondToAcmeChallengeResponseSchema, - RespondToAcmeChallengeSchema + RespondToAcmeChallengeResponseSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { + const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { + res.code(response.status); + for (const [key, value] of Object.entries(response.headers)) { + res.header(key, value); + } + + const nonce = await server.services.pkiAcme.getAcmeNewNonce(profileId); + res.header("Replay-Nonce", nonce); + res.header("Cache-Control", "no-store"); + return response.body; + }; + server.addContentTypeParser("application/jose+json", { parseAs: "string" }, (_, body, done) => { try { const strBody = body instanceof Buffer ? body.toString() : body; @@ -53,7 +59,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Directory - provides URLs for the client to make API calls to", - ...GetAcmeDirectorySchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), response: { 200: GetAcmeDirectoryResponseSchema } @@ -77,15 +85,17 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Nonce - generate a new nonce and return in Replay-Nonce header", - ...GetAcmeNewNonceSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), response: { - 200: z.object({}) + 200: z.string().length(0) } }, handler: async (req, res) => { const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId); res.header("Replay-Nonce", nonce); - return {}; + return ""; } }); @@ -112,23 +122,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { handler: async (req, res) => { const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); const { alg, jwk } = protectedHeader; - const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({ - profileId: req.params.profileId, - alg, - jwk: jwk!, - payload - }); - // TODO: DRY - res.code(status); - for (const [key, value] of Object.entries(headers)) { - res.header(key, value); - } - - // TODO: DRY - const nonce = await server.services.pkiAcme.getAcmeNewNonce(req.params.profileId); - res.header("Replay-Nonce", nonce); - res.header("Cache-Control", "no-store"); - return body; + return sendAcmeResponse( + res, + req.params.profileId, + await server.services.pkiAcme.createAcmeAccount({ + profileId: req.params.profileId, + alg, + jwk: jwk!, + payload + }) + ); } }); @@ -155,14 +158,20 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { payload, protectedHeader, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( + const { payload, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( req.params.profileId, req.body, CreateAcmeOrderBodySchema ); - const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body); - res.code(201); - return order; + return sendAcmeResponse( + res, + req.params.profileId, + await server.services.pkiAcme.createAcmeOrder({ + profileId: req.params.profileId, + accountId, + payload + }) + ); } }); @@ -178,7 +187,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Account Deactivation", - ...DeactivateAcmeAccountSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + accountId: z.string() + }), + body: DeactivateAcmeAccountBodySchema, response: { 200: DeactivateAcmeAccountResponseSchema } @@ -203,7 +216,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME List Orders - get existing orders from current account", - ...ListAcmeOrdersSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + accountId: z.string() + }), response: { 200: ListAcmeOrdersResponseSchema } @@ -228,7 +244,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Get Order - return status and details of the order", - ...GetAcmeOrderSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), response: { 200: GetAcmeOrderResponseSchema } @@ -253,10 +272,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Finalize Order - finalize cert order by providing CSR", - ...FinalizeAcmeOrderSchema.shape, - response: { - 200: FinalizeAcmeOrderResponseSchema - } + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), + body: FinalizeAcmeOrderBodySchema }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -278,7 +298,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Download Certificate - download certificate when ready", - ...DownloadAcmeCertificateSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + orderId: z.string().uuid() + }), response: { 200: z.string() } @@ -307,7 +330,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Identifier Authorization - get authorization info (challenges)", - ...GetAcmeAuthorizationSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + authzId: z.string().uuid() + }), response: { 200: GetAcmeAuthorizationResponseSchema } @@ -332,7 +358,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME Respond to Challenge - let ACME server know challenge is ready", - ...RespondToAcmeChallengeSchema.shape, + params: z.object({ + profileId: z.string().uuid(), + authzId: z.string().uuid() + }), response: { 200: RespondToAcmeChallengeResponseSchema } diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 7e50b2108..935776a8e 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,5 +1,26 @@ import { z } from "zod"; +export enum AcmeIdentifierType { + DNS = "dns" +} + +export enum AcmeOrderStatus { + Pending = "pending", + Processing = "processing", + Ready = "ready", + Valid = "valid", + Invalid = "invalid" +} + +export enum AcmeAuthStatus { + Pending = "pending", + Valid = "valid", + Invalid = "invalid", + Deactivated = "deactivated", + Expired = "expired", + Revoked = "revoked" +} + export const ProtectedHeaderSchema = z .object({ alg: z.string(), @@ -20,13 +41,6 @@ export const RawJwsPayloadSchema = z.object({ signature: z.string() }); -// Directory endpoint -export const GetAcmeDirectorySchema = z.object({ - params: z.object({ - profileId: z.string().uuid() - }) -}); - export const GetAcmeDirectoryResponseSchema = z.object({ newNonce: z.string(), newAccount: z.string(), @@ -34,13 +48,6 @@ export const GetAcmeDirectoryResponseSchema = z.object({ revokeCert: z.string().optional() }); -// New Nonce endpoint -export const GetAcmeNewNonceSchema = z.object({ - params: z.object({ - profileId: z.string().uuid() - }) -}); - // New Account payload schema export const CreateAcmeAccountBodySchema = z.object({ contact: z.array(z.string()).optional(), @@ -73,22 +80,16 @@ export const CreateAcmeAccountResponseSchema = z.object({ export const CreateAcmeOrderBodySchema = z.object({ identifiers: z.array( z.object({ - type: z.string(), - value: z.string() + type: z + .string() + .regex(/^(?!-)[A-Za-z0-9-]{1,63}(?; acmeAccountDAL: Pick; acmeOrderDAL: Pick; + acmeAuthDAL: Pick; }; export const pkiAcmeServiceFactory = ({ certificateProfileDAL, acmeAccountDAL, - acmeOrderDAL + acmeOrderDAL, + acmeAuthDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -168,11 +177,11 @@ export const pkiAcmeServiceFactory = ({ }; const getAcmeDirectory = async (profileId: string): Promise => { - await validateAcmeProfile(profileId); + const profile = await validateAcmeProfile(profileId); return { - newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-nonce`), - newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-account`), - newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-order`) + newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), + newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), + newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) }; }; @@ -244,15 +253,29 @@ export const pkiAcmeServiceFactory = ({ payload: TCreateAcmeOrderPayload; }): Promise> => { const account = await acmeAccountDAL.findById(profileId, accountId)!; - // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order - orders = await acmeOrderDAL.create({ - profileId, - accountId, - status: "pending" + const order = await acmeOrderDAL.create({ + accountId: account.id, + status: AcmeOrderStatus.Pending }); + payload.identifiers.forEach(async (identifier) => { + if (identifier.type === AcmeIdentifierType.DNS) { + // TODO: reuse existing authorizations for this identifier if they exist + const auth = await acmeAuthDAL.create({ + accountId: account.id, + status: AcmeAuthStatus.Pending, + identifierType: identifier.type, + identifierValue: identifier.value, + // TODO: read config from the profile to get the expiration time instead + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) + }); + } else { + throw new AcmeMalformedError({ detail: "Only DNS identifiers are supported" }); + } + }); + // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { @@ -262,10 +285,10 @@ export const pkiAcmeServiceFactory = ({ expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), identifiers: [], authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}/finalize`) + finalize: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}/finalize`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/orders/${orderId}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${account.profileId}/orders/${orderId}`) } }; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 49ac7750d..376baaaae 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -74,6 +74,7 @@ import { pamSessionServiceFactory } from "@app/ee/services/pam-session/pam-sessi import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; +import { pkiAcmeAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-auth-dal"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; @@ -350,6 +351,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { pkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; +import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege"; import { injectIdentity } from "../plugins/auth/inject-identity"; @@ -361,7 +363,6 @@ import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; import { registerV4Routes } from "./v4"; -import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -1068,7 +1069,7 @@ export const registerRoutes = async ( const acmeEnrollmentConfigDAL = acmeEnrollmentConfigDALFactory(db); const acmeAccountDAL = pkiAcmeAccountDALFactory(db); const acmeOrderDAL = pkiAcmeOrderDALFactory(db); - + const acmeAuthDAL = pkiAcmeAuthDALFactory(db); const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db); @@ -1172,7 +1173,8 @@ export const registerRoutes = async ( const pkiAcmeService = pkiAcmeServiceFactory({ certificateProfileDAL, acmeAccountDAL, - acmeOrderDAL + acmeOrderDAL, + acmeAuthDAL }); const pkiAlertService = pkiAlertServiceFactory({