sello 0.1.1 → 0.1.2
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/cbor.js +337 -0
- package/dist/cli/bench.js +389 -0
- package/dist/cli/demo.js +113 -0
- package/dist/cli/sello.js +515 -0
- package/dist/cose/protected-header.js +210 -0
- package/dist/cose/sign1.js +124 -0
- package/dist/crypto/ed25519.js +117 -0
- package/dist/crypto/identifiers.js +64 -0
- package/dist/hpke/base.js +349 -0
- package/dist/hpke/receipt.js +79 -0
- package/dist/index.js +15 -0
- package/dist/log/canonical-url.js +168 -0
- package/dist/log/mock-log.js +147 -0
- package/dist/log/rekor.js +120 -0
- package/dist/log/types.js +0 -0
- package/dist/mcp/middleware.js +162 -0
- package/dist/owner/verify.js +271 -0
- package/dist/receipt/body.js +210 -0
- package/dist/registry/json-registry.js +233 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/keys.js +191 -0
- package/dist/sdk/logs.js +196 -0
- package/dist/sdk/publisher.js +106 -0
- package/dist/sdk/service.js +561 -0
- package/dist/service/create-receipt.js +174 -0
- package/dist/token/jws-profile.js +174 -0
- package/docs/release-checklist.md +1 -0
- package/package.json +9 -5
package/dist/cbor.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export function cborTag(tag , value ) {
|
|
19
|
+
assertNonNegativeSafeInteger(tag, "tag");
|
|
20
|
+
return { tag, value };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function encodeCbor(value ) {
|
|
24
|
+
if (typeof value === "number") {
|
|
25
|
+
return encodeInteger(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return concat([encodeLength(3, utf8ByteLength(value)), encodeUtf8(value)]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (value instanceof Uint8Array) {
|
|
33
|
+
return concat([encodeLength(2, value.byteLength), value]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return concat([
|
|
38
|
+
encodeLength(4, value.length),
|
|
39
|
+
...value.map((entry) => encodeCbor(entry)),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value instanceof Map) {
|
|
44
|
+
return encodeMap(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isTagged(value)) {
|
|
48
|
+
return concat([encodeLength(6, value.tag), encodeCbor(value.value)]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new TypeError("unsupported CBOR value");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function decodeCbor(bytes ) {
|
|
55
|
+
const reader = new CborReader(bytes);
|
|
56
|
+
const value = reader.readValue();
|
|
57
|
+
reader.assertDone();
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function concat(parts ) {
|
|
62
|
+
const length = parts.reduce((sum, part) => sum + part.byteLength, 0);
|
|
63
|
+
const out = new Uint8Array(length);
|
|
64
|
+
let offset = 0;
|
|
65
|
+
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
out.set(part, offset);
|
|
68
|
+
offset += part.byteLength;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function encodeInteger(value ) {
|
|
75
|
+
if (!Number.isSafeInteger(value)) {
|
|
76
|
+
throw new TypeError("CBOR integer must be a safe integer");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value >= 0) {
|
|
80
|
+
return encodeLength(0, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return encodeLength(1, -1 - value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function encodeMap(value ) {
|
|
87
|
+
const entries = [...value.entries()].map(([key, entryValue]) => {
|
|
88
|
+
if (typeof key !== "number" && typeof key !== "string") {
|
|
89
|
+
throw new TypeError("CBOR map keys must be strings or numbers");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
key: encodeCbor(key),
|
|
94
|
+
value: encodeCbor(entryValue),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
entries.sort((a, b) => compareBytes(a.key, b.key));
|
|
99
|
+
|
|
100
|
+
for (let index = 1; index < entries.length; index += 1) {
|
|
101
|
+
if (compareBytes(entries[index - 1].key, entries[index].key) === 0) {
|
|
102
|
+
throw new TypeError("CBOR map contains duplicate keys");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return concat([
|
|
107
|
+
encodeLength(5, entries.length),
|
|
108
|
+
...entries.flatMap((entry) => [entry.key, entry.value]),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function encodeLength(majorType , value ) {
|
|
113
|
+
assertNonNegativeSafeInteger(value, "CBOR length/value");
|
|
114
|
+
const prefix = majorType << 5;
|
|
115
|
+
|
|
116
|
+
if (value < 24) {
|
|
117
|
+
return Uint8Array.of(prefix | value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (value <= 0xff) {
|
|
121
|
+
return Uint8Array.of(prefix | 24, value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (value <= 0xffff) {
|
|
125
|
+
return Uint8Array.of(prefix | 25, value >> 8, value & 0xff);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (value <= 0xffffffff) {
|
|
129
|
+
return Uint8Array.of(
|
|
130
|
+
prefix | 26,
|
|
131
|
+
(value >>> 24) & 0xff,
|
|
132
|
+
(value >>> 16) & 0xff,
|
|
133
|
+
(value >>> 8) & 0xff,
|
|
134
|
+
value & 0xff,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw new RangeError("CBOR values larger than uint32 are not supported yet");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compareBytes(a , b ) {
|
|
142
|
+
const length = Math.min(a.byteLength, b.byteLength);
|
|
143
|
+
|
|
144
|
+
for (let index = 0; index < length; index += 1) {
|
|
145
|
+
const diff = a[index] - b[index];
|
|
146
|
+
if (diff !== 0) {
|
|
147
|
+
return diff;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return a.byteLength - b.byteLength;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function assertNonNegativeSafeInteger(value , name ) {
|
|
155
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
156
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isTagged(value ) {
|
|
161
|
+
return (
|
|
162
|
+
typeof value === "object" &&
|
|
163
|
+
value !== null &&
|
|
164
|
+
"tag" in value &&
|
|
165
|
+
"value" in value
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const textEncoder = new TextEncoder();
|
|
170
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
171
|
+
|
|
172
|
+
function encodeUtf8(value ) {
|
|
173
|
+
return textEncoder.encode(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function decodeUtf8(value ) {
|
|
177
|
+
return textDecoder.decode(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function utf8ByteLength(value ) {
|
|
181
|
+
return encodeUtf8(value).byteLength;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class CborReader {
|
|
185
|
+
#bytes ;
|
|
186
|
+
#offset = 0;
|
|
187
|
+
|
|
188
|
+
constructor(bytes ) {
|
|
189
|
+
this.#bytes = bytes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
readValue() {
|
|
193
|
+
const initialByte = this.readByte();
|
|
194
|
+
const majorType = initialByte >> 5;
|
|
195
|
+
const additionalInfo = initialByte & 0x1f;
|
|
196
|
+
|
|
197
|
+
if (majorType === 0) {
|
|
198
|
+
return this.readLength(additionalInfo);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (majorType === 1) {
|
|
202
|
+
return -1 - this.readLength(additionalInfo);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (majorType === 2) {
|
|
206
|
+
const length = this.readLength(additionalInfo);
|
|
207
|
+
return this.readBytes(length);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (majorType === 3) {
|
|
211
|
+
const length = this.readLength(additionalInfo);
|
|
212
|
+
return decodeUtf8(this.readBytes(length));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (majorType === 4) {
|
|
216
|
+
const length = this.readLength(additionalInfo);
|
|
217
|
+
return this.readArray(length);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (majorType === 5) {
|
|
221
|
+
const length = this.readLength(additionalInfo);
|
|
222
|
+
return this.readMap(length);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (majorType === 6) {
|
|
226
|
+
const tag = this.readLength(additionalInfo);
|
|
227
|
+
return cborTag(tag, this.readValue());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
throw new TypeError(`unsupported CBOR major type ${majorType}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
assertDone() {
|
|
234
|
+
if (this.#offset !== this.#bytes.byteLength) {
|
|
235
|
+
throw new TypeError("CBOR data has trailing bytes");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
readArray(length ) {
|
|
240
|
+
const out = [];
|
|
241
|
+
|
|
242
|
+
for (let index = 0; index < length; index += 1) {
|
|
243
|
+
out.push(this.readValue());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
readMap(length ) {
|
|
250
|
+
const out = new Map();
|
|
251
|
+
let previousEncodedKey ;
|
|
252
|
+
|
|
253
|
+
for (let index = 0; index < length; index += 1) {
|
|
254
|
+
const keyStart = this.#offset;
|
|
255
|
+
const key = this.readValue();
|
|
256
|
+
const encodedKey = this.#bytes.subarray(keyStart, this.#offset);
|
|
257
|
+
|
|
258
|
+
if (typeof key !== "string" && typeof key !== "number") {
|
|
259
|
+
throw new TypeError("CBOR map keys must be strings or numbers");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (previousEncodedKey && compareBytes(previousEncodedKey, encodedKey) >= 0) {
|
|
263
|
+
throw new TypeError("CBOR map keys are not in deterministic order");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
previousEncodedKey = encodedKey;
|
|
267
|
+
out.set(key, this.readValue());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
readLength(additionalInfo ) {
|
|
274
|
+
if (additionalInfo < 24) {
|
|
275
|
+
return additionalInfo;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (additionalInfo === 24) {
|
|
279
|
+
const value = this.readByte();
|
|
280
|
+
if (value < 24) {
|
|
281
|
+
throw new TypeError("CBOR integer/length is not minimally encoded");
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (additionalInfo === 25) {
|
|
287
|
+
const value = this.readUint16();
|
|
288
|
+
if (value <= 0xff) {
|
|
289
|
+
throw new TypeError("CBOR integer/length is not minimally encoded");
|
|
290
|
+
}
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (additionalInfo === 26) {
|
|
295
|
+
const value = this.readUint32();
|
|
296
|
+
if (value <= 0xffff) {
|
|
297
|
+
throw new TypeError("CBOR integer/length is not minimally encoded");
|
|
298
|
+
}
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
throw new TypeError("CBOR indefinite or uint64 lengths are not supported");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
readByte() {
|
|
306
|
+
if (this.#offset >= this.#bytes.byteLength) {
|
|
307
|
+
throw new TypeError("unexpected end of CBOR data");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return this.#bytes[this.#offset++];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
readBytes(length ) {
|
|
314
|
+
const end = this.#offset + length;
|
|
315
|
+
|
|
316
|
+
if (end > this.#bytes.byteLength) {
|
|
317
|
+
throw new TypeError("unexpected end of CBOR data");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const out = this.#bytes.subarray(this.#offset, end);
|
|
321
|
+
this.#offset = end;
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
readUint16() {
|
|
326
|
+
return (this.readByte() << 8) | this.readByte();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
readUint32() {
|
|
330
|
+
return (
|
|
331
|
+
(this.readByte() * 0x1000000) +
|
|
332
|
+
(this.readByte() << 16) +
|
|
333
|
+
(this.readByte() << 8) +
|
|
334
|
+
this.readByte()
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -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
|
+
}
|