glacis 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
glacis/wasm_runtime.py DELETED
@@ -1,533 +0,0 @@
1
- """
2
- WASM runtime for offline cryptographic operations.
3
-
4
- Uses wasmtime-py to execute the s3p-core WASM module compiled for WASI target.
5
- Falls back to PyNaCl if WASM runtime fails.
6
-
7
- This provides Ed25519 signing and verification without reimplementing crypto in Python.
8
- """
9
-
10
- import ctypes
11
- import hashlib
12
- import json
13
- from base64 import b64encode
14
- from pathlib import Path
15
- from typing import Any, Optional
16
-
17
-
18
- class WasmRuntimeError(Exception):
19
- """Error from WASM runtime operations."""
20
-
21
- pass
22
-
23
-
24
- class PyNaClRuntime:
25
- """
26
- Pure Python Ed25519 runtime using PyNaCl (libsodium bindings).
27
-
28
- Used as a fallback when WASM runtime is unavailable or fails.
29
- """
30
-
31
- def __init__(self) -> None:
32
- try:
33
- import nacl.encoding
34
- import nacl.signing
35
- self._nacl_signing = nacl.signing
36
- self._nacl_encoding = nacl.encoding
37
- except ImportError:
38
- raise WasmRuntimeError(
39
- "PyNaCl not installed. Install with: pip install pynacl"
40
- )
41
-
42
- def derive_public_key(self, seed: bytes) -> bytes:
43
- """Derive Ed25519 public key from a 32-byte seed."""
44
- if len(seed) != 32:
45
- raise ValueError("Seed must be exactly 32 bytes")
46
- signing_key = self._nacl_signing.SigningKey(seed)
47
- return bytes(signing_key.verify_key)
48
-
49
- def get_public_key_hex(self, seed: bytes) -> str:
50
- """Get hex-encoded public key from a 32-byte seed."""
51
- return self.derive_public_key(seed).hex()
52
-
53
- def sha256(self, data: bytes) -> bytes:
54
- """Compute SHA-256 hash."""
55
- return hashlib.sha256(data).digest()
56
-
57
- def ed25519_sign(self, seed: bytes, message: bytes) -> bytes:
58
- """Sign a message with Ed25519."""
59
- if len(seed) != 32:
60
- raise ValueError("Seed must be exactly 32 bytes")
61
- signing_key = self._nacl_signing.SigningKey(seed)
62
- signed = signing_key.sign(message)
63
- return signed.signature # Just the 64-byte signature
64
-
65
- def ed25519_verify(
66
- self, public_key: bytes, message: bytes, signature: bytes
67
- ) -> bool:
68
- """Verify an Ed25519 signature."""
69
- if len(public_key) != 32:
70
- raise ValueError("Public key must be exactly 32 bytes")
71
- if len(signature) != 64:
72
- raise ValueError("Signature must be exactly 64 bytes")
73
- try:
74
- verify_key = self._nacl_signing.VerifyKey(public_key)
75
- verify_key.verify(message, signature)
76
- return True
77
- except Exception:
78
- return False
79
-
80
- def sign_attestation_json(self, seed: bytes, attestation_json: str) -> str:
81
- """Sign an attestation JSON and return SignedAttestation JSON."""
82
- if len(seed) != 32:
83
- raise ValueError("Seed must be exactly 32 bytes")
84
-
85
- json_bytes = attestation_json.encode("utf-8")
86
- signature = self.ed25519_sign(seed, json_bytes)
87
- signature_b64 = b64encode(signature).decode("ascii")
88
-
89
- # Parse and re-serialize to ensure valid JSON
90
- payload = json.loads(attestation_json)
91
- return json.dumps({
92
- "payload": payload,
93
- "signature": signature_b64,
94
- }, separators=(",", ":"))
95
-
96
-
97
- class WasmRuntime:
98
- """
99
- Singleton WASM runtime for Ed25519 cryptographic operations.
100
-
101
- Loads the s3p_core_wasi.wasm module and provides Python-friendly wrappers
102
- around the low-level C-ABI functions.
103
- """
104
-
105
- _instance: Optional["WasmRuntime"] = None
106
-
107
- def __init__(self) -> None:
108
- """Initialize the WASM runtime and load the s3p-core module."""
109
- try:
110
- import wasmtime
111
- except ImportError:
112
- raise WasmRuntimeError("wasmtime not installed. Install with: pip install wasmtime")
113
-
114
- # Create engine and linker
115
- self._engine = wasmtime.Engine()
116
- self._linker = wasmtime.Linker(self._engine)
117
- self._linker.define_wasi()
118
-
119
- # Load WASM module from package
120
- wasm_path = Path(__file__).parent / "wasm" / "s3p_core_wasi.wasm"
121
- if not wasm_path.exists():
122
- raise WasmRuntimeError(f"WASM module not found at {wasm_path}")
123
-
124
- self._module = wasmtime.Module.from_file(self._engine, str(wasm_path))
125
-
126
- # Create store with WASI config
127
- wasi_config = wasmtime.WasiConfig()
128
- wasi_config.inherit_stdout()
129
- wasi_config.inherit_stderr()
130
-
131
- self._store = wasmtime.Store(self._engine)
132
- self._store.set_wasi(wasi_config)
133
-
134
- # Instantiate the module
135
- self._wasm_instance = self._linker.instantiate(self._store, self._module)
136
-
137
- # Initialize WASI Reactor if present
138
- exports = self._wasm_instance.exports(self._store)
139
- if "_initialize" in exports:
140
- exports["_initialize"](self._store)
141
-
142
- # Get memory and exported functions
143
- self._memory = exports["memory"]
144
- self._alloc = exports["wasi_alloc"]
145
- self._dealloc = exports["wasi_dealloc"]
146
- self._derive_public_key = exports["wasi_derive_public_key"]
147
- self._sha256 = exports["wasi_sha256"]
148
- self._hash_canonical_json = exports["wasi_hash_canonical_json"]
149
- self._ed25519_sign = exports["wasi_ed25519_sign"]
150
- self._ed25519_verify = exports["wasi_ed25519_verify"]
151
- self._sign_attestation_json = exports["wasi_sign_attestation_json"]
152
- self._get_public_key_hex = exports["wasi_get_public_key_hex"]
153
-
154
- @classmethod
155
- def get_instance(cls, use_wasm: bool = False) -> "WasmRuntime":
156
- """
157
- Get or create the singleton runtime instance.
158
-
159
- Args:
160
- use_wasm: If True, try WASM runtime first. If False (default),
161
- use PyNaCl directly for better compatibility.
162
-
163
- Falls back to PyNaCl if WASM runtime initialization fails.
164
- """
165
- if cls._instance is None:
166
- if use_wasm:
167
- try:
168
- cls._instance = WasmRuntime()
169
- except Exception:
170
- # WASM failed, use PyNaCl fallback
171
- cls._instance = PyNaClRuntime() # type: ignore[assignment]
172
- else:
173
- # Use PyNaCl by default for better compatibility
174
- cls._instance = PyNaClRuntime() # type: ignore[assignment]
175
- assert cls._instance is not None
176
- return cls._instance
177
-
178
- def _write_bytes(self, data: bytes) -> int:
179
- """Allocate WASM memory and write bytes, returning the pointer."""
180
- size = len(data)
181
- ptr: int = self._alloc(self._store, size)
182
- if ptr == 0:
183
- raise WasmRuntimeError("Failed to allocate WASM memory")
184
-
185
- # Write data to WASM memory using ctypes
186
- mem_ptr = self._memory.data_ptr(self._store)
187
- base_addr = ctypes.addressof(mem_ptr.contents)
188
- dest_addr = base_addr + ptr
189
-
190
- ctypes.memmove(dest_addr, data, size)
191
-
192
- return ptr
193
-
194
- def _read_bytes(self, ptr: int, size: int) -> bytes:
195
- """Read bytes from WASM memory."""
196
- mem_ptr = self._memory.data_ptr(self._store)
197
- base_addr = ctypes.addressof(mem_ptr.contents)
198
- src_addr = base_addr + ptr
199
-
200
- return ctypes.string_at(src_addr, size)
201
-
202
- def _free(self, ptr: int, size: int) -> None:
203
- """Free WASM memory."""
204
- if ptr != 0 and size > 0:
205
- self._dealloc(self._store, ptr, size)
206
-
207
- def derive_public_key(self, seed: bytes) -> bytes:
208
- """
209
- Derive Ed25519 public key from a 32-byte seed.
210
-
211
- Args:
212
- seed: 32-byte Ed25519 signing seed
213
-
214
- Returns:
215
- 32-byte public key
216
-
217
- Raises:
218
- WasmRuntimeError: If derivation fails
219
- ValueError: If seed is not 32 bytes
220
- """
221
- if len(seed) != 32:
222
- raise ValueError("Seed must be exactly 32 bytes")
223
-
224
- # Allocate memory for input and output
225
- seed_ptr = self._write_bytes(seed)
226
- out_ptr = self._alloc(self._store, 32)
227
- if out_ptr == 0:
228
- self._free(seed_ptr, 32)
229
- raise WasmRuntimeError("Failed to allocate output buffer")
230
-
231
- try:
232
- result = self._derive_public_key(self._store, seed_ptr, 32, out_ptr)
233
- if result != 0:
234
- raise WasmRuntimeError(f"derive_public_key failed with code {result}")
235
-
236
- return self._read_bytes(out_ptr, 32)
237
- finally:
238
- self._free(seed_ptr, 32)
239
- self._free(out_ptr, 32)
240
-
241
- def get_public_key_hex(self, seed: bytes) -> str:
242
- """
243
- Get hex-encoded public key from a 32-byte seed.
244
-
245
- Args:
246
- seed: 32-byte Ed25519 signing seed
247
-
248
- Returns:
249
- 64-character hex string of public key
250
-
251
- Raises:
252
- WasmRuntimeError: If derivation fails
253
- ValueError: If seed is not 32 bytes
254
- """
255
- if len(seed) != 32:
256
- raise ValueError("Seed must be exactly 32 bytes")
257
-
258
- seed_ptr = self._write_bytes(seed)
259
- out_ptr = self._alloc(self._store, 64)
260
- if out_ptr == 0:
261
- self._free(seed_ptr, 32)
262
- raise WasmRuntimeError("Failed to allocate output buffer")
263
-
264
- try:
265
- result = self._get_public_key_hex(self._store, seed_ptr, 32, out_ptr)
266
- if result != 0:
267
- raise WasmRuntimeError(f"get_public_key_hex failed with code {result}")
268
-
269
- return self._read_bytes(out_ptr, 64).decode("ascii")
270
- finally:
271
- self._free(seed_ptr, 32)
272
- self._free(out_ptr, 64)
273
-
274
- def sha256(self, data: bytes) -> bytes:
275
- """
276
- Compute SHA-256 hash.
277
-
278
- Args:
279
- data: Input bytes to hash
280
-
281
- Returns:
282
- 32-byte hash
283
-
284
- Raises:
285
- WasmRuntimeError: If hashing fails
286
- """
287
- data_ptr = self._write_bytes(data)
288
- out_ptr = self._alloc(self._store, 32)
289
- if out_ptr == 0:
290
- self._free(data_ptr, len(data))
291
- raise WasmRuntimeError("Failed to allocate output buffer")
292
-
293
- try:
294
- result = self._sha256(self._store, data_ptr, len(data), out_ptr)
295
- if result != 0:
296
- raise WasmRuntimeError(f"sha256 failed with code {result}")
297
-
298
- return self._read_bytes(out_ptr, 32)
299
- finally:
300
- self._free(data_ptr, len(data))
301
- self._free(out_ptr, 32)
302
-
303
- def hash_canonical_json(self, json_str: str) -> bytes:
304
- """
305
- Hash a JSON string using RFC 8785 canonical JSON + SHA-256.
306
-
307
- Args:
308
- json_str: JSON string to canonicalize and hash
309
-
310
- Returns:
311
- 32-byte hash
312
-
313
- Raises:
314
- WasmRuntimeError: If hashing fails
315
- """
316
- json_bytes = json_str.encode("utf-8")
317
- json_ptr = self._write_bytes(json_bytes)
318
- out_ptr = self._alloc(self._store, 32)
319
- if out_ptr == 0:
320
- self._free(json_ptr, len(json_bytes))
321
- raise WasmRuntimeError("Failed to allocate output buffer")
322
-
323
- try:
324
- result = self._hash_canonical_json(
325
- self._store, json_ptr, len(json_bytes), out_ptr
326
- )
327
- if result != 0:
328
- raise WasmRuntimeError(
329
- f"hash_canonical_json failed with code {result}"
330
- )
331
-
332
- return self._read_bytes(out_ptr, 32)
333
- finally:
334
- self._free(json_ptr, len(json_bytes))
335
- self._free(out_ptr, 32)
336
-
337
- def ed25519_sign(self, seed: bytes, message: bytes) -> bytes:
338
- """
339
- Sign a message with Ed25519.
340
-
341
- Args:
342
- seed: 32-byte signing seed
343
- message: Message bytes to sign
344
-
345
- Returns:
346
- 64-byte signature
347
-
348
- Raises:
349
- WasmRuntimeError: If signing fails
350
- ValueError: If seed is not 32 bytes
351
- """
352
- if len(seed) != 32:
353
- raise ValueError("Seed must be exactly 32 bytes")
354
-
355
- seed_ptr = self._write_bytes(seed)
356
- msg_ptr = self._write_bytes(message)
357
- out_ptr = self._alloc(self._store, 64)
358
- if out_ptr == 0:
359
- self._free(seed_ptr, 32)
360
- self._free(msg_ptr, len(message))
361
- raise WasmRuntimeError("Failed to allocate output buffer")
362
-
363
- try:
364
- result = self._ed25519_sign(
365
- self._store, seed_ptr, 32, msg_ptr, len(message), out_ptr
366
- )
367
- if result != 0:
368
- raise WasmRuntimeError(f"ed25519_sign failed with code {result}")
369
-
370
- return self._read_bytes(out_ptr, 64)
371
- finally:
372
- self._free(seed_ptr, 32)
373
- self._free(msg_ptr, len(message))
374
- self._free(out_ptr, 64)
375
-
376
- def ed25519_verify(
377
- self, public_key: bytes, message: bytes, signature: bytes
378
- ) -> bool:
379
- """
380
- Verify an Ed25519 signature.
381
-
382
- Args:
383
- public_key: 32-byte public key
384
- message: Message bytes that were signed
385
- signature: 64-byte signature to verify
386
-
387
- Returns:
388
- True if signature is valid, False otherwise
389
-
390
- Raises:
391
- ValueError: If public_key or signature have wrong length
392
- """
393
- if len(public_key) != 32:
394
- raise ValueError("Public key must be exactly 32 bytes")
395
- if len(signature) != 64:
396
- raise ValueError("Signature must be exactly 64 bytes")
397
-
398
- pubkey_ptr = self._write_bytes(public_key)
399
- msg_ptr = self._write_bytes(message)
400
- sig_ptr = self._write_bytes(signature)
401
-
402
- try:
403
- result: int = self._ed25519_verify(
404
- self._store, pubkey_ptr, 32, msg_ptr, len(message), sig_ptr, 64
405
- )
406
- return result == 0
407
- finally:
408
- self._free(pubkey_ptr, 32)
409
- self._free(msg_ptr, len(message))
410
- self._free(sig_ptr, 64)
411
-
412
- def sign_attestation_json(self, seed: bytes, attestation_json: str) -> str:
413
- """
414
- Sign an attestation JSON and return SignedAttestation JSON.
415
-
416
- Args:
417
- seed: 32-byte signing seed
418
- attestation_json: JSON string of attestation payload
419
-
420
- Returns:
421
- JSON string of SignedAttestation with payload and signature
422
-
423
- Raises:
424
- WasmRuntimeError: If signing fails
425
- ValueError: If seed is not 32 bytes
426
- """
427
- if len(seed) != 32:
428
- raise ValueError("Seed must be exactly 32 bytes")
429
-
430
- json_bytes = attestation_json.encode("utf-8")
431
- seed_ptr = self._write_bytes(seed)
432
- json_ptr = self._write_bytes(json_bytes)
433
-
434
- # Allocate generous output buffer (input + base64 sig + JSON wrapper)
435
- out_cap = len(json_bytes) + 200
436
- out_ptr = self._alloc(self._store, out_cap)
437
- # size_t on wasm32 is 4 bytes, but use 8 for safety
438
- out_len_ptr = self._alloc(self._store, 8)
439
-
440
- if out_ptr == 0 or out_len_ptr == 0:
441
- self._free(seed_ptr, 32)
442
- self._free(json_ptr, len(json_bytes))
443
- self._free(out_ptr, out_cap)
444
- self._free(out_len_ptr, 8)
445
- raise WasmRuntimeError("Failed to allocate output buffer")
446
-
447
- try:
448
- result = self._sign_attestation_json(
449
- self._store,
450
- seed_ptr,
451
- 32,
452
- json_ptr,
453
- len(json_bytes),
454
- out_ptr,
455
- out_cap,
456
- out_len_ptr,
457
- )
458
- if result != 0:
459
- raise WasmRuntimeError(
460
- f"sign_attestation_json failed with code {result}"
461
- )
462
-
463
- # Read output length (4 bytes for wasm32 usize)
464
- out_len_bytes = self._read_bytes(out_len_ptr, 4)
465
- out_len = int.from_bytes(out_len_bytes, "little")
466
-
467
- # Read output JSON
468
- return self._read_bytes(out_ptr, out_len).decode("utf-8")
469
- finally:
470
- self._free(seed_ptr, 32)
471
- self._free(json_ptr, len(json_bytes))
472
- self._free(out_ptr, out_cap)
473
- self._free(out_len_ptr, 8)
474
-
475
-
476
- def sign_offline_attestation(
477
- signing_seed: bytes,
478
- payload_hash: str,
479
- service_id: str,
480
- operation_type: str,
481
- timestamp_ms: int,
482
- ) -> dict[str, Any]:
483
- """
484
- Create and sign an offline attestation.
485
-
486
- This is a convenience function that builds the attestation structure
487
- and signs it using the WASM runtime.
488
-
489
- Args:
490
- signing_seed: 32-byte Ed25519 signing seed
491
- payload_hash: SHA-256 hash of input+output (64-char hex string)
492
- service_id: Service identifier
493
- operation_type: Type of operation (e.g., "completion")
494
- timestamp_ms: Timestamp in milliseconds
495
-
496
- Returns:
497
- Dict with 'payload' and 'signature' keys (SignedAttestation structure)
498
-
499
- Raises:
500
- WasmRuntimeError: If signing fails
501
- ValueError: If signing_seed is not 32 bytes
502
- """
503
- import json
504
-
505
- runtime = WasmRuntime.get_instance()
506
-
507
- # Build a minimal attestation structure for offline use
508
- # This is simpler than AttestationL0 since we don't need DPRF sampling
509
- attestation = {
510
- "version": 1,
511
- "serviceId": service_id,
512
- "operationType": operation_type,
513
- "payloadHash": payload_hash,
514
- "timestampMs": str(timestamp_ms),
515
- "mode": "offline",
516
- }
517
-
518
- attestation_json = json.dumps(attestation, separators=(",", ":"), sort_keys=True)
519
- signed_json = runtime.sign_attestation_json(signing_seed, attestation_json)
520
-
521
- result: dict[str, Any] = json.loads(signed_json)
522
- return result
523
-
524
-
525
- def get_runtime() -> str:
526
- """
527
- Get the current runtime type name.
528
-
529
- Returns:
530
- String identifying the runtime: "WasmRuntime" or "PyNaClRuntime"
531
- """
532
- runtime = WasmRuntime.get_instance()
533
- return type(runtime).__name__