From 4883c3ead33c76a0a7eafcf9300ec76a2be5b8b6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 16:04:26 -0700 Subject: [PATCH] Implement account creation --- .../services/pki-acme/pki-acme-account-dal.ts | 105 ++++++++++++++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 5 +- .../ee/services/pki-acme/pki-acme-service.ts | 64 ++++++++--- .../ee/services/pki-acme/pki-acme-types.ts | 18 ++- 4 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-account-dal.ts diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts new file mode 100644 index 000000000..93f7b7055 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -0,0 +1,105 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate } from "@app/db/schemas/pki-acme-accounts"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeAccountDALFactory = ReturnType; + +export const pkiAcmeAccountDALFactory = (db: TDbClient) => { + const pkiAcmeAccountOrm = ormify(db, TableName.PkiAcmeAccount); + + const create = async (data: TPkiAcmeAccountsInsert, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).insert(data).returning("*"); + const [account] = result; + + if (!account) { + throw new Error("Failed to create PKI ACME account"); + } + + return account; + } catch (error) { + throw new DatabaseError({ error, name: "Create PKI ACME account" }); + } + }; + + const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*"); + const [account] = result; + + if (!account) { + return null; + } + + return account; + } catch (error) { + throw new DatabaseError({ error, name: "Update PKI ACME account" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by id" }); + } + }; + + const findByProfileId = async (profileId: string, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" }); + } + }; + + const findByPublicKey = async (publicKey: unknown, tx?: Knex) => { + try { + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey }).first(); + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME account by public key" }); + } + }; + + const findManyByProfileId = async (profileId: string, tx?: Knex) => { + try { + const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }); + + return accounts; + } catch (error) { + throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" }); + } + }; + + const deleteById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*"); + const [account] = result; + + return account || null; + } catch (error) { + throw new DatabaseError({ error, name: "Delete PKI ACME account by id" }); + } + }; + + return { + ...pkiAcmeAccountOrm, + create, + updateById, + findById, + findByProfileId, + findByPublicKey, + findManyByProfileId, + deleteById + }; +}; 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 16862ca4a..599203dae 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -22,7 +22,7 @@ export const RawJwsPayloadSchema = z.object({ export const JwsPayloadSchema = z.object({ protectedHeader: ProtectedHeaderSchema, - payload: z.any() + payload: z.unknown() }); // Directory endpoint @@ -71,8 +71,7 @@ export const CreateAcmeAccountSchema = z.object({ export const CreateAcmeAccountResponseSchema = z.object({ status: z.string(), contact: z.array(z.string()).optional(), - orders: z.string().optional(), - accountUrl: z.string() + orders: z.string().optional() }); // New Order payload schema diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 78ed98369..7f1c07e7f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -3,7 +3,7 @@ import { NotFoundError } from "@app/lib/errors"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; +import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors"; import { EnrollmentType, @@ -12,6 +12,7 @@ import { import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { + TAcmeResponse, TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, @@ -24,17 +25,23 @@ import { TGetAcmeDirectoryResponse, TGetAcmeOrderResponse, TJwsPayload, + TJwsPayloadWithJwk, TListAcmeOrdersResponse, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse } from "./pki-acme-types"; +import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; + pkiAcmeAccountDAL: Pick; }; -export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { +export const pkiAcmeServiceFactory = ({ + certificateProfileDAL, + pkiAcmeAccountDAL +}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); if (!profile) { @@ -52,7 +59,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService return `${baseUrl}${path}`; }; - const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { + const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise => { const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { @@ -72,10 +79,11 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService if (!success) { throw new AcmeMalformedError({ detail: "Invalid protected header" }); } + const decoder = new TextDecoder(); const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; // TODO: also consume the nonce here - return { payload, protectedHeader }; + return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey }; }; const getAcmeDirectory = async (profileId: string): Promise => { @@ -96,18 +104,47 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService const createAcmeAccount = async ( profileId: string, + jwk: JWK, payload: TCreateAcmeAccountPayload - ): Promise => { + ): Promise> => { const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME new account registration - // Use EAB authentication to find corresponding Infisical machine identity - // Check permissions and return account information - const accountId = "FIXME-account-id"; + // TODO: the jwk as json obj may not be the best idea for indexing. + // Maybe we should find a way to serialize the jwk deterministically. + let account = await pkiAcmeAccountDAL.findByPublicKey(jwk); + if (payload.onlyReturnExisting && !account) { + throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); + } + if (account) { + // With the same public key, we found an existing account, just return it + return { + status: 200, + payload: { + status: "valid", + contact: account.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + } + }; + } + + account = await pkiAcmeAccountDAL.create({ + profileId, + publicKey: jwk, + emails: payload.contact ?? [] + }); + // TODO: check EAB authentication here return { - status: "valid", - accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`), - contact: [], - orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) + status: 201, + payload: { + status: "valid", + contact: account.emails, + orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`) + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`) + } }; }; @@ -116,6 +153,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService payload: TCreateAcmeOrderPayload ): Promise => { const profile = await validateAcmeProfile(profileId); + // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index ffe5df0d3..9009bc0a7 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { JWK } from "jose"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -39,11 +40,24 @@ export type TCreateAcmeOrderPayload = z.infer; export type TDeactivateAcmeAccountPayload = z.infer; export type TFinalizeAcmeOrderPayload = z.infer; +export type TJwsPayloadWithJwk = TJwsPayload & { + jwk: JsonWebKey; +}; +export type TAcmeResponse = { + status: number; + headers: Record; + payload: TPayload; +}; + export type TPkiAcmeServiceFactory = { - validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; + validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; - createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise; + createAcmeAccount: ( + profileId: string, + jwk: JsonWebKey, + body: TCreateAcmeAccountPayload + ) => Promise>; createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise; deactivateAcmeAccount: ( profileId: string,