@labacacia/nps-sdk 1.0.0-alpha.5 → 1.0.0-alpha.7
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/CHANGELOG.cn.md +29 -5
- package/CHANGELOG.md +29 -5
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.cn.md +8 -13
- package/README.md +8 -13
- package/dist/nip/index.d.ts +1 -0
- package/dist/nip/index.d.ts.map +1 -1
- package/dist/nip/index.js +2 -0
- package/dist/nip/index.js.map +1 -1
- package/dist/nip/reputation-client.d.ts +116 -0
- package/dist/nip/reputation-client.d.ts.map +1 -0
- package/dist/nip/reputation-client.js +261 -0
- package/dist/nip/reputation-client.js.map +1 -0
- package/dist/nip/x509/oids.d.ts +9 -10
- package/dist/nip/x509/oids.d.ts.map +1 -1
- package/dist/nip/x509/oids.js +3 -4
- package/dist/nip/x509/oids.js.map +1 -1
- package/dist/nwp/anchor-client.d.ts +109 -0
- package/dist/nwp/anchor-client.d.ts.map +1 -0
- package/dist/nwp/anchor-client.js +279 -0
- package/dist/nwp/anchor-client.js.map +1 -0
- package/dist/nwp/index.d.ts +1 -1
- package/dist/nwp/index.d.ts.map +1 -1
- package/dist/nwp/index.js +1 -1
- package/dist/nwp/index.js.map +1 -1
- package/doc/nps-sdk.core.cn.md +0 -0
- package/doc/nps-sdk.core.md +0 -0
- package/doc/nps-sdk.ncp.cn.md +0 -0
- package/doc/nps-sdk.ncp.md +0 -0
- package/doc/nps-sdk.ndp.cn.md +0 -0
- package/doc/nps-sdk.ndp.md +0 -0
- package/doc/nps-sdk.nop.cn.md +0 -0
- package/doc/nps-sdk.nop.md +0 -0
- package/doc/overview.cn.md +0 -0
- package/doc/overview.md +0 -0
- package/package.json +12 -1
- package/CONTRIBUTING.cn.md +0 -35
- package/CONTRIBUTING.md +0 -35
- package/dist/nwp/error-codes.d.ts +0 -42
- package/dist/nwp/error-codes.d.ts.map +0 -1
- package/dist/nwp/error-codes.js +0 -53
- package/dist/nwp/error-codes.js.map +0 -1
- package/nip-ca-server/Dockerfile +0 -27
- package/nip-ca-server/README.md +0 -45
- package/nip-ca-server/db/001_init.sql +0 -25
- package/nip-ca-server/docker-compose.yml +0 -29
- package/nip-ca-server/package.json +0 -23
- package/nip-ca-server/src/ca.ts +0 -155
- package/nip-ca-server/src/db.ts +0 -104
- package/nip-ca-server/src/index.ts +0 -157
- package/nip-ca-server/tsconfig.json +0 -13
- package/src/core/anchor-cache.ts +0 -129
- package/src/core/cache.ts +0 -93
- package/src/core/canonical-json.ts +0 -50
- package/src/core/codec.ts +0 -158
- package/src/core/codecs/index.ts +0 -5
- package/src/core/codecs/ncp-codec.ts +0 -170
- package/src/core/codecs/tier1-json-codec.ts +0 -33
- package/src/core/codecs/tier2-msgpack-codec.ts +0 -30
- package/src/core/crypto-provider.ts +0 -47
- package/src/core/exceptions.ts +0 -57
- package/src/core/frame-header.ts +0 -282
- package/src/core/frame-registry.ts +0 -91
- package/src/core/frames.ts +0 -184
- package/src/core/index.ts +0 -42
- package/src/core/registry.ts +0 -28
- package/src/core/status-codes.ts +0 -47
- package/src/index.ts +0 -10
- package/src/ncp/frames/anchor-frame.ts +0 -87
- package/src/ncp/frames/caps-frame.ts +0 -59
- package/src/ncp/frames/diff-frame.ts +0 -69
- package/src/ncp/frames/error-frame.ts +0 -26
- package/src/ncp/frames/hello-frame.ts +0 -50
- package/src/ncp/frames/stream-frame.ts +0 -35
- package/src/ncp/frames.ts +0 -251
- package/src/ncp/handshake.ts +0 -95
- package/src/ncp/index.ts +0 -13
- package/src/ncp/ncp-error-codes.ts +0 -36
- package/src/ncp/ncp-patch-format.ts +0 -16
- package/src/ncp/preamble.ts +0 -79
- package/src/ncp/registry.ts +0 -15
- package/src/ncp/stream-manager.ts +0 -212
- package/src/ndp/dns-txt.ts +0 -86
- package/src/ndp/frames.ts +0 -124
- package/src/ndp/index.ts +0 -8
- package/src/ndp/ndp-registry.ts +0 -116
- package/src/ndp/registry.ts +0 -12
- package/src/ndp/validator.ts +0 -64
- package/src/nip/acme/client.ts +0 -185
- package/src/nip/acme/index.ts +0 -8
- package/src/nip/acme/jws.ts +0 -109
- package/src/nip/acme/messages.ts +0 -85
- package/src/nip/acme/server.ts +0 -480
- package/src/nip/acme/wire.ts +0 -24
- package/src/nip/assurance-level.ts +0 -40
- package/src/nip/cert-format.ts +0 -9
- package/src/nip/error-codes.ts +0 -38
- package/src/nip/frames.ts +0 -138
- package/src/nip/identity.ts +0 -113
- package/src/nip/index.ts +0 -14
- package/src/nip/registry.ts +0 -12
- package/src/nip/verifier.ts +0 -122
- package/src/nip/x509/builder.ts +0 -91
- package/src/nip/x509/index.ts +0 -6
- package/src/nip/x509/oids.ts +0 -28
- package/src/nip/x509/verifier.ts +0 -214
- package/src/nop/client.ts +0 -103
- package/src/nop/frames.ts +0 -181
- package/src/nop/index.ts +0 -7
- package/src/nop/models.ts +0 -79
- package/src/nop/nop-types.ts +0 -208
- package/src/nop/registry.ts +0 -13
- package/src/nwp/client.ts +0 -114
- package/src/nwp/error-codes.ts +0 -62
- package/src/nwp/frames.ts +0 -116
- package/src/nwp/index.ts +0 -7
- package/src/nwp/registry.ts +0 -11
- package/src/setup.ts +0 -32
- package/tests/_rfc0002-keys.ts +0 -57
- package/tests/core/anchor-cache.test.ts +0 -242
- package/tests/core/codec.test.ts +0 -205
- package/tests/core/frame-registry.test.ts +0 -46
- package/tests/core.test.ts +0 -327
- package/tests/ncp/diff-binary-bitset.test.ts +0 -107
- package/tests/ncp/e2e-enc-reject.test.ts +0 -93
- package/tests/ncp/err-error-frame.test.ts +0 -152
- package/tests/ncp/frames.test.ts +0 -359
- package/tests/ncp/framing.test.ts +0 -233
- package/tests/ncp/hello-frame.test.ts +0 -122
- package/tests/ncp/inline-anchor.test.ts +0 -88
- package/tests/ncp/preamble.test.ts +0 -93
- package/tests/ncp/security.test.ts +0 -184
- package/tests/ncp/stream-window.test.ts +0 -167
- package/tests/ncp/stream.test.ts +0 -242
- package/tests/ncp/version-negotiation.test.ts +0 -123
- package/tests/ndp.test.ts +0 -377
- package/tests/nip-acme-agent01.test.ts +0 -192
- package/tests/nip-x509.test.ts +0 -280
- package/tests/nip.test.ts +0 -184
- package/tests/nop.test.ts +0 -344
- package/tests/nwp.test.ts +0 -237
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -20
- package/vitest.config.ts +0 -10
package/src/nip/acme/server.ts
DELETED
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* In-process ACME server implementing the `agent-01` challenge for NPS-RFC-0002 §4.4.
|
|
6
|
-
*
|
|
7
|
-
* Backed by Node's stdlib `http.createServer`. Suitable for tests and reference
|
|
8
|
-
* deployments. State is kept in memory.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as ed25519 from "@noble/ed25519";
|
|
12
|
-
import { sha512 } from "@noble/hashes/sha512";
|
|
13
|
-
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
14
|
-
import { randomBytes } from "node:crypto";
|
|
15
|
-
import * as x509 from "@peculiar/x509";
|
|
16
|
-
|
|
17
|
-
import { AssuranceLevel } from "../assurance-level.js";
|
|
18
|
-
import { ACME_CHALLENGE_FAILED } from "../error-codes.js";
|
|
19
|
-
import { issueLeaf } from "../x509/builder.js";
|
|
20
|
-
import * as Jws from "./jws.js";
|
|
21
|
-
import type {
|
|
22
|
-
Authorization, Challenge, ChallengeRespondPayload, Directory,
|
|
23
|
-
FinalizePayload, Identifier, NewOrderPayload, Order, ProblemDetail,
|
|
24
|
-
} from "./messages.js";
|
|
25
|
-
import * as wire from "./wire.js";
|
|
26
|
-
|
|
27
|
-
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
|
|
28
|
-
x509.cryptoProvider.set(globalThis.crypto);
|
|
29
|
-
|
|
30
|
-
export interface AcmeServerOptions {
|
|
31
|
-
caNid: string;
|
|
32
|
-
caKeys: CryptoKeyPair; // Web Crypto Ed25519 keypair (for issuing X.509 leaves).
|
|
33
|
-
caRootCert: x509.X509Certificate;
|
|
34
|
-
certValidityMs: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface OrderState {
|
|
38
|
-
id: string;
|
|
39
|
-
identifier: Identifier;
|
|
40
|
-
status: string;
|
|
41
|
-
authzId: string;
|
|
42
|
-
finalizeUrl: string;
|
|
43
|
-
accountUrl: string;
|
|
44
|
-
certificateUrl?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface AuthzState {
|
|
48
|
-
id: string;
|
|
49
|
-
identifier: Identifier;
|
|
50
|
-
status: string;
|
|
51
|
-
challengeIds: string[];
|
|
52
|
-
accountUrl: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ChallengeState {
|
|
56
|
-
id: string;
|
|
57
|
-
type: string;
|
|
58
|
-
status: string;
|
|
59
|
-
token: string;
|
|
60
|
-
authzId: string;
|
|
61
|
-
accountUrl: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class AcmeServer {
|
|
65
|
-
private readonly server: Server;
|
|
66
|
-
private readonly nonces = new Set<string>();
|
|
67
|
-
private readonly accountJwks = new Map<string, Jws.Jwk>();
|
|
68
|
-
private readonly orders = new Map<string, OrderState>();
|
|
69
|
-
private readonly authzs = new Map<string, AuthzState>();
|
|
70
|
-
private readonly challenges = new Map<string, ChallengeState>();
|
|
71
|
-
private readonly certs = new Map<string, string>();
|
|
72
|
-
private boundPort: number = 0;
|
|
73
|
-
|
|
74
|
-
constructor(public readonly options: AcmeServerOptions) {
|
|
75
|
-
this.server = createServer((req, res) => this.dispatch(req, res));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async start(): Promise<this> {
|
|
79
|
-
await new Promise<void>((resolve) => {
|
|
80
|
-
this.server.listen(0, "127.0.0.1", () => resolve());
|
|
81
|
-
});
|
|
82
|
-
const addr = this.server.address();
|
|
83
|
-
this.boundPort = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
84
|
-
return this;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
close(): Promise<void> {
|
|
88
|
-
return new Promise((resolve) => this.server.close(() => resolve()));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
get baseUrl(): string { return `http://127.0.0.1:${this.boundPort}`; }
|
|
92
|
-
get directoryUrl(): string { return `${this.baseUrl}/directory`; }
|
|
93
|
-
|
|
94
|
-
// ── Routing ──────────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
private async dispatch(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
97
|
-
const url = req.url ?? "/";
|
|
98
|
-
const method = req.method ?? "GET";
|
|
99
|
-
try {
|
|
100
|
-
if (method === "GET" && url === "/directory") return this.handleDirectory(res);
|
|
101
|
-
if (url === "/new-nonce") return this.handleNewNonce(method, res);
|
|
102
|
-
if (method === "POST" && url === "/new-account") return await this.handleNewAccount(req, res);
|
|
103
|
-
if (method === "POST" && url === "/new-order") return await this.handleNewOrder(req, res);
|
|
104
|
-
if (method === "POST" && url.startsWith("/authz/")) return await this.handleAuthz(req, res, url);
|
|
105
|
-
if (method === "POST" && url.startsWith("/chall/")) return await this.handleChallenge(req, res, url);
|
|
106
|
-
if (method === "POST" && url.startsWith("/finalize/")) return await this.handleFinalize(req, res, url);
|
|
107
|
-
if (method === "POST" && url.startsWith("/cert/")) return await this.handleCert(req, res, url);
|
|
108
|
-
if (method === "POST" && url.startsWith("/order/")) return await this.handleOrder(req, res, url);
|
|
109
|
-
this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no such resource");
|
|
110
|
-
} catch (e) {
|
|
111
|
-
this.sendProblem(res, 500, "urn:ietf:params:acme:error:serverInternal",
|
|
112
|
-
(e as Error).message);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Endpoint handlers ────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
private handleDirectory(res: ServerResponse): void {
|
|
119
|
-
const dir: Directory = {
|
|
120
|
-
newNonce: `${this.baseUrl}/new-nonce`,
|
|
121
|
-
newAccount: `${this.baseUrl}/new-account`,
|
|
122
|
-
newOrder: `${this.baseUrl}/new-order`,
|
|
123
|
-
};
|
|
124
|
-
this.sendJson(res, 200, dir);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private handleNewNonce(method: string, res: ServerResponse): void {
|
|
128
|
-
res.statusCode = method === "HEAD" ? 200 : 204;
|
|
129
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
130
|
-
res.setHeader("Cache-Control", "no-store");
|
|
131
|
-
res.end();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private async handleNewAccount(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
135
|
-
const env = await this.readEnvelope(req, res);
|
|
136
|
-
if (!env) return;
|
|
137
|
-
const header = this.parseHeader(env, res);
|
|
138
|
-
if (!header) return;
|
|
139
|
-
if (!header.jwk) {
|
|
140
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
|
|
141
|
-
"newAccount must include a 'jwk' member");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
145
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce");
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const pub = Jws.publicKeyFromJwk(header.jwk);
|
|
149
|
-
if (Jws.verify(env, pub) === null) {
|
|
150
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
|
|
151
|
-
"JWS signature verify failed");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const accountId = `acc-${shortId()}`;
|
|
156
|
-
const accountUrl = `${this.baseUrl}/account/${accountId}`;
|
|
157
|
-
this.accountJwks.set(accountUrl, header.jwk);
|
|
158
|
-
|
|
159
|
-
res.statusCode = 201;
|
|
160
|
-
res.setHeader("Content-Type", "application/json");
|
|
161
|
-
res.setHeader("Location", accountUrl);
|
|
162
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
163
|
-
res.end(JSON.stringify({ status: wire.Status.VALID }));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private async handleNewOrder(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
167
|
-
const env = await this.readEnvelope(req, res);
|
|
168
|
-
if (!env) return;
|
|
169
|
-
const header = this.parseHeader(env, res);
|
|
170
|
-
if (!header) return;
|
|
171
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
172
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
173
|
-
}
|
|
174
|
-
if (!this.verifyAccount(env, header)) {
|
|
175
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:accountDoesNotExist",
|
|
176
|
-
`unknown kid: ${header.kid ?? "<missing>"}`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const payload = Jws.decodePayload<NewOrderPayload>(env);
|
|
181
|
-
if (!payload || !payload.identifiers?.length) {
|
|
182
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "missing identifiers");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const ident = payload.identifiers[0];
|
|
186
|
-
const orderId = `ord-${shortId()}`;
|
|
187
|
-
const authzId = `az-${shortId()}`;
|
|
188
|
-
const challId = `ch-${shortId()}`;
|
|
189
|
-
const token = Jws.b64uEncode(new Uint8Array(randomBytes(32)));
|
|
190
|
-
|
|
191
|
-
const orderUrl = `${this.baseUrl}/order/${orderId}`;
|
|
192
|
-
const authzUrl = `${this.baseUrl}/authz/${authzId}`;
|
|
193
|
-
const challUrl = `${this.baseUrl}/chall/${challId}`;
|
|
194
|
-
const finalizeUrl = `${this.baseUrl}/finalize/${orderId}`;
|
|
195
|
-
|
|
196
|
-
this.challenges.set(challId, {
|
|
197
|
-
id: challId, type: wire.CHALLENGE_AGENT_01, status: wire.Status.PENDING,
|
|
198
|
-
token, authzId, accountUrl: header.kid ?? "",
|
|
199
|
-
});
|
|
200
|
-
this.authzs.set(authzId, {
|
|
201
|
-
id: authzId, identifier: ident, status: wire.Status.PENDING,
|
|
202
|
-
challengeIds: [challId], accountUrl: header.kid ?? "",
|
|
203
|
-
});
|
|
204
|
-
this.orders.set(orderId, {
|
|
205
|
-
id: orderId, identifier: ident, status: wire.Status.PENDING,
|
|
206
|
-
authzId, finalizeUrl, accountUrl: header.kid ?? "",
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const order: Order = {
|
|
210
|
-
status: wire.Status.PENDING,
|
|
211
|
-
identifiers: [ident],
|
|
212
|
-
authorizations: [authzUrl],
|
|
213
|
-
finalize: finalizeUrl,
|
|
214
|
-
};
|
|
215
|
-
res.statusCode = 201;
|
|
216
|
-
res.setHeader("Content-Type", "application/json");
|
|
217
|
-
res.setHeader("Location", orderUrl);
|
|
218
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
219
|
-
res.end(JSON.stringify(order));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private async handleAuthz(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
|
|
223
|
-
const env = await this.readEnvelope(req, res); if (!env) return;
|
|
224
|
-
const header = this.parseHeader(env, res); if (!header) return;
|
|
225
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
226
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
227
|
-
}
|
|
228
|
-
if (!this.verifyAccount(env, header)) {
|
|
229
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
|
|
230
|
-
}
|
|
231
|
-
const id = url.replace(/^\/authz\//, "");
|
|
232
|
-
const az = this.authzs.get(id);
|
|
233
|
-
if (!az) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no authz"); return; }
|
|
234
|
-
|
|
235
|
-
const challenges: Challenge[] = az.challengeIds.map((cid) => {
|
|
236
|
-
const cs = this.challenges.get(cid)!;
|
|
237
|
-
return {
|
|
238
|
-
type: cs.type, url: `${this.baseUrl}/chall/${cs.id}`,
|
|
239
|
-
status: cs.status, token: cs.token,
|
|
240
|
-
};
|
|
241
|
-
});
|
|
242
|
-
const authz: Authorization = {
|
|
243
|
-
status: az.status, identifier: az.identifier, challenges,
|
|
244
|
-
};
|
|
245
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
246
|
-
this.sendJson(res, 200, authz);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private async handleChallenge(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
|
|
250
|
-
const env = await this.readEnvelope(req, res); if (!env) return;
|
|
251
|
-
const header = this.parseHeader(env, res); if (!header) return;
|
|
252
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
253
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
254
|
-
}
|
|
255
|
-
const accountJwk = this.accountJwks.get(header.kid ?? "");
|
|
256
|
-
if (!accountJwk) {
|
|
257
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:accountDoesNotExist", "unknown kid");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const accountPub = Jws.publicKeyFromJwk(accountJwk);
|
|
261
|
-
if (Jws.verify(env, accountPub) === null) {
|
|
262
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "JWS sig fail"); return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const id = url.replace(/^\/chall\//, "");
|
|
266
|
-
const ch = this.challenges.get(id);
|
|
267
|
-
if (!ch) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no chall"); return; }
|
|
268
|
-
|
|
269
|
-
const payload = Jws.decodePayload<ChallengeRespondPayload>(env);
|
|
270
|
-
if (!payload?.agent_signature) {
|
|
271
|
-
ch.status = wire.Status.INVALID;
|
|
272
|
-
this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
|
|
273
|
-
"missing agent_signature in challenge response");
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const sigBytes = Jws.b64uDecode(payload.agent_signature);
|
|
278
|
-
const tokenBytes = new TextEncoder().encode(ch.token);
|
|
279
|
-
if (!ed25519.verify(sigBytes, tokenBytes, accountPub)) {
|
|
280
|
-
ch.status = wire.Status.INVALID;
|
|
281
|
-
this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
|
|
282
|
-
"agent-01 signature did not verify");
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
} catch (e) {
|
|
286
|
-
ch.status = wire.Status.INVALID;
|
|
287
|
-
this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
|
|
288
|
-
`agent-01 verification error: ${(e as Error).message}`);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
ch.status = wire.Status.VALID;
|
|
293
|
-
const az = this.authzs.get(ch.authzId);
|
|
294
|
-
if (az) az.status = wire.Status.VALID;
|
|
295
|
-
for (const o of this.orders.values()) {
|
|
296
|
-
if (o.authzId === ch.authzId) o.status = wire.Status.READY;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
300
|
-
this.sendJson(res, 200, {
|
|
301
|
-
type: ch.type, url: `${this.baseUrl}/chall/${ch.id}`,
|
|
302
|
-
status: ch.status, token: ch.token,
|
|
303
|
-
} as Challenge);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
private async handleFinalize(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
|
|
307
|
-
const env = await this.readEnvelope(req, res); if (!env) return;
|
|
308
|
-
const header = this.parseHeader(env, res); if (!header) return;
|
|
309
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
310
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
311
|
-
}
|
|
312
|
-
if (!this.verifyAccount(env, header)) {
|
|
313
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
|
|
314
|
-
}
|
|
315
|
-
const orderId = url.replace(/^\/finalize\//, "");
|
|
316
|
-
const os = this.orders.get(orderId);
|
|
317
|
-
if (!os) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no order"); return; }
|
|
318
|
-
if (os.status !== wire.Status.READY) {
|
|
319
|
-
this.sendProblem(res, 403, "urn:ietf:params:acme:error:orderNotReady",
|
|
320
|
-
`order is in state '${os.status}', not 'ready'`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const fp = Jws.decodePayload<FinalizePayload>(env);
|
|
324
|
-
if (!fp?.csr) {
|
|
325
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "missing csr"); return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
const csrDer = Jws.b64uDecode(fp.csr);
|
|
330
|
-
const csr = new x509.Pkcs10CertificateRequest(csrDer.buffer as ArrayBuffer);
|
|
331
|
-
const subjectCn = (() => {
|
|
332
|
-
for (const rdn of csr.subject.split(",")) {
|
|
333
|
-
const t = rdn.trim();
|
|
334
|
-
if (t.startsWith("CN=")) return t.slice(3).replace(/\\([",+;<>\\])/g, "$1");
|
|
335
|
-
}
|
|
336
|
-
return null as string | null;
|
|
337
|
-
})();
|
|
338
|
-
if (subjectCn !== os.identifier.value) {
|
|
339
|
-
this.sendProblem(res, 400, "NIP-CERT-SUBJECT-NID-MISMATCH",
|
|
340
|
-
`CSR subject CN '${subjectCn ?? ""}' does not match order identifier '${os.identifier.value}'`);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const subjectPub = await csr.publicKey.export();
|
|
344
|
-
const now = new Date();
|
|
345
|
-
const leaf = await issueLeaf({
|
|
346
|
-
subjectNid: os.identifier.value,
|
|
347
|
-
subjectPublicKey: subjectPub,
|
|
348
|
-
caKeys: this.options.caKeys,
|
|
349
|
-
issuerNid: this.options.caNid,
|
|
350
|
-
role: "agent",
|
|
351
|
-
assuranceLevel: AssuranceLevel.ANONYMOUS,
|
|
352
|
-
notBefore: new Date(now.getTime() - 60_000),
|
|
353
|
-
notAfter: new Date(now.getTime() + this.options.certValidityMs),
|
|
354
|
-
serialNumber: randomHexSerial(),
|
|
355
|
-
});
|
|
356
|
-
const certId = `crt-${shortId()}`;
|
|
357
|
-
const certUrl = `${this.baseUrl}/cert/${certId}`;
|
|
358
|
-
const pem = leaf.toString("pem") + this.options.caRootCert.toString("pem");
|
|
359
|
-
this.certs.set(certId, pem);
|
|
360
|
-
os.status = wire.Status.VALID;
|
|
361
|
-
os.certificateUrl = certUrl;
|
|
362
|
-
} catch (e) {
|
|
363
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badCSR",
|
|
364
|
-
`CSR processing failed: ${(e as Error).message}`);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const authzUrl = `${this.baseUrl}/authz/${os.authzId}`;
|
|
369
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
370
|
-
this.sendJson(res, 200, {
|
|
371
|
-
status: os.status, identifiers: [os.identifier],
|
|
372
|
-
authorizations: [authzUrl], finalize: os.finalizeUrl,
|
|
373
|
-
certificate: os.certificateUrl,
|
|
374
|
-
} as Order);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
private async handleCert(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
|
|
378
|
-
const env = await this.readEnvelope(req, res); if (!env) return;
|
|
379
|
-
const header = this.parseHeader(env, res); if (!header) return;
|
|
380
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
381
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
382
|
-
}
|
|
383
|
-
if (!this.verifyAccount(env, header)) {
|
|
384
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
|
|
385
|
-
}
|
|
386
|
-
const certId = url.replace(/^\/cert\//, "");
|
|
387
|
-
const pem = this.certs.get(certId);
|
|
388
|
-
if (!pem) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no cert"); return; }
|
|
389
|
-
|
|
390
|
-
res.statusCode = 200;
|
|
391
|
-
res.setHeader("Content-Type", wire.CONTENT_TYPE_PEM_CERT);
|
|
392
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
393
|
-
res.end(pem);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private async handleOrder(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
|
|
397
|
-
const env = await this.readEnvelope(req, res); if (!env) return;
|
|
398
|
-
const header = this.parseHeader(env, res); if (!header) return;
|
|
399
|
-
if (!this.consumeNonce(header.nonce)) {
|
|
400
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
|
|
401
|
-
}
|
|
402
|
-
if (!this.verifyAccount(env, header)) {
|
|
403
|
-
this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
|
|
404
|
-
}
|
|
405
|
-
const orderId = url.replace(/^\/order\//, "");
|
|
406
|
-
const os = this.orders.get(orderId);
|
|
407
|
-
if (!os) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no order"); return; }
|
|
408
|
-
const authzUrl = `${this.baseUrl}/authz/${os.authzId}`;
|
|
409
|
-
res.setHeader("Replay-Nonce", this.mintNonce());
|
|
410
|
-
this.sendJson(res, 200, {
|
|
411
|
-
status: os.status, identifiers: [os.identifier],
|
|
412
|
-
authorizations: [authzUrl], finalize: os.finalizeUrl,
|
|
413
|
-
certificate: os.certificateUrl,
|
|
414
|
-
} as Order);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ── helpers ──────────────────────────────────────────────────────────────
|
|
418
|
-
|
|
419
|
-
private mintNonce(): string {
|
|
420
|
-
const n = Jws.b64uEncode(new Uint8Array(randomBytes(16)));
|
|
421
|
-
this.nonces.add(n);
|
|
422
|
-
return n;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private consumeNonce(nonce: string): boolean {
|
|
426
|
-
return this.nonces.delete(nonce);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private verifyAccount(env: Jws.Envelope, header: Jws.ProtectedHeader): boolean {
|
|
430
|
-
if (!header.kid) return false;
|
|
431
|
-
const jwk = this.accountJwks.get(header.kid);
|
|
432
|
-
if (!jwk) return false;
|
|
433
|
-
return Jws.verify(env, Jws.publicKeyFromJwk(jwk)) !== null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
private async readEnvelope(req: IncomingMessage, res: ServerResponse): Promise<Jws.Envelope | null> {
|
|
437
|
-
try {
|
|
438
|
-
const chunks: Buffer[] = [];
|
|
439
|
-
for await (const chunk of req) {
|
|
440
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
441
|
-
}
|
|
442
|
-
const body = Buffer.concat(chunks).toString("utf8");
|
|
443
|
-
return JSON.parse(body) as Jws.Envelope;
|
|
444
|
-
} catch (e) {
|
|
445
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
|
|
446
|
-
`body read/parse failed: ${(e as Error).message}`);
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
private parseHeader(env: Jws.Envelope, res: ServerResponse): Jws.ProtectedHeader | null {
|
|
452
|
-
try {
|
|
453
|
-
return JSON.parse(new TextDecoder().decode(Jws.b64uDecode(env.protected))) as Jws.ProtectedHeader;
|
|
454
|
-
} catch (e) {
|
|
455
|
-
this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
|
|
456
|
-
`malformed protected header: ${(e as Error).message}`);
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
462
|
-
res.statusCode = status;
|
|
463
|
-
res.setHeader("Content-Type", "application/json");
|
|
464
|
-
res.end(JSON.stringify(body));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private sendProblem(res: ServerResponse, status: number, type: string, detail: string): void {
|
|
468
|
-
res.statusCode = status;
|
|
469
|
-
res.setHeader("Content-Type", wire.CONTENT_TYPE_PROBLEM);
|
|
470
|
-
res.end(JSON.stringify({ type, detail, status } as ProblemDetail));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function shortId(): string {
|
|
475
|
-
return Buffer.from(randomBytes(8)).toString("hex");
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function randomHexSerial(): string {
|
|
479
|
-
return Buffer.from(randomBytes(20)).toString("hex");
|
|
480
|
-
}
|
package/src/nip/acme/wire.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/** ACME wire constants (RFC 8555 + NPS-RFC-0002 §4.4). */
|
|
5
|
-
|
|
6
|
-
export const CONTENT_TYPE_JOSE_JSON = "application/jose+json";
|
|
7
|
-
export const CONTENT_TYPE_PROBLEM = "application/problem+json";
|
|
8
|
-
export const CONTENT_TYPE_PEM_CERT = "application/pem-certificate-chain";
|
|
9
|
-
|
|
10
|
-
export const CHALLENGE_AGENT_01 = "agent-01";
|
|
11
|
-
export const IDENTIFIER_TYPE_NID = "nid";
|
|
12
|
-
|
|
13
|
-
/** ACME status enumeration values (RFC 8555 §7.1.6). */
|
|
14
|
-
export const Status = {
|
|
15
|
-
PENDING: "pending",
|
|
16
|
-
READY: "ready",
|
|
17
|
-
PROCESSING: "processing",
|
|
18
|
-
VALID: "valid",
|
|
19
|
-
INVALID: "invalid",
|
|
20
|
-
EXPIRED: "expired",
|
|
21
|
-
DEACTIVATED: "deactivated",
|
|
22
|
-
REVOKED: "revoked",
|
|
23
|
-
SUBMITTED: "submitted",
|
|
24
|
-
} as const;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/** Agent identity assurance level per NPS-RFC-0003 §5.1.1. */
|
|
5
|
-
export type AssuranceLevelWire = "anonymous" | "attested" | "verified";
|
|
6
|
-
|
|
7
|
-
export class AssuranceLevel {
|
|
8
|
-
static readonly ANONYMOUS = new AssuranceLevel("anonymous", 0);
|
|
9
|
-
static readonly ATTESTED = new AssuranceLevel("attested", 1);
|
|
10
|
-
static readonly VERIFIED = new AssuranceLevel("verified", 2);
|
|
11
|
-
|
|
12
|
-
private constructor(
|
|
13
|
-
public readonly wire: AssuranceLevelWire,
|
|
14
|
-
public readonly rank: number,
|
|
15
|
-
) {}
|
|
16
|
-
|
|
17
|
-
meetsOrExceeds(required: AssuranceLevel): boolean {
|
|
18
|
-
return this.rank >= required.rank;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse a wire string. `null`, `undefined`, or `""` → `ANONYMOUS`
|
|
23
|
-
* (backward compat per NPS-RFC-0003 §5.1.1). Any other unrecognised
|
|
24
|
-
* non-empty value throws — callers MUST surface it as `NIP-ASSURANCE-UNKNOWN`.
|
|
25
|
-
*/
|
|
26
|
-
static fromWire(wire: string | null | undefined): AssuranceLevel {
|
|
27
|
-
if (!wire) return AssuranceLevel.ANONYMOUS; // null, undefined, or ""
|
|
28
|
-
for (const level of [AssuranceLevel.ANONYMOUS, AssuranceLevel.ATTESTED, AssuranceLevel.VERIFIED]) {
|
|
29
|
-
if (level.wire === wire) return level;
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`Unknown assurance_level: ${JSON.stringify(wire)}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
static fromRank(rank: number): AssuranceLevel {
|
|
35
|
-
for (const level of [AssuranceLevel.ANONYMOUS, AssuranceLevel.ATTESTED, AssuranceLevel.VERIFIED]) {
|
|
36
|
-
if (level.rank === rank) return level;
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Unknown assurance_level rank: ${rank}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/nip/cert-format.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/** Wire-form constants for `IdentFrame.cert_format` (NPS-RFC-0002 §4.5). */
|
|
5
|
-
|
|
6
|
-
export const V1_PROPRIETARY = "v1-proprietary" as const;
|
|
7
|
-
export const V2_X509 = "v2-x509" as const;
|
|
8
|
-
|
|
9
|
-
export type CertFormat = typeof V1_PROPRIETARY | typeof V2_X509;
|
package/src/nip/error-codes.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/** NIP error code wire constants — mirror of `spec/error-codes.md` NIP section. */
|
|
5
|
-
|
|
6
|
-
// ── Cert verification (v1 + v2) ──────────────────────────────────────────────
|
|
7
|
-
export const CERT_EXPIRED = "NIP-CERT-EXPIRED";
|
|
8
|
-
export const CERT_REVOKED = "NIP-CERT-REVOKED";
|
|
9
|
-
export const CERT_SIGNATURE_INVALID = "NIP-CERT-SIGNATURE-INVALID";
|
|
10
|
-
export const CERT_UNTRUSTED_ISSUER = "NIP-CERT-UNTRUSTED-ISSUER";
|
|
11
|
-
export const CERT_CAPABILITY_MISSING = "NIP-CERT-CAPABILITY-MISSING";
|
|
12
|
-
export const CERT_SCOPE_VIOLATION = "NIP-CERT-SCOPE-VIOLATION";
|
|
13
|
-
|
|
14
|
-
// ── CA service ───────────────────────────────────────────────────────────────
|
|
15
|
-
export const CA_NID_NOT_FOUND = "NIP-CA-NID-NOT-FOUND";
|
|
16
|
-
export const CA_NID_ALREADY_EXISTS = "NIP-CA-NID-ALREADY-EXISTS";
|
|
17
|
-
export const CA_SERIAL_DUPLICATE = "NIP-CA-SERIAL-DUPLICATE";
|
|
18
|
-
export const CA_RENEWAL_TOO_EARLY = "NIP-CA-RENEWAL-TOO-EARLY";
|
|
19
|
-
export const CA_SCOPE_EXPANSION_DENIED = "NIP-CA-SCOPE-EXPANSION-DENIED";
|
|
20
|
-
|
|
21
|
-
export const OCSP_UNAVAILABLE = "NIP-OCSP-UNAVAILABLE";
|
|
22
|
-
export const TRUST_FRAME_INVALID = "NIP-TRUST-FRAME-INVALID";
|
|
23
|
-
|
|
24
|
-
// ── RFC-0003 (assurance level) ───────────────────────────────────────────────
|
|
25
|
-
export const ASSURANCE_MISMATCH = "NIP-ASSURANCE-MISMATCH";
|
|
26
|
-
export const ASSURANCE_UNKNOWN = "NIP-ASSURANCE-UNKNOWN";
|
|
27
|
-
|
|
28
|
-
// ── RFC-0004 (reputation log) ────────────────────────────────────────────────
|
|
29
|
-
export const REPUTATION_ENTRY_INVALID = "NIP-REPUTATION-ENTRY-INVALID";
|
|
30
|
-
export const REPUTATION_LOG_UNREACHABLE = "NIP-REPUTATION-LOG-UNREACHABLE";
|
|
31
|
-
export const REPUTATION_GOSSIP_FORK = "NIP-REPUTATION-GOSSIP-FORK";
|
|
32
|
-
export const REPUTATION_GOSSIP_SIG_INVALID = "NIP-REPUTATION-GOSSIP-SIG-INVALID";
|
|
33
|
-
|
|
34
|
-
// ── RFC-0002 (X.509 + ACME) ──────────────────────────────────────────────────
|
|
35
|
-
export const CERT_FORMAT_INVALID = "NIP-CERT-FORMAT-INVALID";
|
|
36
|
-
export const CERT_EKU_MISSING = "NIP-CERT-EKU-MISSING";
|
|
37
|
-
export const CERT_SUBJECT_NID_MISMATCH = "NIP-CERT-SUBJECT-NID-MISMATCH";
|
|
38
|
-
export const ACME_CHALLENGE_FAILED = "NIP-ACME-CHALLENGE-FAILED";
|