feat(server): add an embedded ACME directory server and certificate authority with challenge, order, and certificate endpoints
This commit is contained in:
93
ts_server/server.handlers.finalize.ts
Normal file
93
ts_server/server.handlers.finalize.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type * as http from 'node:http';
|
||||
import type { JwsVerifier } from './server.classes.jws.verifier.js';
|
||||
import { AcmeServerError } from './server.classes.jws.verifier.js';
|
||||
import type { IServerOrderStore } from './server.interfaces.js';
|
||||
import type { AcmeServerCA } from './server.classes.ca.js';
|
||||
|
||||
/**
|
||||
* POST /finalize/:id — Submit CSR and issue certificate.
|
||||
*/
|
||||
export function createFinalizeHandler(
|
||||
baseUrl: string,
|
||||
jwsVerifier: JwsVerifier,
|
||||
orderStore: IServerOrderStore,
|
||||
ca: AcmeServerCA,
|
||||
) {
|
||||
return async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
params: Record<string, string>,
|
||||
body: any,
|
||||
): Promise<void> => {
|
||||
const orderId = params.id;
|
||||
const requestUrl = `${baseUrl}/finalize/${orderId}`;
|
||||
const verified = await jwsVerifier.verify(body, requestUrl);
|
||||
|
||||
if (!verified.kid) {
|
||||
throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'Finalize requires kid');
|
||||
}
|
||||
|
||||
const order = await orderStore.getOrder(orderId);
|
||||
if (!order) {
|
||||
throw new AcmeServerError(404, 'urn:ietf:params:acme:error:malformed', 'Order not found');
|
||||
}
|
||||
|
||||
// Check all authorizations are valid and update order status if needed
|
||||
if (order.status === 'pending') {
|
||||
let allValid = true;
|
||||
for (const authzId of order.authorizationIds) {
|
||||
const authz = await orderStore.getAuthorization(authzId);
|
||||
if (!authz || authz.status !== 'valid') {
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allValid) {
|
||||
await orderStore.updateOrder(orderId, { status: 'ready' });
|
||||
order.status = 'ready';
|
||||
}
|
||||
}
|
||||
|
||||
if (order.status !== 'ready') {
|
||||
throw new AcmeServerError(
|
||||
403,
|
||||
'urn:ietf:params:acme:error:orderNotReady',
|
||||
`Order is in "${order.status}" state, expected "ready"`,
|
||||
);
|
||||
}
|
||||
|
||||
const { payload } = verified;
|
||||
if (!payload?.csr) {
|
||||
throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'Missing CSR in finalize request');
|
||||
}
|
||||
|
||||
// Transition to processing
|
||||
await orderStore.updateOrder(orderId, { status: 'processing' });
|
||||
|
||||
// Sign the certificate
|
||||
const certPem = await ca.signCsr(payload.csr);
|
||||
|
||||
// Store certificate and update order
|
||||
const certUrl = `${baseUrl}/cert/${orderId}`;
|
||||
await orderStore.storeCertPem(orderId, certPem);
|
||||
await orderStore.updateOrder(orderId, {
|
||||
status: 'valid',
|
||||
certificate: certUrl,
|
||||
});
|
||||
|
||||
const responseBody = {
|
||||
status: 'valid',
|
||||
expires: order.expires,
|
||||
identifiers: order.identifiers,
|
||||
authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`),
|
||||
finalize: order.finalize,
|
||||
certificate: certUrl,
|
||||
};
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Location': `${baseUrl}/order/${orderId}`,
|
||||
});
|
||||
res.end(JSON.stringify(responseBody));
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user