glacis 0.1.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/__init__.py +88 -0
- glacis/__main__.py +76 -0
- glacis/client.py +847 -0
- glacis/crypto.py +121 -0
- glacis/integrations/__init__.py +12 -0
- glacis/integrations/anthropic.py +222 -0
- glacis/integrations/openai.py +208 -0
- glacis/models.py +293 -0
- glacis/storage.py +331 -0
- glacis/streaming.py +363 -0
- glacis/wasm/s3p_core_wasi.wasm +0 -0
- glacis/wasm_runtime.py +519 -0
- glacis-0.1.0.dist-info/METADATA +324 -0
- glacis-0.1.0.dist-info/RECORD +16 -0
- glacis-0.1.0.dist-info/WHEEL +4 -0
- glacis-0.1.0.dist-info/licenses/LICENSE +190 -0
glacis/wasm_runtime.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
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 hashlib
|
|
11
|
+
import json
|
|
12
|
+
import ctypes
|
|
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.signing
|
|
34
|
+
import nacl.encoding
|
|
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
|
+
return cls._instance
|
|
176
|
+
|
|
177
|
+
def _write_bytes(self, data: bytes) -> int:
|
|
178
|
+
"""Allocate WASM memory and write bytes, returning the pointer."""
|
|
179
|
+
size = len(data)
|
|
180
|
+
ptr = self._alloc(self._store, size)
|
|
181
|
+
if ptr == 0:
|
|
182
|
+
raise WasmRuntimeError("Failed to allocate WASM memory")
|
|
183
|
+
|
|
184
|
+
# Write data to WASM memory using ctypes
|
|
185
|
+
mem_ptr = self._memory.data_ptr(self._store)
|
|
186
|
+
base_addr = ctypes.addressof(mem_ptr.contents)
|
|
187
|
+
dest_addr = base_addr + ptr
|
|
188
|
+
|
|
189
|
+
ctypes.memmove(dest_addr, data, size)
|
|
190
|
+
|
|
191
|
+
return ptr
|
|
192
|
+
|
|
193
|
+
def _read_bytes(self, ptr: int, size: int) -> bytes:
|
|
194
|
+
"""Read bytes from WASM memory."""
|
|
195
|
+
mem_ptr = self._memory.data_ptr(self._store)
|
|
196
|
+
base_addr = ctypes.addressof(mem_ptr.contents)
|
|
197
|
+
src_addr = base_addr + ptr
|
|
198
|
+
|
|
199
|
+
return ctypes.string_at(src_addr, size)
|
|
200
|
+
|
|
201
|
+
def _free(self, ptr: int, size: int) -> None:
|
|
202
|
+
"""Free WASM memory."""
|
|
203
|
+
if ptr != 0 and size > 0:
|
|
204
|
+
self._dealloc(self._store, ptr, size)
|
|
205
|
+
|
|
206
|
+
def derive_public_key(self, seed: bytes) -> bytes:
|
|
207
|
+
"""
|
|
208
|
+
Derive Ed25519 public key from a 32-byte seed.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
seed: 32-byte Ed25519 signing seed
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
32-byte public key
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
WasmRuntimeError: If derivation fails
|
|
218
|
+
ValueError: If seed is not 32 bytes
|
|
219
|
+
"""
|
|
220
|
+
if len(seed) != 32:
|
|
221
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
222
|
+
|
|
223
|
+
# Allocate memory for input and output
|
|
224
|
+
seed_ptr = self._write_bytes(seed)
|
|
225
|
+
out_ptr = self._alloc(self._store, 32)
|
|
226
|
+
if out_ptr == 0:
|
|
227
|
+
self._free(seed_ptr, 32)
|
|
228
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
result = self._derive_public_key(self._store, seed_ptr, 32, out_ptr)
|
|
232
|
+
if result != 0:
|
|
233
|
+
raise WasmRuntimeError(f"derive_public_key failed with code {result}")
|
|
234
|
+
|
|
235
|
+
return self._read_bytes(out_ptr, 32)
|
|
236
|
+
finally:
|
|
237
|
+
self._free(seed_ptr, 32)
|
|
238
|
+
self._free(out_ptr, 32)
|
|
239
|
+
|
|
240
|
+
def get_public_key_hex(self, seed: bytes) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Get hex-encoded public key from a 32-byte seed.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
seed: 32-byte Ed25519 signing seed
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
64-character hex string of public key
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
WasmRuntimeError: If derivation fails
|
|
252
|
+
ValueError: If seed is not 32 bytes
|
|
253
|
+
"""
|
|
254
|
+
if len(seed) != 32:
|
|
255
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
256
|
+
|
|
257
|
+
seed_ptr = self._write_bytes(seed)
|
|
258
|
+
out_ptr = self._alloc(self._store, 64)
|
|
259
|
+
if out_ptr == 0:
|
|
260
|
+
self._free(seed_ptr, 32)
|
|
261
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
result = self._get_public_key_hex(self._store, seed_ptr, 32, out_ptr)
|
|
265
|
+
if result != 0:
|
|
266
|
+
raise WasmRuntimeError(f"get_public_key_hex failed with code {result}")
|
|
267
|
+
|
|
268
|
+
return self._read_bytes(out_ptr, 64).decode("ascii")
|
|
269
|
+
finally:
|
|
270
|
+
self._free(seed_ptr, 32)
|
|
271
|
+
self._free(out_ptr, 64)
|
|
272
|
+
|
|
273
|
+
def sha256(self, data: bytes) -> bytes:
|
|
274
|
+
"""
|
|
275
|
+
Compute SHA-256 hash.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
data: Input bytes to hash
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
32-byte hash
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
WasmRuntimeError: If hashing fails
|
|
285
|
+
"""
|
|
286
|
+
data_ptr = self._write_bytes(data)
|
|
287
|
+
out_ptr = self._alloc(self._store, 32)
|
|
288
|
+
if out_ptr == 0:
|
|
289
|
+
self._free(data_ptr, len(data))
|
|
290
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
result = self._sha256(self._store, data_ptr, len(data), out_ptr)
|
|
294
|
+
if result != 0:
|
|
295
|
+
raise WasmRuntimeError(f"sha256 failed with code {result}")
|
|
296
|
+
|
|
297
|
+
return self._read_bytes(out_ptr, 32)
|
|
298
|
+
finally:
|
|
299
|
+
self._free(data_ptr, len(data))
|
|
300
|
+
self._free(out_ptr, 32)
|
|
301
|
+
|
|
302
|
+
def hash_canonical_json(self, json_str: str) -> bytes:
|
|
303
|
+
"""
|
|
304
|
+
Hash a JSON string using RFC 8785 canonical JSON + SHA-256.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
json_str: JSON string to canonicalize and hash
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
32-byte hash
|
|
311
|
+
|
|
312
|
+
Raises:
|
|
313
|
+
WasmRuntimeError: If hashing fails
|
|
314
|
+
"""
|
|
315
|
+
json_bytes = json_str.encode("utf-8")
|
|
316
|
+
json_ptr = self._write_bytes(json_bytes)
|
|
317
|
+
out_ptr = self._alloc(self._store, 32)
|
|
318
|
+
if out_ptr == 0:
|
|
319
|
+
self._free(json_ptr, len(json_bytes))
|
|
320
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
result = self._hash_canonical_json(
|
|
324
|
+
self._store, json_ptr, len(json_bytes), out_ptr
|
|
325
|
+
)
|
|
326
|
+
if result != 0:
|
|
327
|
+
raise WasmRuntimeError(
|
|
328
|
+
f"hash_canonical_json failed with code {result}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return self._read_bytes(out_ptr, 32)
|
|
332
|
+
finally:
|
|
333
|
+
self._free(json_ptr, len(json_bytes))
|
|
334
|
+
self._free(out_ptr, 32)
|
|
335
|
+
|
|
336
|
+
def ed25519_sign(self, seed: bytes, message: bytes) -> bytes:
|
|
337
|
+
"""
|
|
338
|
+
Sign a message with Ed25519.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
seed: 32-byte signing seed
|
|
342
|
+
message: Message bytes to sign
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
64-byte signature
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
WasmRuntimeError: If signing fails
|
|
349
|
+
ValueError: If seed is not 32 bytes
|
|
350
|
+
"""
|
|
351
|
+
if len(seed) != 32:
|
|
352
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
353
|
+
|
|
354
|
+
seed_ptr = self._write_bytes(seed)
|
|
355
|
+
msg_ptr = self._write_bytes(message)
|
|
356
|
+
out_ptr = self._alloc(self._store, 64)
|
|
357
|
+
if out_ptr == 0:
|
|
358
|
+
self._free(seed_ptr, 32)
|
|
359
|
+
self._free(msg_ptr, len(message))
|
|
360
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
result = self._ed25519_sign(
|
|
364
|
+
self._store, seed_ptr, 32, msg_ptr, len(message), out_ptr
|
|
365
|
+
)
|
|
366
|
+
if result != 0:
|
|
367
|
+
raise WasmRuntimeError(f"ed25519_sign failed with code {result}")
|
|
368
|
+
|
|
369
|
+
return self._read_bytes(out_ptr, 64)
|
|
370
|
+
finally:
|
|
371
|
+
self._free(seed_ptr, 32)
|
|
372
|
+
self._free(msg_ptr, len(message))
|
|
373
|
+
self._free(out_ptr, 64)
|
|
374
|
+
|
|
375
|
+
def ed25519_verify(
|
|
376
|
+
self, public_key: bytes, message: bytes, signature: bytes
|
|
377
|
+
) -> bool:
|
|
378
|
+
"""
|
|
379
|
+
Verify an Ed25519 signature.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
public_key: 32-byte public key
|
|
383
|
+
message: Message bytes that were signed
|
|
384
|
+
signature: 64-byte signature to verify
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
True if signature is valid, False otherwise
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
ValueError: If public_key or signature have wrong length
|
|
391
|
+
"""
|
|
392
|
+
if len(public_key) != 32:
|
|
393
|
+
raise ValueError("Public key must be exactly 32 bytes")
|
|
394
|
+
if len(signature) != 64:
|
|
395
|
+
raise ValueError("Signature must be exactly 64 bytes")
|
|
396
|
+
|
|
397
|
+
pubkey_ptr = self._write_bytes(public_key)
|
|
398
|
+
msg_ptr = self._write_bytes(message)
|
|
399
|
+
sig_ptr = self._write_bytes(signature)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
result = self._ed25519_verify(
|
|
403
|
+
self._store, pubkey_ptr, 32, msg_ptr, len(message), sig_ptr, 64
|
|
404
|
+
)
|
|
405
|
+
return result == 0
|
|
406
|
+
finally:
|
|
407
|
+
self._free(pubkey_ptr, 32)
|
|
408
|
+
self._free(msg_ptr, len(message))
|
|
409
|
+
self._free(sig_ptr, 64)
|
|
410
|
+
|
|
411
|
+
def sign_attestation_json(self, seed: bytes, attestation_json: str) -> str:
|
|
412
|
+
"""
|
|
413
|
+
Sign an attestation JSON and return SignedAttestation JSON.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
seed: 32-byte signing seed
|
|
417
|
+
attestation_json: JSON string of attestation payload
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
JSON string of SignedAttestation with payload and signature
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
WasmRuntimeError: If signing fails
|
|
424
|
+
ValueError: If seed is not 32 bytes
|
|
425
|
+
"""
|
|
426
|
+
if len(seed) != 32:
|
|
427
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
428
|
+
|
|
429
|
+
json_bytes = attestation_json.encode("utf-8")
|
|
430
|
+
seed_ptr = self._write_bytes(seed)
|
|
431
|
+
json_ptr = self._write_bytes(json_bytes)
|
|
432
|
+
|
|
433
|
+
# Allocate generous output buffer (input + base64 sig + JSON wrapper)
|
|
434
|
+
out_cap = len(json_bytes) + 200
|
|
435
|
+
out_ptr = self._alloc(self._store, out_cap)
|
|
436
|
+
out_len_ptr = self._alloc(self._store, 8) # size_t on wasm32 is 4 bytes, but use 8 for safety
|
|
437
|
+
|
|
438
|
+
if out_ptr == 0 or out_len_ptr == 0:
|
|
439
|
+
self._free(seed_ptr, 32)
|
|
440
|
+
self._free(json_ptr, len(json_bytes))
|
|
441
|
+
self._free(out_ptr, out_cap)
|
|
442
|
+
self._free(out_len_ptr, 8)
|
|
443
|
+
raise WasmRuntimeError("Failed to allocate output buffer")
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
result = self._sign_attestation_json(
|
|
447
|
+
self._store,
|
|
448
|
+
seed_ptr,
|
|
449
|
+
32,
|
|
450
|
+
json_ptr,
|
|
451
|
+
len(json_bytes),
|
|
452
|
+
out_ptr,
|
|
453
|
+
out_cap,
|
|
454
|
+
out_len_ptr,
|
|
455
|
+
)
|
|
456
|
+
if result != 0:
|
|
457
|
+
raise WasmRuntimeError(
|
|
458
|
+
f"sign_attestation_json failed with code {result}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Read output length (4 bytes for wasm32 usize)
|
|
462
|
+
out_len_bytes = self._read_bytes(out_len_ptr, 4)
|
|
463
|
+
out_len = int.from_bytes(out_len_bytes, "little")
|
|
464
|
+
|
|
465
|
+
# Read output JSON
|
|
466
|
+
return self._read_bytes(out_ptr, out_len).decode("utf-8")
|
|
467
|
+
finally:
|
|
468
|
+
self._free(seed_ptr, 32)
|
|
469
|
+
self._free(json_ptr, len(json_bytes))
|
|
470
|
+
self._free(out_ptr, out_cap)
|
|
471
|
+
self._free(out_len_ptr, 8)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def sign_offline_attestation(
|
|
475
|
+
signing_seed: bytes,
|
|
476
|
+
payload_hash: str,
|
|
477
|
+
service_id: str,
|
|
478
|
+
operation_type: str,
|
|
479
|
+
timestamp_ms: int,
|
|
480
|
+
) -> dict[str, Any]:
|
|
481
|
+
"""
|
|
482
|
+
Create and sign an offline attestation.
|
|
483
|
+
|
|
484
|
+
This is a convenience function that builds the attestation structure
|
|
485
|
+
and signs it using the WASM runtime.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
signing_seed: 32-byte Ed25519 signing seed
|
|
489
|
+
payload_hash: SHA-256 hash of input+output (64-char hex string)
|
|
490
|
+
service_id: Service identifier
|
|
491
|
+
operation_type: Type of operation (e.g., "completion")
|
|
492
|
+
timestamp_ms: Timestamp in milliseconds
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Dict with 'payload' and 'signature' keys (SignedAttestation structure)
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
WasmRuntimeError: If signing fails
|
|
499
|
+
ValueError: If signing_seed is not 32 bytes
|
|
500
|
+
"""
|
|
501
|
+
import json
|
|
502
|
+
|
|
503
|
+
runtime = WasmRuntime.get_instance()
|
|
504
|
+
|
|
505
|
+
# Build a minimal attestation structure for offline use
|
|
506
|
+
# This is simpler than AttestationL0 since we don't need DPRF sampling
|
|
507
|
+
attestation = {
|
|
508
|
+
"version": 1,
|
|
509
|
+
"serviceId": service_id,
|
|
510
|
+
"operationType": operation_type,
|
|
511
|
+
"payloadHash": payload_hash,
|
|
512
|
+
"timestampMs": str(timestamp_ms),
|
|
513
|
+
"mode": "offline",
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
attestation_json = json.dumps(attestation, separators=(",", ":"), sort_keys=True)
|
|
517
|
+
signed_json = runtime.sign_attestation_json(signing_seed, attestation_json)
|
|
518
|
+
|
|
519
|
+
return json.loads(signed_json)
|