Refactor the code a bit

This commit is contained in:
Fang-Pen Lin
2025-10-30 17:33:42 -07:00
parent e2799b934a
commit 744b02ee00
5 changed files with 136 additions and 45 deletions

View File

@@ -0,0 +1,77 @@
Feature: Order
Scenario: Create a new order
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"ORGANIZATION_NAME": "Infisical Inc",
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then the value order.uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)
Then the value order.body with jq .status should be equal to "pending"
Then the value order.body with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}]
Then the value order.body with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
Then the value order.body with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true
Scenario: Create a new order with SANs
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"ORGANIZATION_NAME": "Infisical Inc",
"COMMON_NAME": "localhost"
}
"""
Then I add subject alternative name to certificate signing request csr
"""
[
"example.com",
"infisical.com"
]
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then the value order.body with jq .identifiers | sort_by(.value) should be equal to json
"""
[
{"type": "dns", "value": "example.com"},
{"type": "dns", "value": "infisical.com"},
{"type": "dns", "value": "localhost"}
]
"""
Scenario: Fetch an order
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id {acme_profile.eab_kid} with secret {acme_profile.eab_secret} as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"ORGANIZATION_NAME": "Infisical Inc",
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I send an ACME post-as-get to order.uri as fetched_order
Then the value fetched_order with jq .status should be equal to "pending"
Then the value fetched_order with jq .identifiers should be equal to [{"type": "dns", "value": "localhost"}]
Then the value fetched_order with jq .finalize should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
Then the value fetched_order with jq all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/")) should be equal to true

View File

