sello 0.1.1 → 0.1.3

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.
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { performance } from "node:perf_hooks";
4
+
5
+ import { encodeReceiptBody } from "../receipt/body.js";
6
+ import { decodeReceiptEnvelope, generateEd25519KeyPair } from "../cose/sign1.js";
7
+ import { toHex } from "../crypto/identifiers.js";
8
+ import { generateHpkeKeyPair } from "../hpke/receipt.js";
9
+ import { MockTransparencyLog } from "../log/mock-log.js";
10
+ import { verifyReceipts } from "../owner/verify.js";
11
+ import {
12
+ loadSignedRegistry,
13
+ signRegistryJson,
14
+ } from "../registry/json-registry.js";
15
+ import {
16
+ createReceiptFromJwsToken,
17
+
18
+ } from "../service/create-receipt.js";
19
+ import { base64urlEncode, signSelloJwsToken } from "../token/jws-profile.js";
20
+
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
+ const DEFAULT_WARMUP_ITERATIONS = 500;
56
+
57
+ const textEncoder = new TextEncoder();
58
+ const logUrl = "https://rekor.example.com/api" ;
59
+ const serviceIdentifier = "github.com/mcp/v1";
60
+
61
+ const options = parseArgs(process.argv.slice(2));
62
+ const result = runBenchmark(options.iterations, options.warmupIterations);
63
+
64
+ if (options.json) {
65
+ console.log(JSON.stringify(result, null, 2));
66
+ } else {
67
+ printText(result);
68
+ }
69
+
70
+ function runBenchmark(iterations , warmupIterations ) {
71
+ const sampleFixture = makeFixture();
72
+ const sampleReceipt = createBenchReceipt(sampleFixture, 0);
73
+ const sampleEnvelope = decodeReceiptEnvelope(sampleReceipt.envelope);
74
+ const sizes = {
75
+ receipt_body_cbor_bytes: encodeReceiptBody(sampleReceipt.receiptBody).byteLength,
76
+ protected_header_bytes: sampleReceipt.protectedHeaderBytes.byteLength,
77
+ hpke_payload_bytes: sampleEnvelope.payload.byteLength,
78
+ cose_sign1_envelope_bytes: sampleReceipt.envelope.byteLength,
79
+ mock_log_proof_json_bytes: textEncoder.encode(
80
+ JSON.stringify(sampleReceipt.logEntry.proof),
81
+ ).byteLength,
82
+ };
83
+
84
+ // When --expose-gc is set, force a baseline GC before warmup so the steady-state
85
+ // sample isn't contaminated by collection of size-sample allocations. Silently
86
+ // skipped if the flag isn't present.
87
+ if (typeof globalThis.gc === "function") {
88
+ globalThis.gc();
89
+ }
90
+
91
+ warmup(warmupIterations);
92
+
93
+ const createFixture = makeFixture();
94
+ const createDurations = new Array (iterations);
95
+ for (let index = 0; index < iterations; index += 1) {
96
+ const t0 = performance.now();
97
+ createBenchReceipt(createFixture, index);
98
+ createDurations[index] = performance.now() - t0;
99
+ }
100
+
101
+ const verifyOneFixture = makeFixture();
102
+ createBenchReceipt(verifyOneFixture, 0);
103
+ const verifyOneDurations = new Array (iterations);
104
+ for (let index = 0; index < iterations; index += 1) {
105
+ const t0 = performance.now();
106
+ const result = verifyReceipts(verifyOneFixture.ownerInput());
107
+ verifyOneDurations[index] = performance.now() - t0;
108
+ assertVerifiedCount(result.receipts.length, 1);
109
+ }
110
+
111
+ const batchFixture = makeFixture();
112
+ for (let index = 0; index < iterations; index += 1) {
113
+ createBenchReceipt(batchFixture, index);
114
+ }
115
+ const verifyBatchTiming = time(() => {
116
+ const result = verifyReceipts(batchFixture.ownerInput());
117
+ assertVerifiedCount(result.receipts.length, iterations);
118
+ });
119
+
120
+ const createTotal = sum(createDurations);
121
+ const verifyOneTotal = sum(verifyOneDurations);
122
+
123
+ return {
124
+ iterations,
125
+ warmup_iterations: warmupIterations,
126
+ node: process.version,
127
+ sizes,
128
+ timings_ms: {
129
+ create_receipt_avg: roundMs(createTotal / iterations),
130
+ verify_one_receipt_avg: roundMs(verifyOneTotal / iterations),
131
+ verify_batch_total: roundMs(verifyBatchTiming),
132
+ verify_batch_per_receipt: roundMs(verifyBatchTiming / iterations),
133
+ },
134
+ distributions: {
135
+ create_receipt: summarize(createDurations),
136
+ verify_one_receipt: summarize(verifyOneDurations),
137
+ verify_batch_total: { count: 1, value: roundMs(verifyBatchTiming) },
138
+ verify_batch_per_receipt: {
139
+ count: 1,
140
+ value: roundMs(verifyBatchTiming / iterations),
141
+ },
142
+ },
143
+ };
144
+ }
145
+
146
+ function warmup(count ) {
147
+ const fixture = makeFixture();
148
+ for (let index = 0; index < count; index += 1) {
149
+ createBenchReceipt(fixture, index);
150
+ }
151
+ const verifyFixture = makeFixture();
152
+ createBenchReceipt(verifyFixture, 0);
153
+ for (let index = 0; index < count; index += 1) {
154
+ const result = verifyReceipts(verifyFixture.ownerInput());
155
+ assertVerifiedCount(result.receipts.length, 1);
156
+ }
157
+ }
158
+
159
+ function summarize(durations ) {
160
+ if (durations.length === 0) {
161
+ throw new Error("cannot summarize empty distribution");
162
+ }
163
+ const sorted = durations.slice().sort((a, b) => a - b);
164
+ const count = sorted.length;
165
+ const total = sum(sorted);
166
+ const mean = total / count;
167
+ const variance =
168
+ sorted.reduce((acc, value) => acc + (value - mean) ** 2, 0) / count;
169
+ return {
170
+ count,
171
+ mean: roundMs(mean),
172
+ median: roundMs(percentile(sorted, 0.5)),
173
+ p95: roundMs(percentile(sorted, 0.95)),
174
+ p99: roundMs(percentile(sorted, 0.99)),
175
+ stddev: roundMs(Math.sqrt(variance)),
176
+ };
177
+ }
178
+
179
+ function percentile(sortedAscending , quantile ) {
180
+ if (sortedAscending.length === 1) {
181
+ return sortedAscending[0];
182
+ }
183
+ const rank = quantile * (sortedAscending.length - 1);
184
+ const lower = Math.floor(rank);
185
+ const upper = Math.ceil(rank);
186
+ if (lower === upper) {
187
+ return sortedAscending[lower];
188
+ }
189
+ const fraction = rank - lower;
190
+ return sortedAscending[lower] * (1 - fraction) + sortedAscending[upper] * fraction;
191
+ }
192
+
193
+ function sum(values ) {
194
+ let total = 0;
195
+ for (const value of values) {
196
+ total += value;
197
+ }
198
+ return total;
199
+ }
200
+
201
+ function makeFixture() {
202
+ const owner = generateHpkeKeyPair();
203
+ const service = generateEd25519KeyPair();
204
+ const trustRoot = generateEd25519KeyPair();
205
+ const tokenIssuer = generateEd25519KeyPair();
206
+ const serviceKid = textEncoder.encode("github-mcp-v1-2026-q2");
207
+ const log = new MockTransparencyLog(logUrl);
208
+ const authorizationToken = signSelloJwsToken({
209
+ issuerPrivateKey: tokenIssuer.privateKey,
210
+ payload: {
211
+ sub: "bench-agent",
212
+ owner_hpke_pk: base64urlEncode(owner.publicKey),
213
+ sello_logs: [logUrl],
214
+ },
215
+ });
216
+ const registryBytes = textEncoder.encode(
217
+ JSON.stringify({
218
+ [toHex(serviceKid)]: {
219
+ service_identifier: serviceIdentifier,
220
+ public_key_ed25519: Buffer.from(service.publicKey).toString("base64url"),
221
+ },
222
+ }),
223
+ );
224
+ const registry = loadSignedRegistry({
225
+ registryBytes,
226
+ signatureBase64Url: signRegistryJson(registryBytes, trustRoot.privateKey),
227
+ trustRootPublicKey: trustRoot.publicKey,
228
+ });
229
+
230
+ return {
231
+ authorizationToken,
232
+ tokenIssuerPublicKey: tokenIssuer.publicKey,
233
+ serviceKid,
234
+ servicePrivateKey: service.privateKey,
235
+ serviceIdentifier,
236
+ log,
237
+ ownerInput: () => ({
238
+ authorizationTokenBytes: textEncoder.encode(authorizationToken),
239
+ trustedLogs: [log],
240
+ registry,
241
+ ownerPrivateKey: owner.privateKey,
242
+ }),
243
+ };
244
+ }
245
+
246
+ function createBenchReceipt(
247
+ fixture ,
248
+ index ,
249
+ ) {
250
+ return createReceiptFromJwsToken({
251
+ authorizationToken: fixture.authorizationToken,
252
+ tokenIssuerPublicKey: fixture.tokenIssuerPublicKey,
253
+ serviceKid: fixture.serviceKid,
254
+ servicePrivateKey: fixture.servicePrivateKey,
255
+ serviceIdentifier: fixture.serviceIdentifier,
256
+ log: fixture.log,
257
+ actionType: "tools/call",
258
+ actionInputBytes: textEncoder.encode(`bench input ${index}`),
259
+ actionOutputBytes: textEncoder.encode(`bench output ${index}`),
260
+ resultStatus: "success",
261
+ timestamp: timestampForIndex(index),
262
+ });
263
+ }
264
+
265
+ function timestampForIndex(index ) {
266
+ const base = Date.parse("2026-05-28T10:00:00Z");
267
+ return new Date(base + index * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
268
+ }
269
+
270
+ function time(callback ) {
271
+ const start = performance.now();
272
+ callback();
273
+ return performance.now() - start;
274
+ }
275
+
276
+ function parseArgs(args )
277
+
278
+
279
+
280
+ {
281
+ let iterations = 100;
282
+ let warmupIterations = DEFAULT_WARMUP_ITERATIONS;
283
+ let json = false;
284
+
285
+ for (let index = 0; index < args.length; index += 1) {
286
+ const arg = args[index];
287
+
288
+ if (arg === "--json") {
289
+ json = true;
290
+ continue;
291
+ }
292
+
293
+ if (arg === "--iterations") {
294
+ const raw = args[index + 1];
295
+ index += 1;
296
+ iterations = parseIterations(raw);
297
+ continue;
298
+ }
299
+
300
+ if (arg.startsWith("--iterations=")) {
301
+ iterations = parseIterations(arg.slice("--iterations=".length));
302
+ continue;
303
+ }
304
+
305
+ if (arg === "--warmup") {
306
+ const raw = args[index + 1];
307
+ index += 1;
308
+ warmupIterations = parseWarmupIterations(raw);
309
+ continue;
310
+ }
311
+
312
+ if (arg.startsWith("--warmup=")) {
313
+ warmupIterations = parseWarmupIterations(arg.slice("--warmup=".length));
314
+ continue;
315
+ }
316
+
317
+ if (arg === "--help") {
318
+ printHelp();
319
+ process.exit(0);
320
+ }
321
+
322
+ throw new TypeError(`unknown argument: ${arg}`);
323
+ }
324
+
325
+ return { iterations, warmupIterations, json };
326
+ }
327
+
328
+ function parseIterations(value ) {
329
+ const iterations = Number(value);
330
+
331
+ if (!Number.isSafeInteger(iterations) || iterations < 1) {
332
+ throw new TypeError("--iterations must be a positive integer");
333
+ }
334
+
335
+ return iterations;
336
+ }
337
+
338
+ function parseWarmupIterations(value ) {
339
+ const iterations = Number(value);
340
+
341
+ if (!Number.isSafeInteger(iterations) || iterations < 0) {
342
+ throw new TypeError("--warmup must be a non-negative integer");
343
+ }
344
+
345
+ return iterations;
346
+ }
347
+
348
+ function assertVerifiedCount(actual , expected ) {
349
+ if (actual !== expected) {
350
+ throw new Error(`expected ${expected} verified receipts, got ${actual}`);
351
+ }
352
+ }
353
+
354
+ function roundMs(value ) {
355
+ return Math.round(value * 1000) / 1000;
356
+ }
357
+
358
+ function printText(result ) {
359
+ console.log(
360
+ `Sello benchmark (${result.iterations} iterations, ${result.warmup_iterations} warmup, ${result.node})`,
361
+ );
362
+ console.log("");
363
+ console.log("Receipt sizes:");
364
+ for (const [name, value] of Object.entries(result.sizes)) {
365
+ console.log(` ${name}: ${value} bytes`);
366
+ }
367
+ console.log("");
368
+ console.log("Timings:");
369
+ for (const [name, value] of Object.entries(result.timings_ms)) {
370
+ console.log(` ${name}: ${value} ms`);
371
+ }
372
+ console.log("");
373
+ console.log("Distributions:");
374
+ printDistribution("create_receipt", result.distributions.create_receipt);
375
+ printDistribution("verify_one_receipt", result.distributions.verify_one_receipt);
376
+ }
377
+
378
+ function printHelp() {
379
+ console.log(`Usage: sello-bench [--iterations N] [--warmup N] [--json]
380
+
381
+ Runs a local benchmark over the mock-log Sello receipt flow.
382
+ Results are useful for rough regression tracking, not formal crypto benchmarks.`);
383
+ }
384
+
385
+ function printDistribution(name , distribution ) {
386
+ console.log(
387
+ ` ${name}: mean ${distribution.mean} ms, median ${distribution.median} ms, p95 ${distribution.p95} ms, p99 ${distribution.p99} ms, stddev ${distribution.stddev} ms`,
388
+ );
389
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { encodeCbor } from "../cbor.js";
4
+ import { decodeReceiptEnvelope, generateEd25519KeyPair } from "../cose/sign1.js";
5
+ import { toHex } from "../crypto/identifiers.js";
6
+ import { generateHpkeKeyPair } from "../hpke/receipt.js";
7
+ import { MockTransparencyLog } from "../log/mock-log.js";
8
+ import { verifyReceipts } from "../owner/verify.js";
9
+ import {
10
+ loadSignedRegistry,
11
+ signRegistryJson,
12
+ } from "../registry/json-registry.js";
13
+ import { createReceiptFromJwsToken } from "../service/create-receipt.js";
14
+ import { base64urlEncode, signSelloJwsToken } from "../token/jws-profile.js";
15
+
16
+ const textEncoder = new TextEncoder();
17
+ const logUrl = "https://rekor.example.com/api" ;
18
+ const serviceIdentifier = "github.com/mcp/v1";
19
+
20
+ const owner = generateHpkeKeyPair();
21
+ const service = generateEd25519KeyPair();
22
+ const trustRoot = generateEd25519KeyPair();
23
+ const tokenIssuer = generateEd25519KeyPair();
24
+ const serviceKid = textEncoder.encode("github-mcp-v1-2026-q2");
25
+ const log = new MockTransparencyLog(logUrl);
26
+ const authorizationToken = signSelloJwsToken({
27
+ issuerPrivateKey: tokenIssuer.privateKey,
28
+ payload: {
29
+ sub: "demo-agent",
30
+ owner_hpke_pk: base64urlEncode(owner.publicKey),
31
+ sello_logs: [logUrl],
32
+ },
33
+ });
34
+ const registryBytes = textEncoder.encode(
35
+ JSON.stringify({
36
+ [toHex(serviceKid)]: {
37
+ service_identifier: serviceIdentifier,
38
+ public_key_ed25519: Buffer.from(service.publicKey).toString("base64url"),
39
+ },
40
+ }),
41
+ );
42
+ const registry = loadSignedRegistry({
43
+ registryBytes,
44
+ signatureBase64Url: signRegistryJson(registryBytes, trustRoot.privateKey),
45
+ trustRootPublicKey: trustRoot.publicKey,
46
+ });
47
+
48
+ const created = [
49
+ createDemoReceipt("success", "2026-05-28T10:00:00Z", "issue created"),
50
+ createDemoReceipt("error", "2026-05-28T10:00:01Z", "service error"),
51
+ createDemoReceipt("denied", "2026-05-28T10:00:02Z", "ignored"),
52
+ ];
53
+
54
+ if (process.argv.includes("--tamper")) {
55
+ const decoded = decodeReceiptEnvelope(created[0].envelope);
56
+ log.append(
57
+ encodeCbor([
58
+ decoded.protectedBytes,
59
+ new Map(),
60
+ textEncoder.encode("tampered payload"),
61
+ decoded.signature,
62
+ ]),
63
+ "2026-05-28T10:00:03Z",
64
+ );
65
+ }
66
+
67
+ const result = verifyReceipts({
68
+ authorizationTokenBytes: textEncoder.encode(authorizationToken),
69
+ trustedLogs: [log],
70
+ registry,
71
+ ownerPrivateKey: owner.privateKey,
72
+ });
73
+
74
+ console.log(
75
+ JSON.stringify(
76
+ {
77
+ receipts: result.receipts.map((record) => ({
78
+ service: record.serviceIdentifier,
79
+ "action-type": record.receipt["action-type"],
80
+ "result-status": record.receipt["result-status"],
81
+ timestamp: record.receipt.timestamp,
82
+ verified: record.status === "valid",
83
+ status: record.status,
84
+ })),
85
+ rejected: result.rejected.map((record) => ({
86
+ code: record.code,
87
+ message: record.message,
88
+ })),
89
+ },
90
+ null,
91
+ 2,
92
+ ),
93
+ );
94
+
95
+ function createDemoReceipt(
96
+ resultStatus ,
97
+ timestamp ,
98
+ outputText ,
99
+ ) {
100
+ return createReceiptFromJwsToken({
101
+ authorizationToken,
102
+ tokenIssuerPublicKey: tokenIssuer.publicKey,
103
+ serviceKid,
104
+ servicePrivateKey: service.privateKey,
105
+ serviceIdentifier,
106
+ log,
107
+ actionType: "tools/call",
108
+ actionInputBytes: textEncoder.encode(`demo ${resultStatus} input`),
109
+ actionOutputBytes: textEncoder.encode(outputText),
110
+ resultStatus,
111
+ timestamp,
112
+ });
113
+ }