@push.rocks/smartacme 9.1.2 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/acme.classes.account.js +1 -1
- package/dist_ts/acme/acme.classes.challenge.js +1 -1
- package/dist_ts/acme/acme.classes.client.js +1 -1
- package/dist_ts/acme/acme.classes.crypto.d.ts +1 -1
- package/dist_ts/acme/acme.classes.crypto.js +1 -1
- package/dist_ts/acme/acme.classes.directory.js +1 -1
- package/dist_ts/acme/acme.classes.error.js +1 -1
- package/dist_ts/acme/acme.classes.http-client.js +1 -1
- package/dist_ts/acme/acme.classes.order.js +1 -1
- package/dist_ts/acme/acme.interfaces.js +1 -1
- package/dist_ts/acme/index.js +1 -1
- package/dist_ts/certmanagers/index.js +1 -1
- package/dist_ts/certmanagers/memory.js +1 -1
- package/dist_ts/certmanagers/mongo.js +1 -1
- package/dist_ts/handlers/Dns01Handler.js +1 -1
- package/dist_ts/handlers/Http01Handler.js +1 -1
- package/dist_ts/handlers/Http01MemoryHandler.js +1 -1
- package/dist_ts/handlers/IChallengeHandler.js +1 -1
- package/dist_ts/handlers/index.js +1 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/interfaces/accountdata.js +1 -1
- package/dist_ts/interfaces/certmanager.js +1 -1
- package/dist_ts/interfaces/index.js +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts/smartacme.classes.cert.js +1 -1
- package/dist_ts/smartacme.classes.certmatcher.js +1 -1
- package/dist_ts/smartacme.classes.smartacme.js +2 -1
- package/dist_ts_server/index.d.ts +4 -0
- package/dist_ts_server/index.js +4 -0
- package/dist_ts_server/server.classes.account.store.d.ts +12 -0
- package/dist_ts_server/server.classes.account.store.js +25 -0
- package/dist_ts_server/server.classes.acmeserver.d.ts +22 -0
- package/dist_ts_server/server.classes.acmeserver.js +100 -0
- package/dist_ts_server/server.classes.ca.d.ts +25 -0
- package/dist_ts_server/server.classes.ca.js +109 -0
- package/dist_ts_server/server.classes.challenge.verifier.d.ts +16 -0
- package/dist_ts_server/server.classes.challenge.verifier.js +56 -0
- package/dist_ts_server/server.classes.jws.verifier.d.ts +31 -0
- package/dist_ts_server/server.classes.jws.verifier.js +116 -0
- package/dist_ts_server/server.classes.nonce.d.ts +12 -0
- package/dist_ts_server/server.classes.nonce.js +32 -0
- package/dist_ts_server/server.classes.order.store.d.ts +21 -0
- package/dist_ts_server/server.classes.order.store.js +57 -0
- package/dist_ts_server/server.classes.router.d.ts +17 -0
- package/dist_ts_server/server.classes.router.js +99 -0
- package/dist_ts_server/server.handlers.account.d.ts +8 -0
- package/dist_ts_server/server.handlers.account.js +59 -0
- package/dist_ts_server/server.handlers.authz.d.ts +7 -0
- package/dist_ts_server/server.handlers.authz.js +41 -0
- package/dist_ts_server/server.handlers.cert.d.ts +7 -0
- package/dist_ts_server/server.handlers.cert.js +18 -0
- package/dist_ts_server/server.handlers.challenge.d.ts +10 -0
- package/dist_ts_server/server.handlers.challenge.js +110 -0
- package/dist_ts_server/server.handlers.directory.d.ts +5 -0
- package/dist_ts_server/server.handlers.directory.js +23 -0
- package/dist_ts_server/server.handlers.finalize.d.ts +8 -0
- package/dist_ts_server/server.handlers.finalize.js +65 -0
- package/dist_ts_server/server.handlers.nonce.d.ts +7 -0
- package/dist_ts_server/server.handlers.nonce.js +17 -0
- package/dist_ts_server/server.handlers.order.d.ts +11 -0
- package/dist_ts_server/server.handlers.order.js +131 -0
- package/dist_ts_server/server.interfaces.d.ts +72 -0
- package/dist_ts_server/server.interfaces.js +2 -0
- package/package.json +11 -11
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme.classes.crypto.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/smartacme.classes.smartacme.ts +1 -0
- package/ts_server/index.ts +12 -0
- package/ts_server/server.classes.account.store.ts +27 -0
- package/ts_server/server.classes.acmeserver.ts +128 -0
- package/ts_server/server.classes.ca.ts +142 -0
- package/ts_server/server.classes.challenge.verifier.ts +61 -0
- package/ts_server/server.classes.jws.verifier.ts +153 -0
- package/ts_server/server.classes.nonce.ts +36 -0
- package/ts_server/server.classes.order.store.ts +72 -0
- package/ts_server/server.classes.router.ts +116 -0
- package/ts_server/server.handlers.account.ts +85 -0
- package/ts_server/server.handlers.authz.ts +58 -0
- package/ts_server/server.handlers.cert.ts +32 -0
- package/ts_server/server.handlers.challenge.ts +142 -0
- package/ts_server/server.handlers.directory.ts +30 -0
- package/ts_server/server.handlers.finalize.ts +93 -0
- package/ts_server/server.handlers.nonce.ts +29 -0
- package/ts_server/server.handlers.order.ts +177 -0
- package/ts_server/server.interfaces.ts +98 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { AcmeServerError } from './server.classes.jws.verifier.js';
|
|
3
|
+
/**
|
|
4
|
+
* POST /new-order — Create a new ACME order.
|
|
5
|
+
*/
|
|
6
|
+
export function createNewOrderHandler(baseUrl, jwsVerifier, orderStore) {
|
|
7
|
+
return async (req, res, _params, body) => {
|
|
8
|
+
const requestUrl = `${baseUrl}/new-order`;
|
|
9
|
+
const verified = await jwsVerifier.verify(body, requestUrl);
|
|
10
|
+
if (!verified.kid) {
|
|
11
|
+
throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'newOrder requires kid');
|
|
12
|
+
}
|
|
13
|
+
const { payload } = verified;
|
|
14
|
+
const identifiers = payload?.identifiers;
|
|
15
|
+
if (!identifiers || !Array.isArray(identifiers) || identifiers.length === 0) {
|
|
16
|
+
throw new AcmeServerError(400, 'urn:ietf:params:acme:error:malformed', 'Order must include at least one identifier');
|
|
17
|
+
}
|
|
18
|
+
const orderId = crypto.randomBytes(16).toString('hex');
|
|
19
|
+
const expires = new Date();
|
|
20
|
+
expires.setDate(expires.getDate() + 7);
|
|
21
|
+
// Create authorizations and challenges for each identifier
|
|
22
|
+
const authorizationIds = [];
|
|
23
|
+
for (const identifier of identifiers) {
|
|
24
|
+
const authzId = crypto.randomBytes(16).toString('hex');
|
|
25
|
+
const isWildcard = identifier.value.startsWith('*.');
|
|
26
|
+
const domain = isWildcard ? identifier.value.slice(2) : identifier.value;
|
|
27
|
+
// Create challenges for this authorization
|
|
28
|
+
const challengeIds = [];
|
|
29
|
+
// HTTP-01 challenge (not for wildcards)
|
|
30
|
+
if (!isWildcard) {
|
|
31
|
+
const http01Id = crypto.randomBytes(16).toString('hex');
|
|
32
|
+
const http01Token = crypto.randomBytes(32).toString('base64url');
|
|
33
|
+
await orderStore.createChallenge({
|
|
34
|
+
id: http01Id,
|
|
35
|
+
authorizationId: authzId,
|
|
36
|
+
type: 'http-01',
|
|
37
|
+
token: http01Token,
|
|
38
|
+
status: 'pending',
|
|
39
|
+
});
|
|
40
|
+
challengeIds.push(http01Id);
|
|
41
|
+
}
|
|
42
|
+
// DNS-01 challenge (always)
|
|
43
|
+
const dns01Id = crypto.randomBytes(16).toString('hex');
|
|
44
|
+
const dns01Token = crypto.randomBytes(32).toString('base64url');
|
|
45
|
+
await orderStore.createChallenge({
|
|
46
|
+
id: dns01Id,
|
|
47
|
+
authorizationId: authzId,
|
|
48
|
+
type: 'dns-01',
|
|
49
|
+
token: dns01Token,
|
|
50
|
+
status: 'pending',
|
|
51
|
+
});
|
|
52
|
+
challengeIds.push(dns01Id);
|
|
53
|
+
await orderStore.createAuthorization({
|
|
54
|
+
id: authzId,
|
|
55
|
+
orderId,
|
|
56
|
+
identifier: { type: 'dns', value: domain },
|
|
57
|
+
status: 'pending',
|
|
58
|
+
expires: expires.toISOString(),
|
|
59
|
+
challengeIds,
|
|
60
|
+
wildcard: isWildcard || undefined,
|
|
61
|
+
});
|
|
62
|
+
authorizationIds.push(authzId);
|
|
63
|
+
}
|
|
64
|
+
const order = await orderStore.createOrder({
|
|
65
|
+
id: orderId,
|
|
66
|
+
accountUrl: verified.kid,
|
|
67
|
+
status: 'pending',
|
|
68
|
+
identifiers,
|
|
69
|
+
authorizationIds,
|
|
70
|
+
expires: expires.toISOString(),
|
|
71
|
+
finalize: `${baseUrl}/finalize/${orderId}`,
|
|
72
|
+
});
|
|
73
|
+
const responseBody = {
|
|
74
|
+
status: order.status,
|
|
75
|
+
expires: order.expires,
|
|
76
|
+
identifiers: order.identifiers,
|
|
77
|
+
authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`),
|
|
78
|
+
finalize: order.finalize,
|
|
79
|
+
};
|
|
80
|
+
res.writeHead(201, {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'Location': `${baseUrl}/order/${orderId}`,
|
|
83
|
+
});
|
|
84
|
+
res.end(JSON.stringify(responseBody));
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* POST /order/:id — Poll order status (POST-as-GET).
|
|
89
|
+
*/
|
|
90
|
+
export function createOrderPollHandler(baseUrl, jwsVerifier, orderStore) {
|
|
91
|
+
return async (req, res, params, body) => {
|
|
92
|
+
const orderId = params.id;
|
|
93
|
+
const requestUrl = `${baseUrl}/order/${orderId}`;
|
|
94
|
+
await jwsVerifier.verify(body, requestUrl);
|
|
95
|
+
const order = await orderStore.getOrder(orderId);
|
|
96
|
+
if (!order) {
|
|
97
|
+
throw new AcmeServerError(404, 'urn:ietf:params:acme:error:malformed', 'Order not found');
|
|
98
|
+
}
|
|
99
|
+
// Check if all authorizations are valid → transition to ready
|
|
100
|
+
if (order.status === 'pending') {
|
|
101
|
+
let allValid = true;
|
|
102
|
+
for (const authzId of order.authorizationIds) {
|
|
103
|
+
const authz = await orderStore.getAuthorization(authzId);
|
|
104
|
+
if (!authz || authz.status !== 'valid') {
|
|
105
|
+
allValid = false;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (allValid) {
|
|
110
|
+
await orderStore.updateOrder(orderId, { status: 'ready' });
|
|
111
|
+
order.status = 'ready';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const responseBody = {
|
|
115
|
+
status: order.status,
|
|
116
|
+
expires: order.expires,
|
|
117
|
+
identifiers: order.identifiers,
|
|
118
|
+
authorizations: order.authorizationIds.map((id) => `${baseUrl}/authz/${id}`),
|
|
119
|
+
finalize: order.finalize,
|
|
120
|
+
};
|
|
121
|
+
if (order.certificate) {
|
|
122
|
+
responseBody.certificate = order.certificate;
|
|
123
|
+
}
|
|
124
|
+
res.writeHead(200, {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
'Location': requestUrl,
|
|
127
|
+
});
|
|
128
|
+
res.end(JSON.stringify(responseBody));
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLmhhbmRsZXJzLm9yZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfc2VydmVyL3NlcnZlci5oYW5kbGVycy5vcmRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUV0QyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFJbkU7O0dBRUc7QUFDSCxNQUFNLFVBQVUscUJBQXFCLENBQ25DLE9BQWUsRUFDZixXQUF3QixFQUN4QixVQUE2QjtJQUU3QixPQUFPLEtBQUssRUFDVixHQUF5QixFQUN6QixHQUF3QixFQUN4QixPQUErQixFQUMvQixJQUFTLEVBQ00sRUFBRTtRQUNqQixNQUFNLFVBQVUsR0FBRyxHQUFHLE9BQU8sWUFBWSxDQUFDO1FBQzFDLE1BQU0sUUFBUSxHQUFHLE1BQU0sV0FBVyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFFNUQsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsQ0FBQztZQUNsQixNQUFNLElBQUksZUFBZSxDQUFDLEdBQUcsRUFBRSxzQ0FBc0MsRUFBRSx1QkFBdUIsQ0FBQyxDQUFDO1FBQ2xHLENBQUM7UUFFRCxNQUFNLEVBQUUsT0FBTyxFQUFFLEdBQUcsUUFBUSxDQUFDO1FBQzdCLE1BQU0sV0FBVyxHQUFzQixPQUFPLEVBQUUsV0FBVyxDQUFDO1FBRTVELElBQUksQ0FBQyxXQUFXLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxJQUFJLFdBQVcsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDNUUsTUFBTSxJQUFJLGVBQWUsQ0FDdkIsR0FBRyxFQUNILHNDQUFzQyxFQUN0Qyw0Q0FBNEMsQ0FDN0MsQ0FBQztRQUNKLENBQUM7UUFFRCxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQyxD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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { IAcmeIdentifier } from '../dist_ts/acme/acme.interfaces.js';
|
|
2
|
+
export interface IAcmeServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
hostname?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** When false, challenges auto-approve on trigger (useful for testing) */
|
|
7
|
+
challengeVerification?: boolean;
|
|
8
|
+
caOptions?: {
|
|
9
|
+
commonName?: string;
|
|
10
|
+
validityDays?: number;
|
|
11
|
+
certValidityDays?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface IServerAccountStore {
|
|
15
|
+
create(account: IServerAccount): Promise<IServerAccount>;
|
|
16
|
+
getByThumbprint(thumbprint: string): Promise<IServerAccount | null>;
|
|
17
|
+
getByUrl(url: string): Promise<IServerAccount | null>;
|
|
18
|
+
}
|
|
19
|
+
export interface IServerOrderStore {
|
|
20
|
+
createOrder(order: IServerOrder): Promise<IServerOrder>;
|
|
21
|
+
getOrder(id: string): Promise<IServerOrder | null>;
|
|
22
|
+
updateOrder(id: string, updates: Partial<IServerOrder>): Promise<void>;
|
|
23
|
+
createAuthorization(authz: IServerAuthorization): Promise<IServerAuthorization>;
|
|
24
|
+
getAuthorization(id: string): Promise<IServerAuthorization | null>;
|
|
25
|
+
updateAuthorization(id: string, updates: Partial<IServerAuthorization>): Promise<void>;
|
|
26
|
+
createChallenge(challenge: IServerChallenge): Promise<IServerChallenge>;
|
|
27
|
+
getChallenge(id: string): Promise<IServerChallenge | null>;
|
|
28
|
+
updateChallenge(id: string, updates: Partial<IServerChallenge>): Promise<void>;
|
|
29
|
+
storeCertPem(orderId: string, pem: string): Promise<void>;
|
|
30
|
+
getCertPem(orderId: string): Promise<string | null>;
|
|
31
|
+
}
|
|
32
|
+
export interface IServerAccount {
|
|
33
|
+
id: string;
|
|
34
|
+
thumbprint: string;
|
|
35
|
+
url: string;
|
|
36
|
+
jwk: Record<string, string>;
|
|
37
|
+
status: string;
|
|
38
|
+
contact: string[];
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
export interface IServerOrder {
|
|
42
|
+
id: string;
|
|
43
|
+
accountUrl: string;
|
|
44
|
+
status: string;
|
|
45
|
+
identifiers: IAcmeIdentifier[];
|
|
46
|
+
authorizationIds: string[];
|
|
47
|
+
expires: string;
|
|
48
|
+
finalize: string;
|
|
49
|
+
certificate?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface IServerAuthorization {
|
|
52
|
+
id: string;
|
|
53
|
+
orderId: string;
|
|
54
|
+
identifier: IAcmeIdentifier;
|
|
55
|
+
status: string;
|
|
56
|
+
expires: string;
|
|
57
|
+
challengeIds: string[];
|
|
58
|
+
wildcard?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface IServerChallenge {
|
|
61
|
+
id: string;
|
|
62
|
+
authorizationId: string;
|
|
63
|
+
type: string;
|
|
64
|
+
token: string;
|
|
65
|
+
status: string;
|
|
66
|
+
validated?: string;
|
|
67
|
+
error?: {
|
|
68
|
+
type: string;
|
|
69
|
+
detail: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export type TRouteHandler = (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, params: Record<string, string>, body: any) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartacme",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A TypeScript-based ACME client for LetsEncrypt certificate management with a focus on simplicity and power.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "(tstest test/ --verbose --logfile --timeout 600)",
|
|
11
|
-
"build": "(tsbuild)",
|
|
11
|
+
"build": "(tsbuild tsfolders)",
|
|
12
12
|
"buildDocs": "tsdoc"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
@@ -41,28 +41,28 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
|
43
43
|
"@peculiar/x509": "^1.14.3",
|
|
44
|
-
"@push.rocks/lik": "^6.
|
|
45
|
-
"@push.rocks/smartdata": "^7.0
|
|
44
|
+
"@push.rocks/lik": "^6.3.1",
|
|
45
|
+
"@push.rocks/smartdata": "^7.1.0",
|
|
46
46
|
"@push.rocks/smartdelay": "^3.0.5",
|
|
47
|
-
"@push.rocks/smartdns": "^7.
|
|
48
|
-
"@push.rocks/smartlog": "^3.1
|
|
47
|
+
"@push.rocks/smartdns": "^7.9.0",
|
|
48
|
+
"@push.rocks/smartlog": "^3.2.1",
|
|
49
49
|
"@push.rocks/smartnetwork": "^4.4.0",
|
|
50
50
|
"@push.rocks/smartstring": "^4.1.0",
|
|
51
51
|
"@push.rocks/smarttime": "^4.2.3",
|
|
52
52
|
"@push.rocks/smartunique": "^3.0.9",
|
|
53
53
|
"@push.rocks/taskbuffer": "^6.1.2",
|
|
54
|
-
"@tsclass/tsclass": "^9.
|
|
54
|
+
"@tsclass/tsclass": "^9.5.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@git.zone/tsbuild": "^4.
|
|
57
|
+
"@git.zone/tsbuild": "^4.3.0",
|
|
58
58
|
"@git.zone/tsrun": "^2.0.1",
|
|
59
|
-
"@git.zone/tstest": "^3.
|
|
59
|
+
"@git.zone/tstest": "^3.4.0",
|
|
60
60
|
"@push.rocks/qenv": "^6.1.3",
|
|
61
|
-
"@types/node": "^25.
|
|
61
|
+
"@types/node": "^25.5.0"
|
|
62
62
|
},
|
|
63
63
|
"files": [
|
|
64
64
|
"ts/**/*",
|
|
65
|
-
"
|
|
65
|
+
"ts_*/**/*",
|
|
66
66
|
"dist/**/*",
|
|
67
67
|
"dist_*/**/*",
|
|
68
68
|
"dist_ts/**/*",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -175,7 +175,7 @@ export class AcmeCrypto {
|
|
|
175
175
|
/**
|
|
176
176
|
* Determine JWS algorithm from key type
|
|
177
177
|
*/
|
|
178
|
-
|
|
178
|
+
static getAlg(keyPem: string): string {
|
|
179
179
|
const keyObj = crypto.createPrivateKey(keyPem);
|
|
180
180
|
const keyType = keyObj.asymmetricKeyType;
|
|
181
181
|
if (keyType === 'rsa') return 'RS256';
|
package/ts/index.ts
CHANGED
|
@@ -10,5 +10,9 @@ export { certmanagers };
|
|
|
10
10
|
import * as handlers from './handlers/index.js';
|
|
11
11
|
export { handlers };
|
|
12
12
|
|
|
13
|
+
// server (ACME Directory Server / CA)
|
|
14
|
+
import * as server from '../ts_server/index.js';
|
|
15
|
+
export { server };
|
|
16
|
+
|
|
13
17
|
// re-export taskbuffer event types for consumers
|
|
14
18
|
export type { ITaskEvent, ITaskMetadata } from '@push.rocks/taskbuffer';
|
|
@@ -430,6 +430,7 @@ export class SmartAcme {
|
|
|
430
430
|
|
|
431
431
|
if (isWildcardRequest) {
|
|
432
432
|
identifiers.push({ type: 'dns', value: `*.${certDomainName}` });
|
|
433
|
+
identifiers.push({ type: 'dns', value: certDomainName });
|
|
433
434
|
} else {
|
|
434
435
|
identifiers.push({ type: 'dns', value: certDomainName });
|
|
435
436
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { AcmeServer } from './server.classes.acmeserver.js';
|
|
2
|
+
export { AcmeServerError } from './server.classes.jws.verifier.js';
|
|
3
|
+
export { AcmeServerCA } from './server.classes.ca.js';
|
|
4
|
+
export type {
|
|
5
|
+
IAcmeServerOptions,
|
|
6
|
+
IServerAccountStore,
|
|
7
|
+
IServerOrderStore,
|
|
8
|
+
IServerAccount,
|
|
9
|
+
IServerOrder,
|
|
10
|
+
IServerAuthorization,
|
|
11
|
+
IServerChallenge,
|
|
12
|
+
} from './server.interfaces.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IServerAccountStore, IServerAccount } from './server.interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory account storage for the ACME server.
|
|
5
|
+
*/
|
|
6
|
+
export class MemoryAccountStore implements IServerAccountStore {
|
|
7
|
+
private accounts = new Map<string, IServerAccount>();
|
|
8
|
+
private byThumbprint = new Map<string, string>();
|
|
9
|
+
private byUrl = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
async create(account: IServerAccount): Promise<IServerAccount> {
|
|
12
|
+
this.accounts.set(account.id, account);
|
|
13
|
+
this.byThumbprint.set(account.thumbprint, account.id);
|
|
14
|
+
this.byUrl.set(account.url, account.id);
|
|
15
|
+
return account;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getByThumbprint(thumbprint: string): Promise<IServerAccount | null> {
|
|
19
|
+
const id = this.byThumbprint.get(thumbprint);
|
|
20
|
+
return id ? this.accounts.get(id) || null : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getByUrl(url: string): Promise<IServerAccount | null> {
|
|
24
|
+
const id = this.byUrl.get(url);
|
|
25
|
+
return id ? this.accounts.get(id) || null : null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import type { IAcmeServerOptions } from './server.interfaces.js';
|
|
3
|
+
import { NonceManager } from './server.classes.nonce.js';
|
|
4
|
+
import { JwsVerifier } from './server.classes.jws.verifier.js';
|
|
5
|
+
import { MemoryAccountStore } from './server.classes.account.store.js';
|
|
6
|
+
import { MemoryOrderStore } from './server.classes.order.store.js';
|
|
7
|
+
import { AcmeServerCA } from './server.classes.ca.js';
|
|
8
|
+
import { ChallengeVerifier } from './server.classes.challenge.verifier.js';
|
|
9
|
+
import { AcmeRouter } from './server.classes.router.js';
|
|
10
|
+
import { createDirectoryHandler } from './server.handlers.directory.js';
|
|
11
|
+
import { createNonceHeadHandler, createNonceGetHandler } from './server.handlers.nonce.js';
|
|
12
|
+
import { createAccountHandler } from './server.handlers.account.js';
|
|
13
|
+
import { createNewOrderHandler, createOrderPollHandler } from './server.handlers.order.js';
|
|
14
|
+
import { createAuthzHandler } from './server.handlers.authz.js';
|
|
15
|
+
import { createChallengeHandler } from './server.handlers.challenge.js';
|
|
16
|
+
import { createFinalizeHandler } from './server.handlers.finalize.js';
|
|
17
|
+
import { createCertHandler } from './server.handlers.cert.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ACME Directory Server — a self-contained RFC 8555 Certificate Authority.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```ts
|
|
24
|
+
* const server = new AcmeServer({ port: 14000 });
|
|
25
|
+
* await server.start();
|
|
26
|
+
* console.log(server.getDirectoryUrl()); // http://localhost:14000/directory
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class AcmeServer {
|
|
30
|
+
private options: Required<Pick<IAcmeServerOptions, 'port' | 'hostname'>> & IAcmeServerOptions;
|
|
31
|
+
private httpServer: http.Server | null = null;
|
|
32
|
+
private ca: AcmeServerCA;
|
|
33
|
+
private baseUrl: string;
|
|
34
|
+
|
|
35
|
+
constructor(options: IAcmeServerOptions = {}) {
|
|
36
|
+
this.options = {
|
|
37
|
+
port: options.port ?? 14000,
|
|
38
|
+
hostname: options.hostname ?? '0.0.0.0',
|
|
39
|
+
...options,
|
|
40
|
+
};
|
|
41
|
+
this.baseUrl = options.baseUrl ?? `http://localhost:${this.options.port}`;
|
|
42
|
+
this.ca = new AcmeServerCA(options.caOptions);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async start(): Promise<void> {
|
|
46
|
+
// Initialize CA
|
|
47
|
+
await this.ca.init();
|
|
48
|
+
|
|
49
|
+
// Create stores
|
|
50
|
+
const accountStore = new MemoryAccountStore();
|
|
51
|
+
const orderStore = new MemoryOrderStore();
|
|
52
|
+
|
|
53
|
+
// Create managers
|
|
54
|
+
const nonceManager = new NonceManager();
|
|
55
|
+
const jwsVerifier = new JwsVerifier(nonceManager, accountStore);
|
|
56
|
+
const challengeVerifier = new ChallengeVerifier(this.options.challengeVerification ?? true);
|
|
57
|
+
|
|
58
|
+
// Create router and register routes
|
|
59
|
+
const router = new AcmeRouter(nonceManager);
|
|
60
|
+
|
|
61
|
+
// Directory
|
|
62
|
+
router.addRoute('GET', '/directory', createDirectoryHandler(this.baseUrl));
|
|
63
|
+
|
|
64
|
+
// Nonce
|
|
65
|
+
router.addRoute('HEAD', '/new-nonce', createNonceHeadHandler());
|
|
66
|
+
router.addRoute('GET', '/new-nonce', createNonceGetHandler());
|
|
67
|
+
|
|
68
|
+
// Account
|
|
69
|
+
router.addRoute('POST', '/new-account', createAccountHandler(this.baseUrl, jwsVerifier, accountStore));
|
|
70
|
+
|
|
71
|
+
// Order
|
|
72
|
+
router.addRoute('POST', '/new-order', createNewOrderHandler(this.baseUrl, jwsVerifier, orderStore));
|
|
73
|
+
router.addRoute('POST', '/order/:id', createOrderPollHandler(this.baseUrl, jwsVerifier, orderStore));
|
|
74
|
+
|
|
75
|
+
// Authorization
|
|
76
|
+
router.addRoute('POST', '/authz/:id', createAuthzHandler(this.baseUrl, jwsVerifier, orderStore));
|
|
77
|
+
|
|
78
|
+
// Challenge
|
|
79
|
+
router.addRoute('POST', '/challenge/:id', createChallengeHandler(
|
|
80
|
+
this.baseUrl,
|
|
81
|
+
jwsVerifier,
|
|
82
|
+
orderStore,
|
|
83
|
+
accountStore,
|
|
84
|
+
challengeVerifier,
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
// Finalize
|
|
88
|
+
router.addRoute('POST', '/finalize/:id', createFinalizeHandler(this.baseUrl, jwsVerifier, orderStore, this.ca));
|
|
89
|
+
|
|
90
|
+
// Certificate
|
|
91
|
+
router.addRoute('POST', '/cert/:id', createCertHandler(this.baseUrl, jwsVerifier, orderStore));
|
|
92
|
+
|
|
93
|
+
// Start HTTP server
|
|
94
|
+
this.httpServer = http.createServer((req, res) => {
|
|
95
|
+
router.handle(req, res).catch((err) => {
|
|
96
|
+
if (!res.headersSent) {
|
|
97
|
+
res.writeHead(500, { 'Content-Type': 'application/problem+json' });
|
|
98
|
+
res.end(JSON.stringify({
|
|
99
|
+
type: 'urn:ietf:params:acme:error:serverInternal',
|
|
100
|
+
detail: err instanceof Error ? err.message : 'Unknown error',
|
|
101
|
+
status: 500,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await new Promise<void>((resolve) => {
|
|
108
|
+
this.httpServer!.listen(this.options.port, this.options.hostname, () => resolve());
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async stop(): Promise<void> {
|
|
113
|
+
if (this.httpServer) {
|
|
114
|
+
await new Promise<void>((resolve, reject) => {
|
|
115
|
+
this.httpServer!.close((err) => (err ? reject(err) : resolve()));
|
|
116
|
+
});
|
|
117
|
+
this.httpServer = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getDirectoryUrl(): string {
|
|
122
|
+
return `${this.baseUrl}/directory`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getCaCertPem(): string {
|
|
126
|
+
return this.ca.getCaCertPem();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Certificate Authority for the ACME server.
|
|
5
|
+
* Generates a self-signed root CA and signs certificates from CSRs.
|
|
6
|
+
* Uses @peculiar/x509 (already a project dependency).
|
|
7
|
+
*/
|
|
8
|
+
export class AcmeServerCA {
|
|
9
|
+
private caKeyPair!: CryptoKeyPair;
|
|
10
|
+
private caCert!: InstanceType<typeof import('@peculiar/x509').X509Certificate>;
|
|
11
|
+
private caCertPem!: string;
|
|
12
|
+
private certValidityDays: number;
|
|
13
|
+
|
|
14
|
+
constructor(private options: { commonName?: string; validityDays?: number; certValidityDays?: number } = {}) {
|
|
15
|
+
this.certValidityDays = options.certValidityDays ?? 90;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async init(): Promise<void> {
|
|
19
|
+
const x509 = await import('@peculiar/x509');
|
|
20
|
+
const { webcrypto } = crypto;
|
|
21
|
+
x509.cryptoProvider.set(webcrypto as any);
|
|
22
|
+
|
|
23
|
+
// Generate RSA key pair for the CA
|
|
24
|
+
this.caKeyPair = await webcrypto.subtle.generateKey(
|
|
25
|
+
{
|
|
26
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
27
|
+
modulusLength: 2048,
|
|
28
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
29
|
+
hash: 'SHA-256',
|
|
30
|
+
},
|
|
31
|
+
true,
|
|
32
|
+
['sign', 'verify'],
|
|
33
|
+
) as CryptoKeyPair;
|
|
34
|
+
|
|
35
|
+
const cn = this.options.commonName ?? 'SmartACME Test CA';
|
|
36
|
+
const validityDays = this.options.validityDays ?? 3650;
|
|
37
|
+
const notBefore = new Date();
|
|
38
|
+
const notAfter = new Date();
|
|
39
|
+
notAfter.setDate(notAfter.getDate() + validityDays);
|
|
40
|
+
|
|
41
|
+
// Create self-signed root CA certificate
|
|
42
|
+
this.caCert = await x509.X509CertificateGenerator.createSelfSigned({
|
|
43
|
+
serialNumber: this.randomSerialNumber(),
|
|
44
|
+
name: `CN=${cn}`,
|
|
45
|
+
notBefore,
|
|
46
|
+
notAfter,
|
|
47
|
+
signingAlgorithm: { name: 'RSASSA-PKCS1-v1_5' },
|
|
48
|
+
keys: this.caKeyPair,
|
|
49
|
+
extensions: [
|
|
50
|
+
new x509.BasicConstraintsExtension(true, undefined, true),
|
|
51
|
+
new x509.KeyUsagesExtension(
|
|
52
|
+
x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign,
|
|
53
|
+
true,
|
|
54
|
+
),
|
|
55
|
+
await x509.SubjectKeyIdentifierExtension.create(this.caKeyPair.publicKey),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.caCertPem = this.caCert.toString('pem');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sign a CSR and return a PEM certificate chain (end-entity + root CA).
|
|
64
|
+
* @param csrDerBase64url - The CSR in base64url-encoded DER format (as sent by ACME clients)
|
|
65
|
+
*/
|
|
66
|
+
async signCsr(csrDerBase64url: string): Promise<string> {
|
|
67
|
+
const x509 = await import('@peculiar/x509');
|
|
68
|
+
const { webcrypto } = crypto;
|
|
69
|
+
x509.cryptoProvider.set(webcrypto as any);
|
|
70
|
+
|
|
71
|
+
// Parse the CSR
|
|
72
|
+
const csrDer = Buffer.from(csrDerBase64url, 'base64url');
|
|
73
|
+
const csr = new x509.Pkcs10CertificateRequest(csrDer);
|
|
74
|
+
|
|
75
|
+
// Extract Subject Alternative Names from CSR extensions
|
|
76
|
+
const sanNames: { type: 'dns'; value: string }[] = [];
|
|
77
|
+
const sanExt = csr.extensions?.find(
|
|
78
|
+
(ext) => ext.type === '2.5.29.17', // OID for SubjectAlternativeName
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (sanExt) {
|
|
82
|
+
const san = new x509.SubjectAlternativeNameExtension(sanExt.rawData);
|
|
83
|
+
if (san.names) {
|
|
84
|
+
const jsonNames = san.names.toJSON();
|
|
85
|
+
for (const name of jsonNames) {
|
|
86
|
+
if (name.type === 'dns') {
|
|
87
|
+
sanNames.push({ type: 'dns', value: name.value });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If no SAN found, use CN from subject
|
|
94
|
+
if (sanNames.length === 0) {
|
|
95
|
+
const cnMatch = csr.subject.match(/CN=([^,]+)/);
|
|
96
|
+
if (cnMatch) {
|
|
97
|
+
sanNames.push({ type: 'dns', value: cnMatch[1] });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const notBefore = new Date();
|
|
102
|
+
const notAfter = new Date();
|
|
103
|
+
notAfter.setDate(notAfter.getDate() + this.certValidityDays);
|
|
104
|
+
|
|
105
|
+
// Sign the certificate
|
|
106
|
+
const cert = await x509.X509CertificateGenerator.create({
|
|
107
|
+
serialNumber: this.randomSerialNumber(),
|
|
108
|
+
subject: csr.subject,
|
|
109
|
+
issuer: this.caCert.subject,
|
|
110
|
+
notBefore,
|
|
111
|
+
notAfter,
|
|
112
|
+
signingAlgorithm: { name: 'RSASSA-PKCS1-v1_5' },
|
|
113
|
+
publicKey: csr.publicKey,
|
|
114
|
+
signingKey: this.caKeyPair.privateKey,
|
|
115
|
+
extensions: [
|
|
116
|
+
new x509.BasicConstraintsExtension(false, undefined, true),
|
|
117
|
+
new x509.KeyUsagesExtension(
|
|
118
|
+
x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
|
|
119
|
+
true,
|
|
120
|
+
),
|
|
121
|
+
new x509.ExtendedKeyUsageExtension(
|
|
122
|
+
['1.3.6.1.5.5.7.3.1'], // serverAuth
|
|
123
|
+
true,
|
|
124
|
+
),
|
|
125
|
+
new x509.SubjectAlternativeNameExtension(sanNames),
|
|
126
|
+
await x509.AuthorityKeyIdentifierExtension.create(this.caKeyPair.publicKey),
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Return PEM chain: end-entity cert + root CA cert
|
|
131
|
+
const certPem = cert.toString('pem');
|
|
132
|
+
return `${certPem}\n${this.caCertPem}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getCaCertPem(): string {
|
|
136
|
+
return this.caCertPem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private randomSerialNumber(): string {
|
|
140
|
+
return crypto.randomBytes(16).toString('hex');
|
|
141
|
+
}
|
|
142
|
+
}
|