Implement account creation

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:08 -08:00
parent a29029c192
commit 4883c3ead3
4 changed files with 174 additions and 18 deletions
@@ -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<typeof pkiAcmeAccountDALFactory>;
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
};
};
@@ -22,7 +22,7 @@ export const RawJwsPayloadSchema = z.object({
export const JwsPayloadSchema = z.object({ export const JwsPayloadSchema = z.object({
protectedHeader: ProtectedHeaderSchema, protectedHeader: ProtectedHeaderSchema,
payload: z.any() payload: z.unknown()
}); });
// Directory endpoint // Directory endpoint
@@ -71,8 +71,7 @@ export const CreateAcmeAccountSchema = z.object({
export const CreateAcmeAccountResponseSchema = z.object({ export const CreateAcmeAccountResponseSchema = z.object({
status: z.string(), status: z.string(),
contact: z.array(z.string()).optional(), contact: z.array(z.string()).optional(),
orders: z.string().optional(), orders: z.string().optional()
accountUrl: z.string()
}); });
// New Order payload schema // New Order payload schema
@@ -3,7 +3,7 @@ import { NotFoundError } from "@app/lib/errors";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; 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 { import {
EnrollmentType, EnrollmentType,
@@ -12,6 +12,7 @@ import {
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose"; import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
import { ProtectedHeaderSchema } from "./pki-acme-schemas"; import { ProtectedHeaderSchema } from "./pki-acme-schemas";
import { import {
TAcmeResponse,
TCreateAcmeAccountPayload, TCreateAcmeAccountPayload,
TCreateAcmeAccountResponse, TCreateAcmeAccountResponse,
TCreateAcmeOrderPayload, TCreateAcmeOrderPayload,
@@ -24,17 +25,23 @@ import {
TGetAcmeDirectoryResponse, TGetAcmeDirectoryResponse,
TGetAcmeOrderResponse, TGetAcmeOrderResponse,
TJwsPayload, TJwsPayload,
TJwsPayloadWithJwk,
TListAcmeOrdersResponse, TListAcmeOrdersResponse,
TPkiAcmeServiceFactory, TPkiAcmeServiceFactory,
TRawJwsPayload, TRawJwsPayload,
TRespondToAcmeChallengeResponse TRespondToAcmeChallengeResponse
} from "./pki-acme-types"; } from "./pki-acme-types";
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
pkiAcmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">;
}; };
export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { export const pkiAcmeServiceFactory = ({
certificateProfileDAL,
pkiAcmeAccountDAL
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => { const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
const profile = await certificateProfileDAL.findById(profileId); const profile = await certificateProfileDAL.findById(profileId);
if (!profile) { if (!profile) {
@@ -52,7 +59,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
return `${baseUrl}${path}`; return `${baseUrl}${path}`;
}; };
const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayload> => { const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayloadWithJwk> => {
const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify( const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify(
rawJwsPayload, rawJwsPayload,
async (protectedHeader: JWSHeaderParameters | undefined) => { async (protectedHeader: JWSHeaderParameters | undefined) => {
@@ -72,10 +79,11 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
if (!success) { if (!success) {
throw new AcmeMalformedError({ detail: "Invalid protected header" }); throw new AcmeMalformedError({ detail: "Invalid protected header" });
} }
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload; const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload;
// TODO: also consume the nonce here // TODO: also consume the nonce here
return { payload, protectedHeader }; return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey };
}; };
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => { const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
@@ -96,18 +104,47 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
const createAcmeAccount = async ( const createAcmeAccount = async (
profileId: string, profileId: string,
jwk: JWK,
payload: TCreateAcmeAccountPayload payload: TCreateAcmeAccountPayload
): Promise<TCreateAcmeAccountResponse> => { ): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
// FIXME: Implement ACME new account registration // TODO: the jwk as json obj may not be the best idea for indexing.
// Use EAB authentication to find corresponding Infisical machine identity // Maybe we should find a way to serialize the jwk deterministically.
// Check permissions and return account information let account = await pkiAcmeAccountDAL.findByPublicKey(jwk);
const accountId = "FIXME-account-id"; 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 { return {
status: "valid", status: 201,
accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`), payload: {
contact: [], status: "valid",
orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`) 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 payload: TCreateAcmeOrderPayload
): Promise<TCreateAcmeOrderResponse> => { ): Promise<TCreateAcmeOrderResponse> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
// FIXME: Implement ACME new order creation // FIXME: Implement ACME new order creation
const orderId = "FIXME-order-id"; const orderId = "FIXME-order-id";
return { return {
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { JWK } from "jose";
import { import {
CreateAcmeAccountBodySchema, CreateAcmeAccountBodySchema,
CreateAcmeAccountResponseSchema, CreateAcmeAccountResponseSchema,
@@ -39,11 +40,24 @@ export type TCreateAcmeOrderPayload = z.infer<typeof CreateAcmeOrderBodySchema>;
export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>; export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>;
export type TFinalizeAcmeOrderPayload = z.infer<typeof FinalizeAcmeOrderBodySchema>; export type TFinalizeAcmeOrderPayload = z.infer<typeof FinalizeAcmeOrderBodySchema>;
export type TJwsPayloadWithJwk = TJwsPayload & {
jwk: JsonWebKey;
};
export type TAcmeResponse<TPayload> = {
status: number;
headers: Record<string, string>;
payload: TPayload;
};
export type TPkiAcmeServiceFactory = { export type TPkiAcmeServiceFactory = {
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayload>; validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayloadWithJwk>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>; getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
getAcmeNewNonce: (profileId: string) => Promise<string>; getAcmeNewNonce: (profileId: string) => Promise<string>;
createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise<TCreateAcmeAccountResponse>; createAcmeAccount: (
profileId: string,
jwk: JsonWebKey,
body: TCreateAcmeAccountPayload
) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise<TCreateAcmeOrderResponse>; createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise<TCreateAcmeOrderResponse>;
deactivateAcmeAccount: ( deactivateAcmeAccount: (
profileId: string, profileId: string,