@dorigjo/besa 0.1.0-alpha.2 → 0.1.0-beta.4
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/README.md +310 -337
- package/dist/admit.d.ts +13 -2
- package/dist/admit.js +186 -26
- package/dist/crypto.d.ts +4 -1
- package/dist/crypto.js +127 -19
- package/dist/grant.js +50 -5
- package/dist/index.js +418 -57
- package/dist/io.d.ts +5 -0
- package/dist/io.js +97 -0
- package/dist/keystore.d.ts +16 -0
- package/dist/keystore.js +117 -0
- package/dist/manifest.js +83 -17
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +2 -0
- package/dist/signing.d.ts +16 -2
- package/dist/signing.js +317 -31
- package/dist/trust.d.ts +17 -0
- package/dist/trust.js +466 -0
- package/dist/types.d.ts +25 -0
- package/examples/request.json +3 -0
- package/package.json +66 -57
- package/scripts/postinstall.mjs +30 -0
package/dist/trust.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { sign as ed25519Sign, verify as ed25519Verify, } from "node:crypto";
|
|
2
|
+
import { canonicalize, isCanonicalBase64, privateKeyFromDer, publicKeyFromDer, publicKeyId, signatureMessage, validateKeyPair, } from "./crypto.js";
|
|
3
|
+
import { verifySignedManifest } from "./signing.js";
|
|
4
|
+
const KEY_ID = /^[a-f0-9]{64}$/;
|
|
5
|
+
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1_000;
|
|
6
|
+
const MAX_TRUST_KEYS = 4_096;
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function isIsoDate(value) {
|
|
11
|
+
return (typeof value === "string" &&
|
|
12
|
+
value.length <= 35 &&
|
|
13
|
+
!Number.isNaN(Date.parse(value)) &&
|
|
14
|
+
new Date(value).toISOString() === value);
|
|
15
|
+
}
|
|
16
|
+
function validateAnchor(value, index) {
|
|
17
|
+
if (!isObject(value)) {
|
|
18
|
+
return [`keys[${index}] must be an object`];
|
|
19
|
+
}
|
|
20
|
+
const errors = [];
|
|
21
|
+
const allowedFields = new Set([
|
|
22
|
+
"publicKeyId",
|
|
23
|
+
"publicKey",
|
|
24
|
+
"status",
|
|
25
|
+
"addedAt",
|
|
26
|
+
"retiredAt",
|
|
27
|
+
"revokedAt",
|
|
28
|
+
]);
|
|
29
|
+
for (const field of Object.keys(value)) {
|
|
30
|
+
if (!allowedFields.has(field)) {
|
|
31
|
+
errors.push(`unexpected keys[${index}] field '${field}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof value.publicKeyId !== "string" || !KEY_ID.test(value.publicKeyId)) {
|
|
35
|
+
errors.push(`keys[${index}].publicKeyId must be a 64-character lowercase SHA-256 fingerprint`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof value.publicKey !== "string" ||
|
|
38
|
+
value.publicKey.length > 128 ||
|
|
39
|
+
!isCanonicalBase64(value.publicKey)) {
|
|
40
|
+
errors.push(`keys[${index}].publicKey must be canonical base64`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
try {
|
|
44
|
+
publicKeyFromDer(value.publicKey);
|
|
45
|
+
if (publicKeyId(value.publicKey) !== value.publicKeyId) {
|
|
46
|
+
errors.push(`keys[${index}].publicKeyId does not match publicKey`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
errors.push(`keys[${index}].publicKey must be a valid Ed25519 public key`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (value.status !== "active" &&
|
|
54
|
+
value.status !== "retired" &&
|
|
55
|
+
value.status !== "revoked") {
|
|
56
|
+
errors.push(`keys[${index}].status must be active, retired, or revoked`);
|
|
57
|
+
}
|
|
58
|
+
if (!isIsoDate(value.addedAt)) {
|
|
59
|
+
errors.push(`keys[${index}].addedAt must be a canonical ISO-8601 timestamp`);
|
|
60
|
+
}
|
|
61
|
+
if (value.status === "retired" && !isIsoDate(value.retiredAt)) {
|
|
62
|
+
errors.push(`keys[${index}].retiredAt is required for a retired key`);
|
|
63
|
+
}
|
|
64
|
+
else if (isIsoDate(value.retiredAt) &&
|
|
65
|
+
isIsoDate(value.addedAt) &&
|
|
66
|
+
Date.parse(value.retiredAt) < Date.parse(value.addedAt)) {
|
|
67
|
+
errors.push(`keys[${index}].retiredAt must not be before addedAt`);
|
|
68
|
+
}
|
|
69
|
+
if (value.status === "revoked" && !isIsoDate(value.revokedAt)) {
|
|
70
|
+
errors.push(`keys[${index}].revokedAt is required for a revoked key`);
|
|
71
|
+
}
|
|
72
|
+
else if (isIsoDate(value.revokedAt) &&
|
|
73
|
+
isIsoDate(value.addedAt) &&
|
|
74
|
+
Date.parse(value.revokedAt) < Date.parse(value.addedAt)) {
|
|
75
|
+
errors.push(`keys[${index}].revokedAt must not be before addedAt`);
|
|
76
|
+
}
|
|
77
|
+
if (value.status === "active" &&
|
|
78
|
+
(value.retiredAt !== undefined || value.revokedAt !== undefined)) {
|
|
79
|
+
errors.push(`keys[${index}] active keys must not have lifecycle end fields`);
|
|
80
|
+
}
|
|
81
|
+
if (value.status === "retired" && value.revokedAt !== undefined) {
|
|
82
|
+
errors.push(`keys[${index}] retired keys must not have revokedAt`);
|
|
83
|
+
}
|
|
84
|
+
if (value.status === "revoked" && value.retiredAt !== undefined) {
|
|
85
|
+
errors.push(`keys[${index}] revoked keys must not have retiredAt`);
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
export function validateTrustStore(value) {
|
|
90
|
+
if (!isObject(value)) {
|
|
91
|
+
return { ok: false, errors: ["trust store must be an object"] };
|
|
92
|
+
}
|
|
93
|
+
const errors = [];
|
|
94
|
+
const allowedFields = new Set(["version", "keys"]);
|
|
95
|
+
for (const field of Object.keys(value)) {
|
|
96
|
+
if (!allowedFields.has(field)) {
|
|
97
|
+
errors.push(`unexpected trust store field '${field}'`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (value.version !== 1) {
|
|
101
|
+
errors.push("version must be 1");
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(value.keys)) {
|
|
104
|
+
errors.push("keys must be an array");
|
|
105
|
+
}
|
|
106
|
+
else if (value.keys.length > MAX_TRUST_KEYS) {
|
|
107
|
+
errors.push(`keys must contain at most ${String(MAX_TRUST_KEYS)} entries`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
value.keys.forEach((key, index) => errors.push(...validateAnchor(key, index)));
|
|
111
|
+
const ids = value.keys
|
|
112
|
+
.filter(isObject)
|
|
113
|
+
.map((key) => key.publicKeyId)
|
|
114
|
+
.filter((id) => typeof id === "string");
|
|
115
|
+
if (new Set(ids).size !== ids.length) {
|
|
116
|
+
errors.push("keys must not contain duplicate publicKeyId values");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
canonicalize(value);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
errors.push(`trust store exceeds the JSON safety limits: ${message}`);
|
|
125
|
+
}
|
|
126
|
+
if (errors.length > 0) {
|
|
127
|
+
return { ok: false, errors };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
trustStore: value,
|
|
132
|
+
errors: [],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function emptyTrustStore() {
|
|
136
|
+
return { version: 1, keys: [] };
|
|
137
|
+
}
|
|
138
|
+
function assertValidTrustStore(store) {
|
|
139
|
+
const validation = validateTrustStore(store);
|
|
140
|
+
if (!validation.ok) {
|
|
141
|
+
throw new Error(`invalid trust store: ${validation.errors.join("; ")}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function addTrustAnchor(store, publicKey, addedAt = new Date().toISOString()) {
|
|
145
|
+
assertValidTrustStore(store);
|
|
146
|
+
if (!isIsoDate(addedAt)) {
|
|
147
|
+
throw new Error("addedAt must be a canonical ISO-8601 timestamp");
|
|
148
|
+
}
|
|
149
|
+
publicKeyFromDer(publicKey);
|
|
150
|
+
const id = publicKeyId(publicKey);
|
|
151
|
+
const existing = store.keys.find((key) => key.publicKeyId === id);
|
|
152
|
+
if (existing) {
|
|
153
|
+
if (existing.publicKey !== publicKey) {
|
|
154
|
+
throw new Error(`public key id collision for ${id}`);
|
|
155
|
+
}
|
|
156
|
+
return store;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
version: 1,
|
|
160
|
+
keys: [
|
|
161
|
+
...store.keys,
|
|
162
|
+
{
|
|
163
|
+
publicKeyId: id,
|
|
164
|
+
publicKey,
|
|
165
|
+
status: "active",
|
|
166
|
+
addedAt,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function revokeTrustAnchor(store, keyId, revokedAt = new Date().toISOString()) {
|
|
172
|
+
assertValidTrustStore(store);
|
|
173
|
+
if (!isIsoDate(revokedAt)) {
|
|
174
|
+
throw new Error("revokedAt must be a canonical ISO-8601 timestamp");
|
|
175
|
+
}
|
|
176
|
+
const existing = store.keys.find((key) => key.publicKeyId === keyId);
|
|
177
|
+
if (!existing) {
|
|
178
|
+
throw new Error(`trusted key '${keyId}' was not found`);
|
|
179
|
+
}
|
|
180
|
+
if (Date.parse(revokedAt) < Date.parse(existing.addedAt)) {
|
|
181
|
+
throw new Error("revokedAt must not be before the key was added");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
version: 1,
|
|
185
|
+
keys: store.keys.map((key) => key.publicKeyId === keyId
|
|
186
|
+
? {
|
|
187
|
+
publicKeyId: key.publicKeyId,
|
|
188
|
+
publicKey: key.publicKey,
|
|
189
|
+
status: "revoked",
|
|
190
|
+
addedAt: key.addedAt,
|
|
191
|
+
revokedAt,
|
|
192
|
+
}
|
|
193
|
+
: key),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export function createKeyRotation(previous, next, rotatedAt = new Date().toISOString()) {
|
|
197
|
+
if (!validateKeyPair(previous) || !validateKeyPair(next)) {
|
|
198
|
+
throw new Error("key rotation requires valid Ed25519 key pairs");
|
|
199
|
+
}
|
|
200
|
+
if (!isIsoDate(rotatedAt)) {
|
|
201
|
+
throw new Error("rotatedAt must be a canonical ISO-8601 timestamp");
|
|
202
|
+
}
|
|
203
|
+
if (previous.publicKeyDer === next.publicKeyDer) {
|
|
204
|
+
throw new Error("key rotation requires a different new key");
|
|
205
|
+
}
|
|
206
|
+
const body = {
|
|
207
|
+
artifactVersion: 1,
|
|
208
|
+
algorithm: "ed25519",
|
|
209
|
+
previousPublicKey: previous.publicKeyDer,
|
|
210
|
+
previousPublicKeyId: publicKeyId(previous.publicKeyDer),
|
|
211
|
+
newPublicKey: next.publicKeyDer,
|
|
212
|
+
newPublicKeyId: publicKeyId(next.publicKeyDer),
|
|
213
|
+
rotatedAt,
|
|
214
|
+
};
|
|
215
|
+
const signature = ed25519Sign(null, signatureMessage("key-rotation", body), privateKeyFromDer(previous.privateKeyDer));
|
|
216
|
+
return { ...body, signature: signature.toString("base64") };
|
|
217
|
+
}
|
|
218
|
+
export function verifyKeyRotation(value) {
|
|
219
|
+
if (!isObject(value)) {
|
|
220
|
+
return {
|
|
221
|
+
valid: false,
|
|
222
|
+
reasonCode: "E_ROTATION_INVALID",
|
|
223
|
+
detail: "key rotation must be an object",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const allowedFields = new Set([
|
|
227
|
+
"artifactVersion",
|
|
228
|
+
"algorithm",
|
|
229
|
+
"previousPublicKey",
|
|
230
|
+
"previousPublicKeyId",
|
|
231
|
+
"newPublicKey",
|
|
232
|
+
"newPublicKeyId",
|
|
233
|
+
"rotatedAt",
|
|
234
|
+
"signature",
|
|
235
|
+
]);
|
|
236
|
+
for (const field of Object.keys(value)) {
|
|
237
|
+
if (!allowedFields.has(field)) {
|
|
238
|
+
return {
|
|
239
|
+
valid: false,
|
|
240
|
+
reasonCode: "E_ROTATION_INVALID",
|
|
241
|
+
detail: `unexpected key rotation field '${field}'`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (value.artifactVersion !== 1) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
reasonCode: "E_ARTIFACT_VERSION_UNSUPPORTED",
|
|
249
|
+
detail: "only key rotation artifactVersion 1 is supported",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (value.algorithm !== "ed25519") {
|
|
253
|
+
return {
|
|
254
|
+
valid: false,
|
|
255
|
+
reasonCode: "E_ALGORITHM_UNSUPPORTED",
|
|
256
|
+
detail: "only ed25519 key rotations are supported",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (typeof value.previousPublicKey !== "string" ||
|
|
260
|
+
value.previousPublicKey.length > 128 ||
|
|
261
|
+
!isCanonicalBase64(value.previousPublicKey) ||
|
|
262
|
+
typeof value.newPublicKey !== "string" ||
|
|
263
|
+
value.newPublicKey.length > 128 ||
|
|
264
|
+
!isCanonicalBase64(value.newPublicKey) ||
|
|
265
|
+
typeof value.previousPublicKeyId !== "string" ||
|
|
266
|
+
typeof value.newPublicKeyId !== "string" ||
|
|
267
|
+
!KEY_ID.test(value.previousPublicKeyId) ||
|
|
268
|
+
!KEY_ID.test(value.newPublicKeyId) ||
|
|
269
|
+
!isIsoDate(value.rotatedAt) ||
|
|
270
|
+
typeof value.signature !== "string" ||
|
|
271
|
+
value.signature.length !== 88 ||
|
|
272
|
+
!isCanonicalBase64(value.signature) ||
|
|
273
|
+
Buffer.from(value.signature, "base64").length !== 64) {
|
|
274
|
+
return {
|
|
275
|
+
valid: false,
|
|
276
|
+
reasonCode: "E_ROTATION_INVALID",
|
|
277
|
+
detail: "key rotation fields are invalid",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (publicKeyId(value.previousPublicKey) !== value.previousPublicKeyId ||
|
|
281
|
+
publicKeyId(value.newPublicKey) !== value.newPublicKeyId) {
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
reasonCode: "E_PUBLIC_KEY_ID_MISMATCH",
|
|
285
|
+
detail: "key rotation publicKeyId does not match its public key",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (value.previousPublicKeyId === value.newPublicKeyId) {
|
|
289
|
+
return {
|
|
290
|
+
valid: false,
|
|
291
|
+
reasonCode: "E_ROTATION_INVALID",
|
|
292
|
+
detail: "key rotation must introduce a different public key",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
publicKeyFromDer(value.previousPublicKey);
|
|
297
|
+
publicKeyFromDer(value.newPublicKey);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return {
|
|
301
|
+
valid: false,
|
|
302
|
+
reasonCode: "E_ROTATION_INVALID",
|
|
303
|
+
detail: "key rotation must contain valid Ed25519 public keys",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const rotation = value;
|
|
307
|
+
const { signature, ...body } = rotation;
|
|
308
|
+
try {
|
|
309
|
+
const valid = ed25519Verify(null, signatureMessage("key-rotation", body), publicKeyFromDer(rotation.previousPublicKey), Buffer.from(signature, "base64"));
|
|
310
|
+
return valid
|
|
311
|
+
? { valid: true, reasonCode: "OK", detail: "key rotation is valid" }
|
|
312
|
+
: {
|
|
313
|
+
valid: false,
|
|
314
|
+
reasonCode: "E_SIGNATURE_INVALID",
|
|
315
|
+
detail: "key rotation signature is invalid",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return {
|
|
320
|
+
valid: false,
|
|
321
|
+
reasonCode: "E_SIGNATURE_CHECK_FAILED",
|
|
322
|
+
detail: "key rotation signature verification failed",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function applyKeyRotation(store, rotation) {
|
|
327
|
+
assertValidTrustStore(store);
|
|
328
|
+
const verification = verifyKeyRotation(rotation);
|
|
329
|
+
if (!verification.valid) {
|
|
330
|
+
throw new Error(`${verification.reasonCode}: ${verification.detail}`);
|
|
331
|
+
}
|
|
332
|
+
const previous = store.keys.find((key) => key.publicKeyId === rotation.previousPublicKeyId);
|
|
333
|
+
if (!previous || previous.publicKey !== rotation.previousPublicKey) {
|
|
334
|
+
throw new Error("rotation previous key is not a trust anchor");
|
|
335
|
+
}
|
|
336
|
+
const next = store.keys.find((key) => key.publicKeyId === rotation.newPublicKeyId);
|
|
337
|
+
if (previous.status === "retired" && next?.status === "active") {
|
|
338
|
+
if (previous.retiredAt === rotation.rotatedAt &&
|
|
339
|
+
next.publicKey === rotation.newPublicKey) {
|
|
340
|
+
return store;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (previous.status !== "active") {
|
|
344
|
+
throw new Error(`rotation previous key is ${previous.status}, not active`);
|
|
345
|
+
}
|
|
346
|
+
if (Date.parse(rotation.rotatedAt) < Date.parse(previous.addedAt)) {
|
|
347
|
+
throw new Error("rotation timestamp is before the previous key was added");
|
|
348
|
+
}
|
|
349
|
+
if (next && next.status !== "active") {
|
|
350
|
+
throw new Error(`rotation new key is already ${next.status}`);
|
|
351
|
+
}
|
|
352
|
+
if (next && next.publicKey !== rotation.newPublicKey) {
|
|
353
|
+
throw new Error(`public key id collision for ${rotation.newPublicKeyId}`);
|
|
354
|
+
}
|
|
355
|
+
const retired = {
|
|
356
|
+
publicKeyId: previous.publicKeyId,
|
|
357
|
+
publicKey: previous.publicKey,
|
|
358
|
+
status: "retired",
|
|
359
|
+
addedAt: previous.addedAt,
|
|
360
|
+
retiredAt: rotation.rotatedAt,
|
|
361
|
+
};
|
|
362
|
+
const active = next ?? {
|
|
363
|
+
publicKeyId: rotation.newPublicKeyId,
|
|
364
|
+
publicKey: rotation.newPublicKey,
|
|
365
|
+
status: "active",
|
|
366
|
+
addedAt: rotation.rotatedAt,
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
version: 1,
|
|
370
|
+
keys: [
|
|
371
|
+
...store.keys.filter((key) => key.publicKeyId !== rotation.previousPublicKeyId &&
|
|
372
|
+
key.publicKeyId !== rotation.newPublicKeyId),
|
|
373
|
+
retired,
|
|
374
|
+
{ ...active, status: "active" },
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
export function checkTrustedKey(store, publicKey, artifactTimestamp, purpose = "verify", now = new Date()) {
|
|
379
|
+
const storeValidation = validateTrustStore(store);
|
|
380
|
+
if (!storeValidation.ok) {
|
|
381
|
+
return {
|
|
382
|
+
valid: false,
|
|
383
|
+
reasonCode: "E_TRUST_STORE_INVALID",
|
|
384
|
+
detail: storeValidation.errors.join("; "),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (!isIsoDate(artifactTimestamp)) {
|
|
388
|
+
return {
|
|
389
|
+
valid: false,
|
|
390
|
+
reasonCode: "E_ARTIFACT_TIMESTAMP_INVALID",
|
|
391
|
+
detail: "artifact timestamp must be canonical ISO-8601",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (!Number.isFinite(now.getTime()) ||
|
|
395
|
+
Date.parse(artifactTimestamp) > now.getTime() + MAX_CLOCK_SKEW_MS) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
reasonCode: "E_ARTIFACT_TIMESTAMP_FUTURE",
|
|
399
|
+
detail: "artifact timestamp is beyond the allowed clock skew",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
let id;
|
|
403
|
+
try {
|
|
404
|
+
publicKeyFromDer(publicKey);
|
|
405
|
+
id = publicKeyId(publicKey);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return {
|
|
409
|
+
valid: false,
|
|
410
|
+
reasonCode: "E_PUBLIC_KEY_INVALID",
|
|
411
|
+
detail: "artifact public key is not valid Ed25519 key material",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const anchor = store.keys.find((key) => key.publicKeyId === id);
|
|
415
|
+
if (!anchor) {
|
|
416
|
+
return {
|
|
417
|
+
valid: false,
|
|
418
|
+
reasonCode: "E_KEY_UNTRUSTED",
|
|
419
|
+
detail: `public key ${id} is not in the trust store`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (anchor.publicKey !== publicKey) {
|
|
423
|
+
return {
|
|
424
|
+
valid: false,
|
|
425
|
+
reasonCode: "E_TRUST_ANCHOR_MISMATCH",
|
|
426
|
+
detail: `trust anchor ${id} does not match the artifact public key`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (anchor.status === "revoked") {
|
|
430
|
+
return {
|
|
431
|
+
valid: false,
|
|
432
|
+
reasonCode: "E_KEY_REVOKED",
|
|
433
|
+
detail: `public key ${id} is revoked`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (anchor.status === "retired") {
|
|
437
|
+
if (purpose === "admit") {
|
|
438
|
+
return {
|
|
439
|
+
valid: false,
|
|
440
|
+
reasonCode: "E_KEY_RETIRED",
|
|
441
|
+
detail: `public key ${id} is retired for new admissions`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (!anchor.retiredAt ||
|
|
445
|
+
Date.parse(artifactTimestamp) > Date.parse(anchor.retiredAt)) {
|
|
446
|
+
return {
|
|
447
|
+
valid: false,
|
|
448
|
+
reasonCode: "E_KEY_RETIRED",
|
|
449
|
+
detail: `artifact was signed after public key ${id} was retired`,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
valid: true,
|
|
455
|
+
reasonCode: "OK",
|
|
456
|
+
detail: `public key ${id} is trusted`,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
export function verifyTrustedSignedManifest(value, store, purpose = "verify") {
|
|
460
|
+
const signature = verifySignedManifest(value);
|
|
461
|
+
if (!signature.valid) {
|
|
462
|
+
return signature;
|
|
463
|
+
}
|
|
464
|
+
const signed = value;
|
|
465
|
+
return checkTrustedKey(store, signed.publicKey, signed.signedAt, purpose);
|
|
466
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface Manifest {
|
|
|
18
18
|
tools: ToolDefinition[];
|
|
19
19
|
}
|
|
20
20
|
export interface SignedManifest {
|
|
21
|
+
artifactVersion: 1;
|
|
21
22
|
manifest: Manifest;
|
|
22
23
|
manifestHash: string;
|
|
23
24
|
algorithm: "ed25519";
|
|
@@ -34,6 +35,7 @@ export interface AdmissionDecision {
|
|
|
34
35
|
agentId?: string;
|
|
35
36
|
}
|
|
36
37
|
export interface Receipt {
|
|
38
|
+
artifactVersion: 1;
|
|
37
39
|
receiptId: string;
|
|
38
40
|
manifestHash: string;
|
|
39
41
|
toolName: string;
|
|
@@ -61,3 +63,26 @@ export interface GrantDecision {
|
|
|
61
63
|
toolName: string;
|
|
62
64
|
detail: string;
|
|
63
65
|
}
|
|
66
|
+
export type TrustKeyStatus = "active" | "retired" | "revoked";
|
|
67
|
+
export interface TrustAnchor {
|
|
68
|
+
publicKeyId: string;
|
|
69
|
+
publicKey: string;
|
|
70
|
+
status: TrustKeyStatus;
|
|
71
|
+
addedAt: string;
|
|
72
|
+
retiredAt?: string;
|
|
73
|
+
revokedAt?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface TrustStore {
|
|
76
|
+
version: 1;
|
|
77
|
+
keys: TrustAnchor[];
|
|
78
|
+
}
|
|
79
|
+
export interface KeyRotation {
|
|
80
|
+
artifactVersion: 1;
|
|
81
|
+
algorithm: "ed25519";
|
|
82
|
+
previousPublicKey: string;
|
|
83
|
+
previousPublicKeyId: string;
|
|
84
|
+
newPublicKey: string;
|
|
85
|
+
newPublicKeyId: string;
|
|
86
|
+
rotatedAt: string;
|
|
87
|
+
signature: string;
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dorigjo/besa",
|
|
3
|
-
"version": "0.1.0-
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/sdk.js",
|
|
7
|
-
"types": "./dist/sdk.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/sdk.d.ts",
|
|
11
|
-
"default": "./dist/sdk.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"bin": {
|
|
15
|
-
"besa": "
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsc",
|
|
19
|
-
"test": "npm run build && node
|
|
20
|
-
"smoke": "node scripts/smoke.mjs"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@dorigjo/besa",
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
|
+
"description": "Cryptographic execution evidence for AI-agent tool calls: signed manifests, admission decisions, and tamper-evident receipts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/sdk.js",
|
|
7
|
+
"types": "./dist/sdk.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/sdk.d.ts",
|
|
11
|
+
"default": "./dist/sdk.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"besa": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"test": "npm run build && node dist/tests/run.js",
|
|
20
|
+
"smoke": "node scripts/smoke.mjs",
|
|
21
|
+
"test:package": "node scripts/package-smoke.mjs",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"ai-agents",
|
|
28
|
+
"agent-security",
|
|
29
|
+
"trust",
|
|
30
|
+
"execution-evidence",
|
|
31
|
+
"audit-readiness",
|
|
32
|
+
"ed25519",
|
|
33
|
+
"signing",
|
|
34
|
+
"manifest",
|
|
35
|
+
"receipt",
|
|
36
|
+
"attestation"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/dorigjo/besa.git"
|
|
41
|
+
},
|
|
42
|
+
"author": "dorigjo",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"yaml": "^2.5.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^24.0.0",
|
|
49
|
+
"typescript": "^5.6.2"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist/*.js",
|
|
56
|
+
"dist/*.d.ts",
|
|
57
|
+
"examples/manifest.yaml",
|
|
58
|
+
"examples/grants.yaml",
|
|
59
|
+
"examples/request.json",
|
|
60
|
+
"scripts/postinstall.mjs"
|
|
61
|
+
],
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/dorigjo/besa/issues"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/dorigjo/besa#readme"
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// Skip in CI, non-TTY, or piped output — never block automated installs
|
|
9
|
+
if (!process.stdout.isTTY || process.env.CI || process.env.NO_COLOR) {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(
|
|
15
|
+
readFileSync(join(__dirname, "..", "package.json"), "utf8"),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
const RED = "\x1b[31m";
|
|
20
|
+
const BOLD = "\x1b[1m";
|
|
21
|
+
const DIM = "\x1b[2m";
|
|
22
|
+
|
|
23
|
+
process.stdout.write([
|
|
24
|
+
"",
|
|
25
|
+
` ${BOLD}${RED}BESA${RESET}`,
|
|
26
|
+
` ${DIM}Signed trust for AI-agent tools.${RESET}`,
|
|
27
|
+
` ${DIM}v${pkg.version}${RESET}`,
|
|
28
|
+
"",
|
|
29
|
+
"",
|
|
30
|
+
].join("\n"));
|