Add challenge

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:19:06 -08:00
parent b50d7157b9
commit 7b15dee2e6
5 changed files with 47 additions and 20 deletions
@@ -18,6 +18,7 @@ Feature: Order
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+)
Then the value order.authorizations[0].body with jq .status should be equal to "pending" Then the value order.authorizations[0].body with jq .status should be equal to "pending"
Then the value order.authorizations[0].body with jq .challenge should be equal to "pending"
Then the value order.authorizations[0].body with jq .identifier should be equal to json Then the value order.authorizations[0].body with jq .identifier should be equal to json
""" """
{ {
+10 -16
View File
@@ -186,8 +186,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: DeactivateAcmeAccountResponseSchema 200: DeactivateAcmeAccountResponseSchema
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { payload, profileId, accountId } = await validateExistingAccount({ const { payload, profileId, accountId } = await validateExistingAccount({
req, req,
@@ -223,8 +221,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
201: AcmeOrderResourceSchema 201: AcmeOrderResourceSchema
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ const { profileId, accountId, payload } = await validateExistingAccount({
req, req,
@@ -262,8 +258,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: AcmeOrderResourceSchema 200: AcmeOrderResourceSchema
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId } = await validateExistingAccount({ const { profileId, accountId } = await validateExistingAccount({
req, req,
@@ -301,8 +295,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: AcmeOrderResourceSchema 200: AcmeOrderResourceSchema
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ const { profileId, accountId, payload } = await validateExistingAccount({
req, req,
@@ -378,8 +370,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: z.string() 200: z.string()
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId } = await validateExistingAccount({ const { profileId, accountId } = await validateExistingAccount({
req, req,
@@ -413,8 +403,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: GetAcmeAuthorizationResponseSchema 200: GetAcmeAuthorizationResponseSchema
} }
}, },
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ req }); const { profileId, accountId, payload } = await validateExistingAccount({ req });
if (payload !== "") { if (payload !== "") {
@@ -451,10 +439,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
200: RespondToAcmeChallengeResponseSchema 200: RespondToAcmeChallengeResponseSchema
} }
}, },
// TODO: replace with verify ACME signature here instead handler: async (req, res) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), const { profileId, accountId, payload } = await validateExistingAccount({ req });
handler: async (req) => { if (payload !== "") {
const challenge = await server.services.pkiAcme.respondToAcmeChallenge(req.params.profileId, req.params.authzId); throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId })
);
return challenge; return challenge;
} }
}); });
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TPkiAcmeChallengeDALFactory = ReturnType<typeof pkiAcmeChallengeDALFactory>;
export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge);
return {
...pkiAcmeChallengeOrm
};
};
@@ -21,6 +21,12 @@ export enum AcmeAuthStatus {
Revoked = "revoked" Revoked = "revoked"
} }
export enum AcmeChallengeType {
HTTP_01 = "http-01",
DNS_01 = "dns-01",
TLS_ALPN_01 = "tls-alpn-01"
}
export const ProtectedHeaderSchema = z export const ProtectedHeaderSchema = z
.object({ .object({
alg: z.string(), alg: z.string(),
@@ -136,7 +142,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({
}), }),
challenges: z.array( challenges: z.array(
z.object({ z.object({
type: z.string(), type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]),
url: z.string(), url: z.string(),
status: z.string(), status: z.string(),
token: z.string(), token: z.string(),
@@ -146,7 +152,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({
}); });
export const RespondToAcmeChallengeResponseSchema = z.object({ export const RespondToAcmeChallengeResponseSchema = z.object({
type: z.string(), type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]),
url: z.string(), url: z.string(),
status: z.string(), status: z.string(),
token: z.string(), token: z.string(),
@@ -15,6 +15,7 @@ import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderPar
import { z, ZodError } from "zod"; import { z, ZodError } from "zod";
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
import { import {
AcmeAccountDoesNotExistError, AcmeAccountDoesNotExistError,
AcmeBadPublicKeyError, AcmeBadPublicKeyError,
@@ -28,6 +29,7 @@ import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { import {
AcmeAuthStatus, AcmeAuthStatus,
AcmeChallengeType,
AcmeIdentifierType, AcmeIdentifierType,
AcmeOrderStatus, AcmeOrderStatus,
CreateAcmeAccountBodySchema, CreateAcmeAccountBodySchema,
@@ -58,6 +60,7 @@ type TPkiAcmeServiceFactoryDep = {
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">; acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">;
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">; acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">; acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">;
acmeChallengeDAL: Pick<TPkiAcmeChallengeDALFactory, "create">;
}; };
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
@@ -65,7 +68,8 @@ export const pkiAcmeServiceFactory = ({
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
acmeOrderAuthDAL acmeOrderAuthDAL,
acmeChallengeDAL
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { }: 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);
@@ -376,7 +380,7 @@ export const pkiAcmeServiceFactory = ({
payload.identifiers.map(async (identifier) => { payload.identifiers.map(async (identifier) => {
if (identifier.type === AcmeIdentifierType.DNS) { if (identifier.type === AcmeIdentifierType.DNS) {
// TODO: reuse existing authorizations for this identifier if they exist // TODO: reuse existing authorizations for this identifier if they exist
return await acmeAuthDAL.create( const auth = await acmeAuthDAL.create(
{ {
accountId: account.id, accountId: account.id,
status: AcmeAuthStatus.Pending, status: AcmeAuthStatus.Pending,
@@ -391,6 +395,15 @@ export const pkiAcmeServiceFactory = ({
}, },
tx tx
); );
// TODO: support other challenge types here. Currently only HTTP-01 is supported.
await acmeChallengeDAL.create(
{
authId: auth.id,
type: AcmeChallengeType.HTTP_01
},
tx
);
return auth;
} else { } else {
throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" });
} }