@svrnsec/pulse 0.7.0 → 0.9.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.
- package/LICENSE +21 -21
- package/README.md +883 -782
- package/SECURITY.md +27 -22
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6428 -6413
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6429 -6415
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +949 -846
- package/package.json +189 -184
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -393
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -804
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -391
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/errors.js +54 -0
- package/src/fingerprint.js +475 -475
- package/src/index.js +345 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +82 -142
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- package/dist/pulse.cjs.js.map +0 -1
package/src/proof/fingerprint.js
CHANGED
|
@@ -1,268 +1,268 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @svrnsec/pulse — Hardware Fingerprint & Proof Builder
|
|
3
|
-
*
|
|
4
|
-
* Assembles all collected signals into a canonical ProofPayload, then
|
|
5
|
-
* produces a BLAKE3 commitment: BLAKE3(canonicalJSON(payload)).
|
|
6
|
-
*
|
|
7
|
-
* The commitment is what gets sent to the server. The server recomputes
|
|
8
|
-
* the hash from the payload to detect tampering. Raw timing arrays and
|
|
9
|
-
* pixel buffers are NOT included — only statistical summaries.
|
|
10
|
-
*
|
|
11
|
-
* Zero-Knowledge property: the server learns only that the device passes
|
|
12
|
-
* statistical thresholds. It never sees raw hardware telemetry.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { blake3 } from '@noble/hashes/blake3';
|
|
16
|
-
import { bytesToHex } from '@noble/hashes/utils';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// BLAKE3 helpers (re-exported for use by canvas.js etc.)
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Compute BLAKE3 of a Uint8Array and return hex string.
|
|
24
|
-
* @param {Uint8Array} data
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
export function blake3Hex(data) {
|
|
28
|
-
return bytesToHex(blake3(data));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Compute BLAKE3 of a UTF-8 string and return hex string.
|
|
33
|
-
* @param {string} str
|
|
34
|
-
* @returns {string}
|
|
35
|
-
*/
|
|
36
|
-
export function blake3HexStr(str) {
|
|
37
|
-
return blake3Hex(new TextEncoder().encode(str));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// buildProof
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Assembles a ProofPayload from all collected signals.
|
|
46
|
-
* This is the canonical structure that gets hashed into the commitment.
|
|
47
|
-
*
|
|
48
|
-
* @param {object} p
|
|
49
|
-
* @param {import('../collector/entropy.js').EntropyResult} p.entropy
|
|
50
|
-
* @param {import('../analysis/jitter.js').JitterAnalysis} p.jitter
|
|
51
|
-
* @param {import('../collector/bio.js').BioSnapshot} p.bio
|
|
52
|
-
* @param {import('../collector/canvas.js').CanvasFingerprint} p.canvas
|
|
53
|
-
* @param {import('../analysis/audio.js').AudioJitter} p.audio
|
|
54
|
-
* @param {string} p.nonce – server-issued challenge nonce (hex)
|
|
55
|
-
* @returns {ProofPayload}
|
|
56
|
-
*/
|
|
57
|
-
export function buildProof({ entropy, jitter, bio, canvas, audio, enf, gpu, dram, llm, nonce }) {
|
|
58
|
-
if (!nonce || typeof nonce !== 'string') {
|
|
59
|
-
throw new Error('@svrnsec/pulse: nonce is required for anti-replay protection');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Hash the raw timing arrays IN-BROWSER so we can prove their integrity
|
|
63
|
-
// without transmitting the raw data.
|
|
64
|
-
const timingsHash = blake3HexStr(JSON.stringify(entropy.timings));
|
|
65
|
-
const memHash = blake3HexStr(JSON.stringify(entropy.memTimings));
|
|
66
|
-
|
|
67
|
-
const payload = {
|
|
68
|
-
version: 1,
|
|
69
|
-
timestamp: entropy.collectedAt,
|
|
70
|
-
nonce,
|
|
71
|
-
|
|
72
|
-
signals: {
|
|
73
|
-
// ── Entropy probe ───────────────────────────────────────────────────
|
|
74
|
-
entropy: {
|
|
75
|
-
timingsMean: _round(jitter.stats?.mean, 4),
|
|
76
|
-
timingsCV: _round(jitter.stats?.cv, 4),
|
|
77
|
-
timingsP50: _round(jitter.stats?.p50, 4),
|
|
78
|
-
timingsP95: _round(jitter.stats?.p95, 4),
|
|
79
|
-
timingsSkewness: _round(jitter.stats?.skewness, 4),
|
|
80
|
-
timingsKurtosis: _round(jitter.stats?.kurtosis, 4),
|
|
81
|
-
autocorr_lag1: _round(jitter.autocorrelations?.lag1, 4),
|
|
82
|
-
autocorr_lag2: _round(jitter.autocorrelations?.lag2, 4),
|
|
83
|
-
autocorr_lag5: _round(jitter.autocorrelations?.lag5, 4),
|
|
84
|
-
autocorr_lag10: _round(jitter.autocorrelations?.lag10, 4),
|
|
85
|
-
hurstExponent: _round(jitter.hurstExponent, 4),
|
|
86
|
-
quantizationEntropy: _round(jitter.quantizationEntropy, 4),
|
|
87
|
-
thermalDrift: _round(jitter.thermalSignature?.slope, 8),
|
|
88
|
-
thermalPattern: jitter.thermalSignature?.pattern ?? 'unknown',
|
|
89
|
-
outlierRate: _round(jitter.outlierRate, 4),
|
|
90
|
-
timerGranularityMs: _round(entropy.timerGranularityMs, 6),
|
|
91
|
-
checksum: entropy.checksum, // proves computation ran
|
|
92
|
-
timingsHash, // proves timing array integrity
|
|
93
|
-
memTimingsHash: memHash,
|
|
94
|
-
iterations: entropy.iterations,
|
|
95
|
-
matrixSize: entropy.matrixSize,
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
// ── Bio signals ─────────────────────────────────────────────────────
|
|
99
|
-
bio: {
|
|
100
|
-
mouseSampleCount: bio.mouse.sampleCount,
|
|
101
|
-
mouseIEIMean: _round(bio.mouse.ieiMean, 3),
|
|
102
|
-
mouseIEICV: _round(bio.mouse.ieiCV, 4),
|
|
103
|
-
mouseVelocityP50: _round(bio.mouse.velocityP50, 3),
|
|
104
|
-
mouseVelocityP95: _round(bio.mouse.velocityP95, 3),
|
|
105
|
-
mouseAngularJerkMean: _round(bio.mouse.angularJerkMean, 4),
|
|
106
|
-
pressureVariance: _round(bio.mouse.pressureVariance, 6),
|
|
107
|
-
keyboardSampleCount: bio.keyboard.sampleCount,
|
|
108
|
-
keyboardDwellMean: _round(bio.keyboard.dwellMean, 3),
|
|
109
|
-
keyboardDwellCV: _round(bio.keyboard.dwellCV, 4),
|
|
110
|
-
keyboardIKIMean: _round(bio.keyboard.ikiMean, 3),
|
|
111
|
-
keyboardIKICV: _round(bio.keyboard.ikiCV, 4),
|
|
112
|
-
interferenceCoefficient: _round(bio.interferenceCoefficient, 4),
|
|
113
|
-
hasActivity: bio.hasActivity,
|
|
114
|
-
durationMs: _round(bio.durationMs, 1),
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
// ── Canvas fingerprint ───────────────────────────────────────────────
|
|
118
|
-
canvas: {
|
|
119
|
-
webglRenderer: canvas.webglRenderer,
|
|
120
|
-
webglVendor: canvas.webglVendor,
|
|
121
|
-
webglVersion: canvas.webglVersion,
|
|
122
|
-
webglPixelHash: canvas.webglPixelHash,
|
|
123
|
-
canvas2dHash: canvas.canvas2dHash,
|
|
124
|
-
extensionCount: canvas.extensionCount,
|
|
125
|
-
isSoftwareRenderer: canvas.isSoftwareRenderer,
|
|
126
|
-
available: canvas.available,
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
// ── Audio jitter ─────────────────────────────────────────────────────
|
|
130
|
-
audio: {
|
|
131
|
-
available: audio.available,
|
|
132
|
-
workletAvailable: audio.workletAvailable,
|
|
133
|
-
callbackJitterCV: _round(audio.callbackJitterCV, 4),
|
|
134
|
-
noiseFloorMean: _round(audio.noiseFloorMean, 6),
|
|
135
|
-
noiseFloorStd: _round(audio.noiseFloorStd, 6),
|
|
136
|
-
sampleRate: audio.sampleRate,
|
|
137
|
-
callbackCount: audio.callbackCount,
|
|
138
|
-
jitterMeanMs: _round(audio.jitterMeanMs, 4),
|
|
139
|
-
jitterP95Ms: _round(audio.jitterP95Ms, 4),
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
// ── Electrical Network Frequency ─────────────────────────────────────
|
|
143
|
-
enf: enf ? {
|
|
144
|
-
available: enf.enfAvailable,
|
|
145
|
-
ripplePresent: enf.ripplePresent,
|
|
146
|
-
gridFrequency: enf.gridFrequency,
|
|
147
|
-
gridRegion: enf.gridRegion,
|
|
148
|
-
ripplePower: _round(enf.ripplePower, 4),
|
|
149
|
-
enfDeviation: _round(enf.enfDeviation, 3),
|
|
150
|
-
snr50hz: _round(enf.snr50hz, 2),
|
|
151
|
-
snr60hz: _round(enf.snr60hz, 2),
|
|
152
|
-
sampleRateHz: _round(enf.sampleRateHz, 1),
|
|
153
|
-
verdict: enf.verdict,
|
|
154
|
-
isVmIndicator: enf.isVmIndicator,
|
|
155
|
-
capturedAt: enf.temporalAnchor?.capturedAt ?? null,
|
|
156
|
-
} : null,
|
|
157
|
-
|
|
158
|
-
// ── WebGPU thermal variance ───────────────────────────────────────────
|
|
159
|
-
gpu: gpu ? {
|
|
160
|
-
available: gpu.gpuPresent,
|
|
161
|
-
isSoftware: gpu.isSoftware,
|
|
162
|
-
vendorString: gpu.vendorString,
|
|
163
|
-
dispatchCV: _round(gpu.dispatchCV, 4),
|
|
164
|
-
thermalGrowth: _round(gpu.thermalGrowth, 4),
|
|
165
|
-
verdict: gpu.verdict,
|
|
166
|
-
} : null,
|
|
167
|
-
|
|
168
|
-
// ── DRAM refresh cycle ────────────────────────────────────────────────
|
|
169
|
-
dram: dram ? {
|
|
170
|
-
refreshPresent: dram.refreshPresent,
|
|
171
|
-
refreshPeriodMs: _round(dram.refreshPeriodMs, 2),
|
|
172
|
-
peakPower: _round(dram.peakPower, 4),
|
|
173
|
-
verdict: dram.verdict,
|
|
174
|
-
} : null,
|
|
175
|
-
|
|
176
|
-
// ── LLM / AI agent behavioral fingerprint ────────────────────────────
|
|
177
|
-
llm: llm ? {
|
|
178
|
-
aiConf: _round(llm.aiConf, 3),
|
|
179
|
-
thinkTimePattern: llm.thinkTimePattern,
|
|
180
|
-
correctionRate: _round(llm.correctionRate, 3),
|
|
181
|
-
rhythmicity: _round(llm.rhythmicity, 3),
|
|
182
|
-
pauseDistribution: llm.pauseDistribution,
|
|
183
|
-
verdict: llm.verdict,
|
|
184
|
-
matchedModel: llm.matchedModel ?? null,
|
|
185
|
-
} : null,
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
// Top-level classification summary — all signal layers combined
|
|
189
|
-
classification: {
|
|
190
|
-
jitterScore: _round(jitter.score, 4),
|
|
191
|
-
flags: jitter.flags ?? [],
|
|
192
|
-
enfVerdict: enf?.verdict ?? 'unavailable',
|
|
193
|
-
gpuVerdict: gpu?.verdict ?? 'unavailable',
|
|
194
|
-
dramVerdict: dram?.verdict ?? 'unavailable',
|
|
195
|
-
llmVerdict: llm?.verdict ?? 'unavailable',
|
|
196
|
-
// Combined VM confidence: any hard signal raises this
|
|
197
|
-
vmIndicators: [
|
|
198
|
-
enf?.isVmIndicator ? 'enf_no_grid' : null,
|
|
199
|
-
gpu?.isSoftware ? 'gpu_software' : null,
|
|
200
|
-
dram?.verdict === 'virtual' ? 'dram_no_refresh' : null,
|
|
201
|
-
llm?.aiConf > 0.7 ? 'llm_agent' : null,
|
|
202
|
-
].filter(Boolean),
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
return payload;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* @typedef {object} ProofPayload
|
|
211
|
-
* @property {number} version
|
|
212
|
-
* @property {number} timestamp
|
|
213
|
-
* @property {string} nonce
|
|
214
|
-
* @property {object} signals
|
|
215
|
-
* @property {object} classification
|
|
216
|
-
*/
|
|
217
|
-
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// buildCommitment
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Hashes a ProofPayload into a BLAKE3 commitment.
|
|
224
|
-
* Uses a deterministic canonical JSON serialiser (sorted keys) to ensure
|
|
225
|
-
* byte-identical output across JS engines.
|
|
226
|
-
*
|
|
227
|
-
* @param {ProofPayload} payload
|
|
228
|
-
* @returns {{ payload: ProofPayload, hash: string }}
|
|
229
|
-
*/
|
|
230
|
-
export function buildCommitment(payload) {
|
|
231
|
-
const canonical = canonicalJson(payload);
|
|
232
|
-
const hash = blake3HexStr(canonical);
|
|
233
|
-
return { payload, hash };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// canonicalJson
|
|
238
|
-
//
|
|
239
|
-
// JSON.stringify with sorted keys — ensures the hash is engine-independent.
|
|
240
|
-
// Numbers are serialised with fixed precision to avoid cross-platform float
|
|
241
|
-
// formatting differences.
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
export function canonicalJson(obj) {
|
|
245
|
-
return JSON.stringify(obj, _replacer, 0);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function _replacer(key, value) {
|
|
249
|
-
// Sort object keys deterministically
|
|
250
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
251
|
-
const sorted = {};
|
|
252
|
-
for (const k of Object.keys(value).sort()) {
|
|
253
|
-
sorted[k] = value[k];
|
|
254
|
-
}
|
|
255
|
-
return sorted;
|
|
256
|
-
}
|
|
257
|
-
return value;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Internal utilities
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
|
|
264
|
-
function _round(v, decimals) {
|
|
265
|
-
if (v == null || !isFinite(v)) return null;
|
|
266
|
-
const factor = 10 ** decimals;
|
|
267
|
-
return Math.round(v * factor) / factor;
|
|
268
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — Hardware Fingerprint & Proof Builder
|
|
3
|
+
*
|
|
4
|
+
* Assembles all collected signals into a canonical ProofPayload, then
|
|
5
|
+
* produces a BLAKE3 commitment: BLAKE3(canonicalJSON(payload)).
|
|
6
|
+
*
|
|
7
|
+
* The commitment is what gets sent to the server. The server recomputes
|
|
8
|
+
* the hash from the payload to detect tampering. Raw timing arrays and
|
|
9
|
+
* pixel buffers are NOT included — only statistical summaries.
|
|
10
|
+
*
|
|
11
|
+
* Zero-Knowledge property: the server learns only that the device passes
|
|
12
|
+
* statistical thresholds. It never sees raw hardware telemetry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { blake3 } from '@noble/hashes/blake3';
|
|
16
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// BLAKE3 helpers (re-exported for use by canvas.js etc.)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compute BLAKE3 of a Uint8Array and return hex string.
|
|
24
|
+
* @param {Uint8Array} data
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function blake3Hex(data) {
|
|
28
|
+
return bytesToHex(blake3(data));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compute BLAKE3 of a UTF-8 string and return hex string.
|
|
33
|
+
* @param {string} str
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function blake3HexStr(str) {
|
|
37
|
+
return blake3Hex(new TextEncoder().encode(str));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// buildProof
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Assembles a ProofPayload from all collected signals.
|
|
46
|
+
* This is the canonical structure that gets hashed into the commitment.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} p
|
|
49
|
+
* @param {import('../collector/entropy.js').EntropyResult} p.entropy
|
|
50
|
+
* @param {import('../analysis/jitter.js').JitterAnalysis} p.jitter
|
|
51
|
+
* @param {import('../collector/bio.js').BioSnapshot} p.bio
|
|
52
|
+
* @param {import('../collector/canvas.js').CanvasFingerprint} p.canvas
|
|
53
|
+
* @param {import('../analysis/audio.js').AudioJitter} p.audio
|
|
54
|
+
* @param {string} p.nonce – server-issued challenge nonce (hex)
|
|
55
|
+
* @returns {ProofPayload}
|
|
56
|
+
*/
|
|
57
|
+
export function buildProof({ entropy, jitter, bio, canvas, audio, enf, gpu, dram, llm, nonce }) {
|
|
58
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
59
|
+
throw new Error('@svrnsec/pulse: nonce is required for anti-replay protection');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Hash the raw timing arrays IN-BROWSER so we can prove their integrity
|
|
63
|
+
// without transmitting the raw data.
|
|
64
|
+
const timingsHash = blake3HexStr(JSON.stringify(entropy.timings));
|
|
65
|
+
const memHash = blake3HexStr(JSON.stringify(entropy.memTimings));
|
|
66
|
+
|
|
67
|
+
const payload = {
|
|
68
|
+
version: 1,
|
|
69
|
+
timestamp: entropy.collectedAt,
|
|
70
|
+
nonce,
|
|
71
|
+
|
|
72
|
+
signals: {
|
|
73
|
+
// ── Entropy probe ───────────────────────────────────────────────────
|
|
74
|
+
entropy: {
|
|
75
|
+
timingsMean: _round(jitter.stats?.mean, 4),
|
|
76
|
+
timingsCV: _round(jitter.stats?.cv, 4),
|
|
77
|
+
timingsP50: _round(jitter.stats?.p50, 4),
|
|
78
|
+
timingsP95: _round(jitter.stats?.p95, 4),
|
|
79
|
+
timingsSkewness: _round(jitter.stats?.skewness, 4),
|
|
80
|
+
timingsKurtosis: _round(jitter.stats?.kurtosis, 4),
|
|
81
|
+
autocorr_lag1: _round(jitter.autocorrelations?.lag1, 4),
|
|
82
|
+
autocorr_lag2: _round(jitter.autocorrelations?.lag2, 4),
|
|
83
|
+
autocorr_lag5: _round(jitter.autocorrelations?.lag5, 4),
|
|
84
|
+
autocorr_lag10: _round(jitter.autocorrelations?.lag10, 4),
|
|
85
|
+
hurstExponent: _round(jitter.hurstExponent, 4),
|
|
86
|
+
quantizationEntropy: _round(jitter.quantizationEntropy, 4),
|
|
87
|
+
thermalDrift: _round(jitter.thermalSignature?.slope, 8),
|
|
88
|
+
thermalPattern: jitter.thermalSignature?.pattern ?? 'unknown',
|
|
89
|
+
outlierRate: _round(jitter.outlierRate, 4),
|
|
90
|
+
timerGranularityMs: _round(entropy.timerGranularityMs, 6),
|
|
91
|
+
checksum: entropy.checksum, // proves computation ran
|
|
92
|
+
timingsHash, // proves timing array integrity
|
|
93
|
+
memTimingsHash: memHash,
|
|
94
|
+
iterations: entropy.iterations,
|
|
95
|
+
matrixSize: entropy.matrixSize,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// ── Bio signals ─────────────────────────────────────────────────────
|
|
99
|
+
bio: {
|
|
100
|
+
mouseSampleCount: bio.mouse.sampleCount,
|
|
101
|
+
mouseIEIMean: _round(bio.mouse.ieiMean, 3),
|
|
102
|
+
mouseIEICV: _round(bio.mouse.ieiCV, 4),
|
|
103
|
+
mouseVelocityP50: _round(bio.mouse.velocityP50, 3),
|
|
104
|
+
mouseVelocityP95: _round(bio.mouse.velocityP95, 3),
|
|
105
|
+
mouseAngularJerkMean: _round(bio.mouse.angularJerkMean, 4),
|
|
106
|
+
pressureVariance: _round(bio.mouse.pressureVariance, 6),
|
|
107
|
+
keyboardSampleCount: bio.keyboard.sampleCount,
|
|
108
|
+
keyboardDwellMean: _round(bio.keyboard.dwellMean, 3),
|
|
109
|
+
keyboardDwellCV: _round(bio.keyboard.dwellCV, 4),
|
|
110
|
+
keyboardIKIMean: _round(bio.keyboard.ikiMean, 3),
|
|
111
|
+
keyboardIKICV: _round(bio.keyboard.ikiCV, 4),
|
|
112
|
+
interferenceCoefficient: _round(bio.interferenceCoefficient, 4),
|
|
113
|
+
hasActivity: bio.hasActivity,
|
|
114
|
+
durationMs: _round(bio.durationMs, 1),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// ── Canvas fingerprint ───────────────────────────────────────────────
|
|
118
|
+
canvas: {
|
|
119
|
+
webglRenderer: canvas.webglRenderer,
|
|
120
|
+
webglVendor: canvas.webglVendor,
|
|
121
|
+
webglVersion: canvas.webglVersion,
|
|
122
|
+
webglPixelHash: canvas.webglPixelHash,
|
|
123
|
+
canvas2dHash: canvas.canvas2dHash,
|
|
124
|
+
extensionCount: canvas.extensionCount,
|
|
125
|
+
isSoftwareRenderer: canvas.isSoftwareRenderer,
|
|
126
|
+
available: canvas.available,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ── Audio jitter ─────────────────────────────────────────────────────
|
|
130
|
+
audio: {
|
|
131
|
+
available: audio.available,
|
|
132
|
+
workletAvailable: audio.workletAvailable,
|
|
133
|
+
callbackJitterCV: _round(audio.callbackJitterCV, 4),
|
|
134
|
+
noiseFloorMean: _round(audio.noiseFloorMean, 6),
|
|
135
|
+
noiseFloorStd: _round(audio.noiseFloorStd, 6),
|
|
136
|
+
sampleRate: audio.sampleRate,
|
|
137
|
+
callbackCount: audio.callbackCount,
|
|
138
|
+
jitterMeanMs: _round(audio.jitterMeanMs, 4),
|
|
139
|
+
jitterP95Ms: _round(audio.jitterP95Ms, 4),
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// ── Electrical Network Frequency ─────────────────────────────────────
|
|
143
|
+
enf: enf ? {
|
|
144
|
+
available: enf.enfAvailable,
|
|
145
|
+
ripplePresent: enf.ripplePresent,
|
|
146
|
+
gridFrequency: enf.gridFrequency,
|
|
147
|
+
gridRegion: enf.gridRegion,
|
|
148
|
+
ripplePower: _round(enf.ripplePower, 4),
|
|
149
|
+
enfDeviation: _round(enf.enfDeviation, 3),
|
|
150
|
+
snr50hz: _round(enf.snr50hz, 2),
|
|
151
|
+
snr60hz: _round(enf.snr60hz, 2),
|
|
152
|
+
sampleRateHz: _round(enf.sampleRateHz, 1),
|
|
153
|
+
verdict: enf.verdict,
|
|
154
|
+
isVmIndicator: enf.isVmIndicator,
|
|
155
|
+
capturedAt: enf.temporalAnchor?.capturedAt ?? null,
|
|
156
|
+
} : null,
|
|
157
|
+
|
|
158
|
+
// ── WebGPU thermal variance ───────────────────────────────────────────
|
|
159
|
+
gpu: gpu ? {
|
|
160
|
+
available: gpu.gpuPresent,
|
|
161
|
+
isSoftware: gpu.isSoftware,
|
|
162
|
+
vendorString: gpu.vendorString,
|
|
163
|
+
dispatchCV: _round(gpu.dispatchCV, 4),
|
|
164
|
+
thermalGrowth: _round(gpu.thermalGrowth, 4),
|
|
165
|
+
verdict: gpu.verdict,
|
|
166
|
+
} : null,
|
|
167
|
+
|
|
168
|
+
// ── DRAM refresh cycle ────────────────────────────────────────────────
|
|
169
|
+
dram: dram ? {
|
|
170
|
+
refreshPresent: dram.refreshPresent,
|
|
171
|
+
refreshPeriodMs: _round(dram.refreshPeriodMs, 2),
|
|
172
|
+
peakPower: _round(dram.peakPower, 4),
|
|
173
|
+
verdict: dram.verdict,
|
|
174
|
+
} : null,
|
|
175
|
+
|
|
176
|
+
// ── LLM / AI agent behavioral fingerprint ────────────────────────────
|
|
177
|
+
llm: llm ? {
|
|
178
|
+
aiConf: _round(llm.aiConf, 3),
|
|
179
|
+
thinkTimePattern: llm.thinkTimePattern,
|
|
180
|
+
correctionRate: _round(llm.correctionRate, 3),
|
|
181
|
+
rhythmicity: _round(llm.rhythmicity, 3),
|
|
182
|
+
pauseDistribution: llm.pauseDistribution,
|
|
183
|
+
verdict: llm.verdict,
|
|
184
|
+
matchedModel: llm.matchedModel ?? null,
|
|
185
|
+
} : null,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Top-level classification summary — all signal layers combined
|
|
189
|
+
classification: {
|
|
190
|
+
jitterScore: _round(jitter.score, 4),
|
|
191
|
+
flags: jitter.flags ?? [],
|
|
192
|
+
enfVerdict: enf?.verdict ?? 'unavailable',
|
|
193
|
+
gpuVerdict: gpu?.verdict ?? 'unavailable',
|
|
194
|
+
dramVerdict: dram?.verdict ?? 'unavailable',
|
|
195
|
+
llmVerdict: llm?.verdict ?? 'unavailable',
|
|
196
|
+
// Combined VM confidence: any hard signal raises this
|
|
197
|
+
vmIndicators: [
|
|
198
|
+
enf?.isVmIndicator ? 'enf_no_grid' : null,
|
|
199
|
+
gpu?.isSoftware ? 'gpu_software' : null,
|
|
200
|
+
dram?.verdict === 'virtual' ? 'dram_no_refresh' : null,
|
|
201
|
+
llm?.aiConf > 0.7 ? 'llm_agent' : null,
|
|
202
|
+
].filter(Boolean),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return payload;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @typedef {object} ProofPayload
|
|
211
|
+
* @property {number} version
|
|
212
|
+
* @property {number} timestamp
|
|
213
|
+
* @property {string} nonce
|
|
214
|
+
* @property {object} signals
|
|
215
|
+
* @property {object} classification
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// buildCommitment
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Hashes a ProofPayload into a BLAKE3 commitment.
|
|
224
|
+
* Uses a deterministic canonical JSON serialiser (sorted keys) to ensure
|
|
225
|
+
* byte-identical output across JS engines.
|
|
226
|
+
*
|
|
227
|
+
* @param {ProofPayload} payload
|
|
228
|
+
* @returns {{ payload: ProofPayload, hash: string }}
|
|
229
|
+
*/
|
|
230
|
+
export function buildCommitment(payload) {
|
|
231
|
+
const canonical = canonicalJson(payload);
|
|
232
|
+
const hash = blake3HexStr(canonical);
|
|
233
|
+
return { payload, hash };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// canonicalJson
|
|
238
|
+
//
|
|
239
|
+
// JSON.stringify with sorted keys — ensures the hash is engine-independent.
|
|
240
|
+
// Numbers are serialised with fixed precision to avoid cross-platform float
|
|
241
|
+
// formatting differences.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export function canonicalJson(obj) {
|
|
245
|
+
return JSON.stringify(obj, _replacer, 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _replacer(key, value) {
|
|
249
|
+
// Sort object keys deterministically
|
|
250
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
251
|
+
const sorted = {};
|
|
252
|
+
for (const k of Object.keys(value).sort()) {
|
|
253
|
+
sorted[k] = value[k];
|
|
254
|
+
}
|
|
255
|
+
return sorted;
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Internal utilities
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
function _round(v, decimals) {
|
|
265
|
+
if (v == null || !isFinite(v)) return null;
|
|
266
|
+
const factor = 10 ** decimals;
|
|
267
|
+
return Math.round(v * factor) / factor;
|
|
268
|
+
}
|