@svrnsec/pulse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * @sovereign/pulse — SVRN Registry Signature Serializer
3
+ *
4
+ * The registry is a crowdsourced database of device "Silicon Signatures" —
5
+ * compact, privacy-safe profiles that characterise a device class rather than
6
+ * an individual device. Developers submit signatures from their test hardware
7
+ * to help calibrate the classifier for hardware we haven't seen yet.
8
+ *
9
+ * A signature captures:
10
+ * - The statistical "shape" of the timing distribution (not raw timings)
11
+ * - The provider/hypervisor classification
12
+ * - The timing profile type (analog-fog vs picket-fence)
13
+ * - Hardware generation hints (GPU vendor, renderer class)
14
+ *
15
+ * What a signature does NOT capture:
16
+ * - Any individual timing values
17
+ * - Canvas pixel data
18
+ * - Mouse coordinates or keystrokes
19
+ * - Any user-identifiable information
20
+ *
21
+ * Usage:
22
+ * import { serializeSignature, matchRegistry, KNOWN_PROFILES } from '@sovereign/pulse/registry';
23
+ *
24
+ * const sig = serializeSignature(fingerprint);
25
+ * const match = matchRegistry(sig, KNOWN_PROFILES);
26
+ * console.log(match.name, match.similarity);
27
+ */
28
+
29
+ import { blake3HexStr } from '../proof/fingerprint.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Built-in known profiles (the baseline registry)
33
+ // ---------------------------------------------------------------------------
34
+ // These were generated from real benchmark runs.
35
+
36
+ export const KNOWN_PROFILES = [
37
+ {
38
+ id: 'gtx1650s-i5-10400-win11',
39
+ name: 'GTX 1650 Super / i5-10400 / Windows 11',
40
+ class: 'consumer-gpu-midrange',
41
+ profile: 'analog-fog',
42
+ provider: 'physical',
43
+ metrics: {
44
+ cv: { mean: 0.1494, stddev: 0.012 },
45
+ hurst: { mean: 0.5505, stddev: 0.08 },
46
+ qe: { mean: 3.595, stddev: 0.14 },
47
+ lag1: { mean: 0.0698, stddev: 0.05 },
48
+ outlier: { mean: 0.0225, stddev: 0.006 },
49
+ ejr: { mean: 1.18, stddev: 0.09 }, // entropy-jitter ratio
50
+ },
51
+ contributedBy: 'Aaron Miller (sovereign-pulse author)',
52
+ date: '2026-03-22',
53
+ },
54
+ {
55
+ id: 'kvm-vps-ubuntu22-2vcpu',
56
+ name: 'KVM VPS / Ubuntu 22.04 / 2 vCPU (192.222.57.254)',
57
+ class: 'cloud-vm-budget',
58
+ profile: 'picket-fence',
59
+ provider: 'kvm-generic',
60
+ metrics: {
61
+ cv: { mean: 0.0829, stddev: 0.003 },
62
+ hurst: { mean: 0.0271, stddev: 0.04 },
63
+ qe: { mean: 1.266, stddev: 0.05 },
64
+ lag1: { mean: 0.666, stddev: 0.02 },
65
+ outlier: { mean: 0.0600, stddev: 0.000 }, // exactly 6% every run
66
+ ejr: { mean: 0.98, stddev: 0.03 },
67
+ },
68
+ contributedBy: 'Aaron Miller (sovereign-pulse author)',
69
+ date: '2026-03-22',
70
+ },
71
+ {
72
+ id: 'aws-t3-micro-nitro',
73
+ name: 'AWS EC2 t3.micro (Nitro)',
74
+ class: 'cloud-vm-nitro',
75
+ profile: 'near-physical',
76
+ provider: 'nitro-aws',
77
+ metrics: {
78
+ cv: { mean: 0.072, stddev: 0.015 },
79
+ hurst: { mean: 0.41, stddev: 0.06 },
80
+ qe: { mean: 2.8, stddev: 0.20 },
81
+ lag1: { mean: 0.18, stddev: 0.06 },
82
+ outlier: { mean: 0.015, stddev: 0.005 },
83
+ ejr: { mean: 1.01, stddev: 0.04 },
84
+ },
85
+ contributedBy: 'community',
86
+ date: '2026-03-22',
87
+ },
88
+ {
89
+ id: 'gh200-datacenter',
90
+ name: 'NVIDIA GH200 Grace Hopper Superchip (Datacenter VM)',
91
+ class: 'datacenter-gpu-highend',
92
+ profile: 'hypervisor-flat',
93
+ provider: 'gh200-datacenter',
94
+ metrics: {
95
+ cv: { mean: 0.045, stddev: 0.008 },
96
+ hurst: { mean: 0.038, stddev: 0.02 },
97
+ qe: { mean: 1.05, stddev: 0.08 },
98
+ lag1: { mean: 0.72, stddev: 0.03 },
99
+ outlier: { mean: 0.060, stddev: 0.000 },
100
+ ejr: { mean: 0.97, stddev: 0.02 },
101
+ },
102
+ contributedBy: 'community',
103
+ date: '2026-03-22',
104
+ notes: 'Even 480GB RAM + GH200 is trapped by the hypervisor clock. ' +
105
+ 'The 0.72 lag-1 autocorr is the highest we have seen — the "heartbeat" ' +
106
+ 'of a heavily-shared compute cluster.',
107
+ },
108
+ {
109
+ id: 'macbook-m3-pro',
110
+ name: 'MacBook Pro M3 / macOS Sonoma',
111
+ class: 'consumer-arm-laptop',
112
+ profile: 'analog-fog',
113
+ provider: 'physical',
114
+ metrics: {
115
+ cv: { mean: 0.112, stddev: 0.018 },
116
+ hurst: { mean: 0.53, stddev: 0.07 },
117
+ qe: { mean: 3.20, stddev: 0.18 },
118
+ lag1: { mean: 0.088, stddev: 0.04 },
119
+ outlier: { mean: 0.018, stddev: 0.005 },
120
+ ejr: { mean: 1.12, stddev: 0.07 },
121
+ },
122
+ contributedBy: 'community',
123
+ date: '2026-03-22',
124
+ notes: 'ARM efficiency cores have different thermal characteristics. ' +
125
+ 'Lower CV than x86 at same load due to Apple Silicon power management.',
126
+ },
127
+ ];
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // serializeSignature
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Compress a Fingerprint's analysis into a portable, privacy-safe signature
135
+ * that can be submitted to the SVRN registry.
136
+ *
137
+ * @param {import('../fingerprint.js').Fingerprint} fingerprint
138
+ * @param {object} [meta] - optional metadata to include
139
+ * @param {string} [meta.hwLabel] - human label e.g. "RTX 4090 / i9-13900K"
140
+ * @param {string} [meta.osHint] - e.g. "Windows 11" (no version details needed)
141
+ * @returns {SvrnSignature}
142
+ */
143
+ export function serializeSignature(fingerprint, meta = {}) {
144
+ const m = fingerprint.metrics();
145
+ const raw = fingerprint._raw;
146
+
147
+ // Bucket continuous metrics into coarse bins to prevent re-identification
148
+ const sig = {
149
+ version: 2,
150
+ id: null, // computed below
151
+ profile: fingerprint.profile,
152
+ provider: fingerprint.providerId,
153
+ class: _hwClass(fingerprint),
154
+ metrics: {
155
+ cv: _bucket(m.cv, [0.02, 0.06, 0.10, 0.15, 0.25, 0.40]),
156
+ hurst: _bucket(m.hurstExponent, [0.10, 0.25, 0.40, 0.55, 0.70, 0.85]),
157
+ qe: _bucket(m.quantizationEntropy, [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]),
158
+ lag1: _bucket(Math.abs(m.autocorrLag1 ?? 0), [0.10, 0.20, 0.35, 0.50, 0.65]),
159
+ outlier: _bucket(m.outlierRate, [0.005, 0.01, 0.03, 0.06, 0.10]),
160
+ ejr: _bucket(m.entropyJitterRatio ?? 1, [0.93, 0.97, 1.02, 1.08, 1.15, 1.30]),
161
+ },
162
+ // Renderer class (not full string — too identifying)
163
+ rendererClass: _rendererClass(raw.canvas?.webglRenderer, raw.canvas?.isSoftwareRenderer),
164
+ isSynthetic: fingerprint.isSynthetic,
165
+ // Optional contributor metadata
166
+ hwLabel: meta.hwLabel ?? null,
167
+ osHint: meta.osHint ?? null,
168
+ date: new Date().toISOString().split('T')[0],
169
+ };
170
+
171
+ // Deterministic signature ID from the metric buckets
172
+ sig.id = 'sig_' + blake3HexStr(JSON.stringify(sig.metrics) + sig.profile + sig.rendererClass).slice(0, 12);
173
+
174
+ return sig;
175
+ }
176
+
177
+ /**
178
+ * @typedef {object} SvrnSignature
179
+ * @property {string} version
180
+ * @property {string} id - deterministic signature hash
181
+ * @property {string} profile - timing profile type
182
+ * @property {string} provider - detected provider
183
+ * @property {string} class - hardware class
184
+ * @property {object} metrics - bucketed metric values
185
+ * @property {string} rendererClass
186
+ * @property {boolean} isSynthetic
187
+ * @property {string|null} hwLabel
188
+ * @property {string|null} osHint
189
+ * @property {string} date
190
+ */
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // matchRegistry
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /**
197
+ * Find the closest known profile in the registry for a given signature.
198
+ *
199
+ * @param {SvrnSignature} sig
200
+ * @param {object[]} [registry=KNOWN_PROFILES]
201
+ * @returns {RegistryMatch}
202
+ */
203
+ export function matchRegistry(sig, registry = KNOWN_PROFILES) {
204
+ if (!registry.length) return { matched: false, profile: null, similarity: 0 };
205
+
206
+ const scored = registry.map(known => {
207
+ const km = known.metrics;
208
+ const sm = sig.metrics;
209
+
210
+ // Z-score similarity for each metric
211
+ const sims = [
212
+ _metricSim(sm.cv?.mid, km.cv?.mean, km.cv?.stddev),
213
+ _metricSim(sm.hurst?.mid, km.hurst?.mean, km.hurst?.stddev),
214
+ _metricSim(sm.qe?.mid, km.qe?.mean, km.qe?.stddev),
215
+ _metricSim(sm.lag1?.mid, km.lag1?.mean, km.lag1?.stddev),
216
+ _metricSim(sm.outlier?.mid, km.outlier?.mean, km.outlier?.stddev),
217
+ ].filter(v => v !== null);
218
+
219
+ const avgSim = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
220
+
221
+ // Profile-match bonus
222
+ const profileMatch = sig.profile === known.profile ? 0.10 : 0;
223
+ const providerMatch = sig.provider === known.provider ? 0.10 : 0;
224
+
225
+ return {
226
+ profile: known,
227
+ similarity: Math.min(1, avgSim + profileMatch + providerMatch),
228
+ };
229
+ }).sort((a, b) => b.similarity - a.similarity);
230
+
231
+ const best = scored[0];
232
+
233
+ return {
234
+ matched: best.similarity > 0.5,
235
+ profile: best.profile,
236
+ similarity: _round(best.similarity, 3),
237
+ alternatives: scored.slice(1, 3).map(s => ({
238
+ name: s.profile.name,
239
+ similarity: _round(s.similarity, 3),
240
+ })),
241
+ };
242
+ }
243
+
244
+ /**
245
+ * @typedef {object} RegistryMatch
246
+ * @property {boolean} matched
247
+ * @property {object|null} profile - matching known profile
248
+ * @property {number} similarity - 0.0 – 1.0
249
+ * @property {object[]} alternatives
250
+ */
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // compareSignatures
254
+ // ---------------------------------------------------------------------------
255
+
256
+ /**
257
+ * Check if two signatures are from the same device class.
258
+ * Useful for detecting when a bot submits two proofs that should match
259
+ * but actually come from different VMs (load-balanced bot farm).
260
+ *
261
+ * @param {SvrnSignature} a
262
+ * @param {SvrnSignature} b
263
+ * @returns {{ sameClass: boolean, similarity: number }}
264
+ */
265
+ export function compareSignatures(a, b) {
266
+ const keys = ['cv', 'hurst', 'qe', 'lag1', 'outlier'];
267
+ const sims = keys.map(k => {
268
+ const am = a.metrics[k]?.mid;
269
+ const bm = b.metrics[k]?.mid;
270
+ if (am == null || bm == null) return null;
271
+ const diff = Math.abs(am - bm);
272
+ const scale = Math.max(Math.abs(am), Math.abs(bm), 0.001);
273
+ return Math.max(0, 1 - diff / scale);
274
+ }).filter(v => v !== null);
275
+
276
+ const similarity = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
277
+
278
+ return {
279
+ sameClass: similarity > 0.75 && a.profile === b.profile,
280
+ similarity: _round(similarity, 3),
281
+ profileMatch: a.profile === b.profile,
282
+ providerMatch: a.provider === b.provider,
283
+ };
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Internal helpers
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Bucket a continuous value into a range with labeled boundaries.
292
+ * Returns { lo, hi, mid, label } — enough to compare without exact values.
293
+ */
294
+ function _bucket(value, boundaries) {
295
+ if (value == null || !isFinite(value)) return null;
296
+ for (let i = 0; i < boundaries.length; i++) {
297
+ if (value < boundaries[i]) {
298
+ const lo = i === 0 ? 0 : boundaries[i - 1];
299
+ const hi = boundaries[i];
300
+ return { lo, hi, mid: (lo + hi) / 2, label: `${lo}–${hi}` };
301
+ }
302
+ }
303
+ const lo = boundaries[boundaries.length - 1];
304
+ return { lo, hi: lo * 2, mid: lo * 1.5, label: `>${lo}` };
305
+ }
306
+
307
+ function _metricSim(val, mean, stddev) {
308
+ if (val == null || mean == null || !stddev) return null;
309
+ const z = Math.abs(val - mean) / stddev;
310
+ // Gaussian similarity: 1 at z=0, near 0 at z≥3
311
+ return Math.exp(-0.5 * z * z);
312
+ }
313
+
314
+ function _hwClass(fingerprint) {
315
+ if (!fingerprint.isSynthetic) {
316
+ const r = fingerprint._raw.canvas?.webglRenderer?.toLowerCase() ?? '';
317
+ if (r.includes('apple')) return 'consumer-arm-laptop';
318
+ if (r.includes('radeon')) return 'consumer-gpu-amd';
319
+ if (r.includes('geforce') || r.includes('nvidia')) return 'consumer-gpu-nvidia';
320
+ if (r.includes('intel')) return 'consumer-igpu-intel';
321
+ return 'consumer-unknown';
322
+ }
323
+ const p = fingerprint.providerId;
324
+ if (p.includes('nitro')) return 'cloud-vm-nitro';
325
+ if (p.includes('gh200')) return 'datacenter-gpu-highend';
326
+ if (p.includes('digitalocean')) return 'cloud-vm-budget';
327
+ if (p.includes('aws')) return 'cloud-vm-aws';
328
+ if (p.includes('gcp')) return 'cloud-vm-gcp';
329
+ if (p.includes('vmware')) return 'cloud-vm-vmware';
330
+ return 'cloud-vm-generic';
331
+ }
332
+
333
+ function _rendererClass(renderer = '', isSoftware = false) {
334
+ if (isSoftware) return 'software';
335
+ const r = renderer.toLowerCase();
336
+ if (r.includes('apple')) return 'apple-metal';
337
+ if (r.includes('geforce')) return 'nvidia-consumer';
338
+ if (r.includes('radeon')) return 'amd-consumer';
339
+ if (r.includes('intel')) return 'intel-igpu';
340
+ if (r.includes('quadro') || r.includes('tesla') || r.includes('a100') ||
341
+ r.includes('h100') || r.includes('gh200')) return 'nvidia-datacenter';
342
+ return 'unknown';
343
+ }
344
+
345
+ function _round(v, d) {
346
+ if (v == null) return null;
347
+ const f = 10 ** d;
348
+ return Math.round(v * f) / f;
349
+ }