@@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */
import type { TAcmeResponse } from "@app/ee/services/pki-acme/pki-acme-types"; import type { TAcmeResponse, TAuthenciatedJwsPayload, TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types";
import { FastifyReply } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
import { import {
AcmeOrderResourceSchema, AcmeOrderResourceSchema,
CreateAcmeAccountResponseSchema, CreateAcmeAccountResponseSchema,
@@ -19,9 +20,27 @@ import {
} from "@app/ee/services/pki-acme/pki-acme-schemas"; } from "@app/ee/services/pki-acme/pki-acme-schemas";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { AcmeAccountDoesNotExistError, AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
const validateExistingAccount = async <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>({
req,
schema
}: {
req: FastifyRequest<{ Params: { profileId: string; accountId?: string }; Body: TRawJwsPayload }>;
schema?: TSchema;
}): Promise<TAuthenciatedJwsPayload<T>> => {
return await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: new URL(req.url, `${req.protocol}://${req.hostname}`),
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema,
expectedAccountId: req.params.accountId
});
};
const sendAcmeResponse = async <T>(res: FastifyReply, profileId: string, response: TAcmeResponse<T>): Promise<T> => { const sendAcmeResponse = async <T>(res: FastifyReply, profileId: string, response: TAcmeResponse<T>): Promise<T> => {
res.code(response.status); res.code(response.status);
for (const [key, value] of Object.entries(response.headers)) { for (const [key, value] of Object.entries(response.headers)) {
@@ -163,12 +182,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { payload, profileId, accountId } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId, schema: DeactivateAcmeAccountBodySchema
rawJwsPayload: req.body,
schema: DeactivateAcmeAccountBodySchema,
expectedAccountId: req.params.accountId
}); });
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -205,10 +221,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: CreateAcmeOrderBodySchema schema: CreateAcmeOrderBodySchema
}); });
return sendAcmeResponse( return sendAcmeResponse(
@@ -247,10 +261,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId, schema: FinalizeAcmeOrderBodySchema
rawJwsPayload: req.body
}); });
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -288,10 +301,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: FinalizeAcmeOrderBodySchema schema: FinalizeAcmeOrderBodySchema
}); });
return sendAcmeResponse( return sendAcmeResponse(
@@ -330,12 +341,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId, schema: ListAcmeOrdersPayloadSchema
rawJwsPayload: req.body,
schema: ListAcmeOrdersPayloadSchema,
expectedAccountId: req.params.accountId
}); });
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -372,10 +380,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId, schema: FinalizeAcmeOrderBodySchema
rawJwsPayload: req.body
}); });
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -409,10 +416,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await validateExistingAccount({
url: req.url, req,
profileId: req.params.profileId, schema: GetAcmeAuthorizationBodySchema
rawJwsPayload: req.body
}); });
if (payload !== "") { if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" }); throw new AcmeMalformedError({ detail: "Payload should be empty" });

View File

@@ -6,6 +6,7 @@ import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { TPkiAcmeChallenges } from "@app/db/schemas";
import { import {
EnrollmentType, EnrollmentType,
TCertificateProfileWithConfigs TCertificateProfileWithConfigs
@@ -17,6 +18,7 @@ import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { import {
AcmeAccountDoesNotExistError, AcmeAccountDoesNotExistError,
AcmeBadPublicKeyError, AcmeBadPublicKeyError,
AcmeError,
AcmeMalformedError, AcmeMalformedError,
AcmeServerInternalError, AcmeServerInternalError,
AcmeUnauthorizedError, AcmeUnauthorizedError,
@@ -49,7 +51,6 @@ import {
TRawJwsPayload, TRawJwsPayload,
TRespondToAcmeChallengeResponse TRespondToAcmeChallengeResponse
} from "./pki-acme-types"; } from "./pki-acme-types";
import { TPkiAcmeChallenges } from "@app/db/schemas";
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
@@ -100,7 +101,7 @@ export const pkiAcmeServiceFactory = ({
getJWK, getJWK,
schema schema
}: { }: {
url: string; url: URL;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>; getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>;
schema?: TSchema; schema?: TSchema;
@@ -115,6 +116,9 @@ export const pkiAcmeServiceFactory = ({
return await importJWK(jwk, protectedHeader.alg); return await importJWK(jwk, protectedHeader.alg);
}); });
} catch (error) { } catch (error) {
if (error instanceof AcmeError) {
throw error;
}
if (error instanceof ZodError) { if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` });
} }
@@ -127,7 +131,7 @@ export const pkiAcmeServiceFactory = ({
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try { try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
if (protectedHeader.url !== url) { if (new URL(protectedHeader.url).href !== url.href) {
throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" });
} }
// TODO: consume the nonce here // TODO: consume the nonce here
@@ -139,6 +143,9 @@ export const pkiAcmeServiceFactory = ({
payload payload
}; };
} catch (error) { } catch (error) {
if (error instanceof AcmeError) {
throw error;
}
if (error instanceof ZodError) { if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` });
} }
@@ -151,7 +158,7 @@ export const pkiAcmeServiceFactory = ({
url, url,
rawJwsPayload rawJwsPayload
}: { }: {
url: string; url: URL;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
}): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => { }): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => {
return await validateJwsPayload({ return await validateJwsPayload({
@@ -177,7 +184,7 @@ export const pkiAcmeServiceFactory = ({
schema, schema,
expectedAccountId expectedAccountId
}: { }: {
url: string; url: URL;
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema?: TSchema; schema?: TSchema;

View File

@@ -58,16 +58,16 @@ export type TPkiAcmeServiceFactory = {
getJWK, getJWK,
schema schema
}: { }: {
url: string; url: URL;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>; getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>;
schema?: z.ZodSchema<any>; schema?: TSchema;
}) => Promise<TJwsPayload<any>>; }) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: ({ validateNewAccountJwsPayload: ({
url, url,
rawJwsPayload rawJwsPayload
}: { }: {
url: string; url: URL;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
}) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>; }) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: < validateExistingAccountJwsPayload: <
@@ -80,7 +80,7 @@ export type TPkiAcmeServiceFactory = {
schema, schema,
expectedAccountId expectedAccountId
}: { }: {
url: string; url: URL;
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema?: TSchema; schema?: TSchema;

View File

@@ -248,6 +248,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
.type("application/problem+json") .type("application/problem+json")
.status(error.status) .status(error.status)
.send({ .send({
reqId: req.id,
status: error.status, status: error.status,
type: `urn:ietf:params:acme:error:${error.type}`, type: `urn:ietf:params:acme:error:${error.type}`,
detail: error.detail detail: error.detail