diff --git a/app-core/lib/attested_wallet_api.dart b/app-core/lib/attested_wallet_api.dart index 42601a54..babd3a15 100644 --- a/app-core/lib/attested_wallet_api.dart +++ b/app-core/lib/attested_wallet_api.dart @@ -1,142 +1,324 @@ -/// Attested REST transport that routes HTTP through the enclave FFI client -/// running in a background isolate (non-blocking). +/// Attested REST transport. Dart owns HTTP (`package:http`); the Rust +/// FFI is called only for the crypto-heavy work: COSE_Sign1 / X.509 +/// verification of the attestation document, and BIP-340 Schnorr +/// verification of each response. /// -/// Every request is verified against the enclave's attestation document (PCR0) -/// and response signatures are checked (BIP-340 Schnorr). +/// Per-request flow: +/// 1. ensure attestation cache is fresh (re-verify on TTL expiry) +/// 2. POST `/` via `package:http` +/// 3. read `X-Attestation-Signature` header +/// 4. FFI `verify_schnorr_signature(body, sig, pubkey)` (sub-ms) +/// 5. parse body as JSON, return library; +import 'dart:async'; import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; -import 'enclave/async_enclave.dart'; -import 'enclave/native_enclave.dart' show AttestationStatus; +import 'package:crypto/crypto.dart' show sha256; +import 'package:http/http.dart' as http; +import 'package:protocol/protocol.dart'; + +import 'enclave/native_enclave.dart'; import 'rest_wallet_api.dart'; import 'wallet_api.dart'; -import 'package:protocol/protocol.dart'; -/// WalletApi implementation that uses the enclave FFI client for attested HTTP. -/// -/// Delegates all serialization to [RestWalletApi] but routes HTTP through -/// [AsyncEnclaveClient] for attestation verification + response signing. -/// FFI calls run in a background isolate to avoid blocking the main thread. +const int _nonceLen = 20; + +/// `WalletApi` whose every response is bound to an attested enclave. class AttestedWalletApi implements WalletApi { - final AsyncEnclaveClient _enclave; + final _Attested _attested; final RestWalletApi _inner; - AttestedWalletApi._(this._enclave, this._inner); + AttestedWalletApi._(this._attested, this._inner); - /// Create an attested wallet API. - /// Must be called with `await` -- initializes the background isolate. + /// Build an attested client and run an initial verification. + /// + /// Throws if the enclave is unreachable, the document fails to verify, + /// or PCR0 doesn't match [expectedPcr0]. static Future create( String baseUrl, { required String expectedPcr0, int cacheTtlSecs = 60, + http.Client? httpClient, }) async { - final enclave = await AsyncEnclaveClient.create( - baseUrl, expectedPcr0, - cacheTtlSecs: cacheTtlSecs, + final attested = _Attested( + baseUrl: baseUrl, + expectedPcr0: expectedPcr0, + ttl: Duration(seconds: cacheTtlSecs), + httpClient: httpClient ?? http.Client(), ); - // Verify attestation eagerly — fail fast if enclave is unreachable - // or PCR0 doesn't match. This also populates the status cache so - // the attestation card shows immediately in the UI. - await enclave.verify(); + await attested.verify(); final inner = RestWalletApi.withPostFn(baseUrl, (path, body) async { - // Retry once on transient errors (attestation warmup, connection reset). + // Retry once on transient HTTP / signature-verify failures, matching + // the prior behavior. A second consecutive failure propagates. + Object? lastErr; for (var attempt = 0; attempt < 2; attempt++) { - final resp = await enclave.post(path, jsonEncode(body)); - if (resp.error != null || resp.statusCode == 0) { - if (attempt == 0) { - print('[AttestedWalletApi] Retrying $path (err: ${resp.error})'); - continue; - } - throw Exception(resp.error ?? 'Enclave request failed'); - } - if (resp.body.isEmpty) { - throw Exception('Empty response from enclave (HTTP ${resp.statusCode})'); + try { + return await attested.post(path, body); + } catch (e) { + lastErr = e; } - if (resp.statusCode != 200) { - try { - final errBody = jsonDecode(resp.body); - throw Exception( - errBody['error'] ?? 'HTTP ${resp.statusCode}: ${resp.body}'); - } catch (e) { - if (e is Exception) rethrow; - throw Exception('HTTP ${resp.statusCode}: ${resp.body}'); - } - } - return jsonDecode(resp.body) as Map; } - throw Exception('Enclave request failed after retries'); + throw Exception('Enclave request failed: $lastErr'); }); - return AttestedWalletApi._(enclave, inner); + + return AttestedWalletApi._(attested, inner); } - /// Current attestation status for UI display. - Future getAttestationStatus() => - _enclave.getAttestationStatus(); + /// Cached attestation status for UI display. Synchronous — no FFI call. + Future getAttestationStatus() async => _attested.status; - // Delegate all WalletApi methods to _inner (which uses attested POST). - @override - Future dKGStep1(DKGStep1Request r) => _inner.dKGStep1(r); - @override - Future dKGStep2(DKGStep2Request r) => _inner.dKGStep2(r); - @override - Future dKGStep3(DKGStep3Request r) => _inner.dKGStep3(r); - @override - Future signStep1(SignStep1Request r) => _inner.signStep1(r); - @override - Future signStep2(SignStep2Request r) => _inner.signStep2(r); - @override - Future refreshStep1(RefreshStep1Request r) => _inner.refreshStep1(r); - @override - Future refreshStep2(RefreshStep2Request r) => _inner.refreshStep2(r); - @override - Future refreshStep3(RefreshStep3Request r) => _inner.refreshStep3(r); - @override - Future getPolicyId(GetPolicyIdRequest r) => _inner.getPolicyId(r); - @override - Future updatePolicy(UpdatePolicyRequest r) => _inner.updatePolicy(r); - @override - Future deletePolicy(DeletePolicyRequest r) => _inner.deletePolicy(r); - @override - Future broadcastTransaction(BroadcastTransactionRequest r) => _inner.broadcastTransaction(r); - @override - Future fetchHistory(FetchHistoryRequest r) => _inner.fetchHistory(r); - @override - Future fetchRecentTransactions(FetchRecentTransactionsRequest r) => _inner.fetchRecentTransactions(r); - @override - Future getArkInfo(GetArkInfoRequest r) => _inner.getArkInfo(r); - @override - Future getArkAddress(GetArkAddressRequest r) => _inner.getArkAddress(r); - @override - Future getBoardingAddress(GetBoardingAddressRequest r) => _inner.getBoardingAddress(r); - @override - Future checkBoardingBalance(CheckBoardingBalanceRequest r) => _inner.checkBoardingBalance(r); - @override - Future listVtxos(ListVtxosRequest r) => _inner.listVtxos(r); - @override - Future listArkTransactions(ListArkTransactionsRequest r) => _inner.listArkTransactions(r); - @override - Future sendVtxo(SendVtxoRequest r) => _inner.sendVtxo(r); - @override - Future redeemVtxo(RedeemVtxoRequest r) => _inner.redeemVtxo(r); - @override - Future settle(SettleRequest r) => _inner.settle(r); - @override - Future settleDelegate(SettleDelegateRequest r) => _inner.settleDelegate(r); - @override - Future submitArkSend(SubmitArkSendRequest r) => _inner.submitArkSend(r); + // ── WalletApi delegation ─────────────────────────────────────────── + @override Future dKGStep1(DKGStep1Request r) => _inner.dKGStep1(r); + @override Future dKGStep2(DKGStep2Request r) => _inner.dKGStep2(r); + @override Future dKGStep3(DKGStep3Request r) => _inner.dKGStep3(r); + @override Future signStep1(SignStep1Request r) => _inner.signStep1(r); + @override Future signStep2(SignStep2Request r) => _inner.signStep2(r); + @override Future refreshStep1(RefreshStep1Request r) => _inner.refreshStep1(r); + @override Future refreshStep2(RefreshStep2Request r) => _inner.refreshStep2(r); + @override Future refreshStep3(RefreshStep3Request r) => _inner.refreshStep3(r); + @override Future getPolicyId(GetPolicyIdRequest r) => _inner.getPolicyId(r); + @override Future updatePolicy(UpdatePolicyRequest r) => _inner.updatePolicy(r); + @override Future deletePolicy(DeletePolicyRequest r) => _inner.deletePolicy(r); + @override Future broadcastTransaction(BroadcastTransactionRequest r) => _inner.broadcastTransaction(r); + @override Future fetchHistory(FetchHistoryRequest r) => _inner.fetchHistory(r); + @override Future fetchRecentTransactions(FetchRecentTransactionsRequest r) => _inner.fetchRecentTransactions(r); + @override Future getArkInfo(GetArkInfoRequest r) => _inner.getArkInfo(r); + @override Future getArkAddress(GetArkAddressRequest r) => _inner.getArkAddress(r); + @override Future getBoardingAddress(GetBoardingAddressRequest r) => _inner.getBoardingAddress(r); + @override Future checkBoardingBalance(CheckBoardingBalanceRequest r) => _inner.checkBoardingBalance(r); + @override Future listVtxos(ListVtxosRequest r) => _inner.listVtxos(r); + @override Future listArkTransactions(ListArkTransactionsRequest r) => _inner.listArkTransactions(r); + @override Future sendVtxo(SendVtxoRequest r) => _inner.sendVtxo(r); + @override Future redeemVtxo(RedeemVtxoRequest r) => _inner.redeemVtxo(r); + @override Future settle(SettleRequest r) => _inner.settle(r); + @override Future settleDelegate(SettleDelegateRequest r) => _inner.settleDelegate(r); + @override Future submitArkSend(SubmitArkSendRequest r) => _inner.submitArkSend(r); + @override Future getServerInfo(GetServerInfoRequest r) => _inner.getServerInfo(r); + @override Future registerDeviceToken(RegisterDeviceTokenRequest r) => _inner.registerDeviceToken(r); @override - Future getServerInfo(GetServerInfoRequest r) => _inner.getServerInfo(r); + Future shutdown() async { + _attested.dispose(); + } +} - @override - Future registerDeviceToken( - RegisterDeviceTokenRequest r) => - _inner.registerDeviceToken(r); +// ── Internals ──────────────────────────────────────────────────────── - @override - Future shutdown() async { - await _enclave.dispose(); +class _AttestationCache { + final String pcr0; + final String attestationKey; + final DateTime verifiedAt; + _AttestationCache({ + required this.pcr0, + required this.attestationKey, + required this.verifiedAt, + }); +} + +class _Attested { + final String baseUrl; + final String expectedPcr0; + final Duration ttl; + final http.Client _http; + final Random _rng = Random.secure(); + + /// In-flight verify guard: dedupe concurrent re-verifications so the + /// first request after TTL expiry triggers one HTTP round-trip, not N. + Future<_AttestationCache>? _verifyInFlight; + _AttestationCache? _cache; + + _Attested({ + required this.baseUrl, + required this.expectedPcr0, + required this.ttl, + required http.Client httpClient, + }) : _http = httpClient; + + void dispose() { + _http.close(); + } + + AttestationStatus get status { + final c = _cache; + if (c == null) return AttestationStatus.unverified(); + final remaining = ttl - _elapsedSince(c.verifiedAt); + return AttestationStatus( + verified: true, + pcr0: c.pcr0, + verifiedAtEpochSecs: c.verifiedAt.millisecondsSinceEpoch ~/ 1000, + ttlRemainingSecs: + remaining.isNegative ? 0 : remaining.inSeconds, + attestationKey: c.attestationKey, + ); + } + + Future<_AttestationCache> _ensureFresh() { + final c = _cache; + if (c != null && _elapsedSince(c.verifiedAt) < ttl) { + return Future.value(c); + } + return verify(); + } + + /// Wall-clock elapsed time, clamped to zero on backward clock jumps + /// (NTP correction, manual time change). A naive `now - verifiedAt` + /// goes negative across a backward jump, which is harmless for the + /// `< ttl` cache check but produces nonsense `ttlRemainingSecs` + /// values in the UI (e.g. "300s remaining" on a 60s TTL). The clamp + /// keeps both the cache decision and the UI status sane. The cache + /// still self-heals across the next failed signature verify if the + /// enclave actually rotated underneath us. + Duration _elapsedSince(DateTime t) { + final d = DateTime.now().difference(t); + return d.isNegative ? Duration.zero : d; + } + + /// Force a full attestation round-trip. Dedupes concurrent callers. + Future<_AttestationCache> verify() { + return _verifyInFlight ??= _doVerify().whenComplete(() { + _verifyInFlight = null; + }); + } + + Future<_AttestationCache> _doVerify() async { + // 1. Fresh nonce so a replay of an older signed doc fails. + final nonce = Uint8List.fromList( + List.generate(_nonceLen, (_) => _rng.nextInt(256))); + final nonceHex = _hex(nonce); + + // 2. Fetch document. + final docResp = await _http + .get(Uri.parse('$baseUrl/enclave/attestation?nonce=$nonceHex')); + if (docResp.statusCode != 200) { + throw Exception( + 'attestation fetch HTTP ${docResp.statusCode}: ${docResp.body}'); + } + final docBytes = _unwrapDocumentEnvelope(docResp.body); + + // 3. Hand to Rust for COSE/X.509/PCR0/nonce verification. + final v = EnclaveVerifier.verifyAttestationDoc( + docBytes: docBytes, + expectedPcr0: expectedPcr0, + nonce: nonce, + ); + if (!v.ok) { + throw Exception('attestation verify failed: ${v.error}'); + } + + // 4. Fetch the enclave's attestation pubkey and bind it via SHA256. + final infoResp = + await _http.get(Uri.parse('$baseUrl/v1/enclave-info')); + if (infoResp.statusCode != 200) { + throw Exception( + 'enclave-info HTTP ${infoResp.statusCode}: ${infoResp.body}'); + } + final info = jsonDecode(infoResp.body) as Map; + final pubkeyHex = (info['attestation_pubkey'] as String?) ?? ''; + if (pubkeyHex.isEmpty) { + throw Exception('enclave reports no attestation pubkey'); + } + if (v.appKeyHash.isNotEmpty) { + final expected = + sha256.convert(_unhex(pubkeyHex)).bytes; + final got = _unhex(v.appKeyHash); + if (!_constantTimeEq(expected, got)) { + throw Exception( + 'appKeyHash mismatch: SHA256($pubkeyHex) != ${v.appKeyHash}'); + } + } + + final cache = _AttestationCache( + pcr0: v.pcr0, + attestationKey: pubkeyHex, + verifiedAt: DateTime.now(), + ); + _cache = cache; + return cache; + } + + /// Make a POST and verify the response signature. The path is + /// relative to `baseUrl` (e.g. `/api/u//sign-step1`); the body is + /// the JSON map RestWalletApi wants to send. + Future> post( + String path, Map body) async { + final cache = await _ensureFresh(); + + final resp = await _http.post( + Uri.parse('$baseUrl$path'), + headers: const {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + + // Verify response signature. The server signs SHA256(body) with + // BIP-340 Schnorr using the attestation pubkey. + final sigHex = resp.headers['x-attestation-signature'] ?? ''; + if (sigHex.isEmpty) { + throw Exception( + 'response missing X-Attestation-Signature header (HTTP ${resp.statusCode})'); + } + final v = EnclaveVerifier.verifySchnorrSignature( + body: resp.bodyBytes, + sigHex: sigHex, + pubkeyHex: cache.attestationKey, + ); + if (!v.ok) { + throw Exception('response signature verify failed: ${v.error}'); + } + + if (resp.statusCode != 200) { + try { + final err = jsonDecode(resp.body); + throw Exception(err['error'] ?? 'HTTP ${resp.statusCode}: ${resp.body}'); + } catch (e) { + if (e is Exception) rethrow; + throw Exception('HTTP ${resp.statusCode}: ${resp.body}'); + } + } + if (resp.body.isEmpty) { + throw Exception('empty response body (HTTP ${resp.statusCode})'); + } + return jsonDecode(resp.body) as Map; + } +} + +/// The attestation endpoint may return raw base64 or `{"document": "..."}`. +/// Unwrap either to raw bytes. +Uint8List _unwrapDocumentEnvelope(String payload) { + final trimmed = payload.trim(); + String b64; + if (trimmed.startsWith('{')) { + final json = jsonDecode(trimmed); + if (json is Map && json['document'] is String) { + b64 = json['document'] as String; + } else { + b64 = trimmed; + } + } else { + b64 = trimmed; + } + return base64Decode(b64); +} + +String _hex(List bytes) => + bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + +Uint8List _unhex(String s) { + final out = Uint8List(s.length ~/ 2); + for (var i = 0; i < out.length; i++) { + out[i] = int.parse(s.substring(i * 2, i * 2 + 2), radix: 16); + } + return out; +} + +bool _constantTimeEq(List a, List b) { + if (a.length != b.length) return false; + var diff = 0; + for (var i = 0; i < a.length; i++) { + diff |= a[i] ^ b[i]; } + return diff == 0; } diff --git a/app-core/lib/enclave/async_enclave.dart b/app-core/lib/enclave/async_enclave.dart deleted file mode 100644 index 125ce1ff..00000000 --- a/app-core/lib/enclave/async_enclave.dart +++ /dev/null @@ -1,268 +0,0 @@ -/// Async wrapper for NativeEnclaveClient that runs FFI calls in a -/// background isolate to avoid blocking the Dart main isolate. -/// -/// The NativeEnclaveClient uses synchronous FFI calls (Rust block_on) -/// which freeze the main isolate on Android, killing USB callbacks and -/// UI updates. This wrapper runs all FFI in a dedicated background isolate. -library; - -import 'dart:async'; -import 'dart:convert'; -import 'dart:developer' as developer; -import 'dart:isolate'; - -import 'native_enclave.dart'; - -/// Debug-safe print that works in both Flutter and pure Dart (e2e tests). -void _log(String message) { - developer.log(message, name: 'AsyncEnclave'); -} - -/// Message types for isolate communication. -sealed class _Request {} - -class _PostRequest extends _Request { - final String path; - final String body; - final SendPort replyPort; - _PostRequest(this.path, this.body, this.replyPort); -} - -class _GetRequest extends _Request { - final String path; - final SendPort replyPort; - _GetRequest(this.path, this.replyPort); -} - -class _StatusRequest extends _Request { - final SendPort replyPort; - _StatusRequest(this.replyPort); -} - -class _VerifyRequest extends _Request { - final SendPort replyPort; - _VerifyRequest(this.replyPort); -} - -class _DisposeRequest extends _Request { - final SendPort replyPort; - _DisposeRequest(this.replyPort); -} - -/// Config passed to the background isolate on startup. -class _IsolateConfig { - final String baseUrl; - final String expectedPcr0; - final int cacheTtlSecs; - final SendPort mainPort; - _IsolateConfig( - this.baseUrl, this.expectedPcr0, this.cacheTtlSecs, this.mainPort); -} - -/// Non-blocking enclave client that runs FFI calls in a background isolate. -class AsyncEnclaveClient { - late final SendPort _isolatePort; - late final Isolate _isolate; - bool _disposed = false; - - AsyncEnclaveClient._(); - - /// Create and initialize the async enclave client. - /// The background isolate is spawned and the NativeEnclaveClient is - /// created inside it. - static Future create( - String baseUrl, - String expectedPcr0, { - int cacheTtlSecs = 60, - }) async { - final client = AsyncEnclaveClient._(); - - final receivePort = ReceivePort(); - final config = _IsolateConfig( - baseUrl, - expectedPcr0, - cacheTtlSecs, - receivePort.sendPort, - ); - - client._isolate = await Isolate.spawn(_isolateEntry, config); - - // Wait for the background isolate to send its SendPort (or error). - final completer = Completer(); - receivePort.listen((msg) { - if (msg is SendPort && !completer.isCompleted) { - completer.complete(msg); - } else if (msg is String && - msg.startsWith('ERROR:') && - !completer.isCompleted) { - completer.completeError(StateError(msg)); - } - }); - client._isolatePort = await completer.future; - receivePort.close(); - _log('[AsyncEnclave] Background isolate ready'); - - return client; - } - - /// Background isolate entry point. - static void _isolateEntry(_IsolateConfig config) { - NativeEnclaveClient client; - try { - client = NativeEnclaveClient( - config.baseUrl, - config.expectedPcr0, - cacheTtlSecs: config.cacheTtlSecs, - ); - } catch (e) { - _log( - '[AsyncEnclave] Failed to create NativeEnclaveClient in isolate: $e'); - // Send error back -- the main isolate will get a null SendPort - config.mainPort.send('ERROR: $e'); - return; - } - _log('[AsyncEnclave] NativeEnclaveClient created in background isolate'); - - final receivePort = ReceivePort(); - config.mainPort.send(receivePort.sendPort); - - receivePort.listen((msg) { - if (msg is _PostRequest) { - try { - _log('[AsyncEnclave] POST ${msg.path}...'); - final resp = client.post(msg.path, msg.body); - _log( - '[AsyncEnclave] POST ${msg.path} -> ${resp.statusCode} (${resp.body.length} bytes, sig=${resp.signatureVerified})'); - msg.replyPort.send(jsonEncode({ - 'status_code': resp.statusCode, - 'body': resp.body, - 'signature_verified': resp.signatureVerified, - })); - } catch (e) { - _log('[AsyncEnclave] POST ${msg.path} ERROR: $e'); - msg.replyPort.send(jsonEncode({'error': e.toString()})); - } - } else if (msg is _GetRequest) { - try { - final resp = client.get(msg.path); - msg.replyPort.send(jsonEncode({ - 'status_code': resp.statusCode, - 'body': resp.body, - 'signature_verified': resp.signatureVerified, - })); - } catch (e) { - msg.replyPort.send(jsonEncode({'error': e.toString()})); - } - } else if (msg is _StatusRequest) { - try { - final status = client.attestationStatus; - _log('[AsyncEnclave] Status: verified=${status.verified}, pcr0=${status.pcr0.length}, ttl=${status.ttlRemainingSecs}, epoch=${status.verifiedAtEpochSecs}'); - msg.replyPort.send(jsonEncode({ - 'verified': status.verified, - 'pcr0': status.pcr0, - 'verified_at_epoch_secs': status.verifiedAtEpochSecs, - 'ttl_remaining_secs': status.ttlRemainingSecs, - 'attestation_key': status.attestationKey, - })); - } catch (e) { - msg.replyPort.send(jsonEncode({ - 'verified': false, - 'error': e.toString(), - })); - } - } else if (msg is _VerifyRequest) { - try { - final status = client.verify(); - msg.replyPort.send(jsonEncode({ - 'verified': status.verified, - 'pcr0': status.pcr0, - 'verified_at_epoch_secs': status.verifiedAtEpochSecs, - 'ttl_remaining_secs': status.ttlRemainingSecs, - 'attestation_key': status.attestationKey, - })); - } catch (e) { - msg.replyPort.send(jsonEncode({ - 'verified': false, - 'error': e.toString(), - })); - } - } else if (msg is _DisposeRequest) { - client.dispose(); - msg.replyPort.send('done'); - receivePort.close(); - } - }); - } - - /// Make a verified POST request (non-blocking). - Future post(String path, String body) async { - _checkDisposed(); - final replyPort = ReceivePort(); - _isolatePort.send(_PostRequest(path, body, replyPort.sendPort)); - final result = await replyPort.first as String; - replyPort.close(); - final json = jsonDecode(result) as Map; - if (json.containsKey('error') && !json.containsKey('status_code')) { - throw Exception(json['error']); - } - return EnclaveResponse.fromJson(json); - } - - /// Make a verified GET request (non-blocking). - Future get(String path) async { - _checkDisposed(); - final replyPort = ReceivePort(); - _isolatePort.send(_GetRequest(path, replyPort.sendPort)); - final result = await replyPort.first as String; - replyPort.close(); - final json = jsonDecode(result) as Map; - if (json.containsKey('error') && !json.containsKey('status_code')) { - throw Exception(json['error']); - } - return EnclaveResponse.fromJson(json); - } - - /// Get attestation status (non-blocking). - Future getAttestationStatus() async { - _checkDisposed(); - final replyPort = ReceivePort(); - _isolatePort.send(_StatusRequest(replyPort.sendPort)); - final result = await replyPort.first as String; - replyPort.close(); - return AttestationStatus.fromJson( - jsonDecode(result) as Map); - } - - /// Force attestation verification and return the result. - /// Throws if verification fails (PCR0 mismatch, enclave unreachable, etc.). - Future verify() async { - _checkDisposed(); - final replyPort = ReceivePort(); - _isolatePort.send(_VerifyRequest(replyPort.sendPort)); - final result = await replyPort.first as String; - replyPort.close(); - final status = AttestationStatus.fromJson( - jsonDecode(result) as Map); - if (!status.verified) { - throw StateError( - 'Attestation verification failed: ${status.error ?? "PCR0 mismatch or enclave unreachable"}'); - } - return status; - } - - /// Dispose the client and kill the background isolate. - Future dispose() async { - if (!_disposed) { - _disposed = true; - final replyPort = ReceivePort(); - _isolatePort.send(_DisposeRequest(replyPort.sendPort)); - await replyPort.first; - replyPort.close(); - _isolate.kill(); - } - } - - void _checkDisposed() { - if (_disposed) throw StateError('AsyncEnclaveClient has been disposed'); - } -} diff --git a/app-core/lib/enclave/native_enclave.dart b/app-core/lib/enclave/native_enclave.dart index 6a6853d5..13ae1831 100644 --- a/app-core/lib/enclave/native_enclave.dart +++ b/app-core/lib/enclave/native_enclave.dart @@ -1,42 +1,56 @@ -/// Dart FFI bindings for the enclave-client Rust crate. +/// Dart FFI bindings for the pure-verification `enclave-client` Rust crate. /// -/// Provides verified HTTP communication with AWS Nitro Enclaves, -/// including attestation verification and response signature checking. +/// Three stateless functions — Dart owns HTTP, retries, timeouts, and the +/// attestation cache; Rust only does the crypto-heavy work (COSE_Sign1 +/// over CBOR, X.509 chain validation against the AWS Nitro root CA, +/// NIST P-384 ECDSA, BIP-340 Schnorr over secp256k1). library; import 'dart:convert'; import 'dart:ffi'; +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'native_library.dart'; -// FFI function signatures -typedef _NewNative = Pointer Function( - Pointer, Pointer, Uint32); -typedef _NewDart = Pointer Function( - Pointer, Pointer, int); +// ── FFI typedefs ───────────────────────────────────────────────────── -typedef _PostNative = Pointer Function( - Pointer, Pointer, Pointer); -typedef _PostDart = Pointer Function( - Pointer, Pointer, Pointer); +typedef _VerifyDocNative = Pointer Function( + Pointer, Size, Pointer, Pointer, Size); +typedef _VerifyDocDart = Pointer Function( + Pointer, int, Pointer, Pointer, int); -typedef _GetNative = Pointer Function(Pointer, Pointer); -typedef _GetDart = Pointer Function(Pointer, Pointer); - -typedef _StatusNative = Pointer Function(Pointer); -typedef _StatusDart = Pointer Function(Pointer); - -typedef _VerifyNative = Pointer Function(Pointer); -typedef _VerifyDart = Pointer Function(Pointer); - -typedef _FreeClientNative = Void Function(Pointer); -typedef _FreeClientDart = void Function(Pointer); +typedef _VerifySigNative = Pointer Function( + Pointer, Size, Pointer, Pointer); +typedef _VerifySigDart = Pointer Function( + Pointer, int, Pointer, Pointer); typedef _FreeStringNative = Void Function(Pointer); typedef _FreeStringDart = void Function(Pointer); -/// Attestation verification status from the enclave client. +// ── Lazily-looked-up function handles ──────────────────────────────── + +_VerifyDocDart? _verifyDocFn; +_VerifySigDart? _verifySigFn; +_FreeStringDart? _freeStringFn; + +void _ensureLoaded() { + if (_freeStringFn != null) return; + final lib = nativeLib; + _verifyDocFn = lib.lookupFunction<_VerifyDocNative, _VerifyDocDart>( + 'enclave_verify_attestation_doc'); + _verifySigFn = lib.lookupFunction<_VerifySigNative, _VerifySigDart>( + 'enclave_verify_schnorr_signature'); + _freeStringFn = lib.lookupFunction<_FreeStringNative, _FreeStringDart>( + 'enclave_string_free'); +} + +// ── Public result types ────────────────────────────────────────────── + +/// Snapshot of the current attestation cache for UI display. +/// +/// Populated by [AttestedWalletApi] from its Dart-side cache after a +/// successful verify. No FFI involved — this is plain data. class AttestationStatus { final bool verified; final String pcr0; @@ -45,7 +59,7 @@ class AttestationStatus { final String attestationKey; final String? error; - AttestationStatus({ + const AttestationStatus({ required this.verified, required this.pcr0, required this.verifiedAtEpochSecs, @@ -54,16 +68,15 @@ class AttestationStatus { this.error, }); - factory AttestationStatus.fromJson(Map json) { - return AttestationStatus( - verified: json['verified'] as bool? ?? false, - pcr0: json['pcr0'] as String? ?? '', - verifiedAtEpochSecs: json['verified_at_epoch_secs'] as int? ?? 0, - ttlRemainingSecs: json['ttl_remaining_secs'] as int? ?? 0, - attestationKey: json['attestation_key'] as String? ?? '', - error: json['error'] as String?, - ); - } + /// Status when no attestation has been performed (or the cache is empty). + factory AttestationStatus.unverified({String? error}) => AttestationStatus( + verified: false, + pcr0: '', + verifiedAtEpochSecs: 0, + ttlRemainingSecs: 0, + attestationKey: '', + error: error, + ); Duration get ttlRemaining => Duration(seconds: ttlRemainingSecs); @@ -72,151 +85,116 @@ class AttestationStatus { : null; } -/// Response from an attested HTTP request. -class EnclaveResponse { - final int statusCode; - final String body; - final bool signatureVerified; +/// Result of verifying a CBOR-encoded COSE_Sign1 attestation document. +class AttestationVerifyResult { + final bool ok; + final String pcr0; + final Map pcrs; + + /// SHA-256 hex of the enclave's attestation pubkey, parsed out of the + /// document's UserData. Empty when the document predates the nitriding + /// key-binding format. + final String appKeyHash; final String? error; - EnclaveResponse({ - required this.statusCode, - required this.body, - required this.signatureVerified, + const AttestationVerifyResult({ + required this.ok, + required this.pcr0, + required this.pcrs, + required this.appKeyHash, this.error, }); - factory EnclaveResponse.fromJson(Map json) { - return EnclaveResponse( - statusCode: json['status_code'] as int? ?? 0, - body: json['body'] as String? ?? '', - signatureVerified: json['signature_verified'] as bool? ?? false, + factory AttestationVerifyResult.fromJson(Map json) { + return AttestationVerifyResult( + ok: json['ok'] as bool? ?? false, + pcr0: json['pcr0'] as String? ?? '', + pcrs: (json['pcrs'] as Map? ?? {}) + .map((k, v) => MapEntry(k, v as String)), + appKeyHash: json['app_key_hash'] as String? ?? '', error: json['error'] as String?, ); } - - bool get isOk => statusCode >= 200 && statusCode < 300 && error == null; } -/// Verified HTTP client for AWS Nitro Enclaves via Rust FFI. -/// -/// Every request verifies the enclave's attestation document (PCR0) -/// and checks the BIP-340 Schnorr response signature. -class NativeEnclaveClient { - late final Pointer _ptr; - late final _PostDart _post; - late final _GetDart _get; - late final _StatusDart _status; - late final _VerifyDart _verify; - late final _FreeClientDart _freeClient; - late final _FreeStringDart _freeString; - bool _disposed = false; - - /// Create a new enclave client. +// ── Verifier entry points (stateless, synchronous, CPU-bound) ──────── + +class EnclaveVerifier { + EnclaveVerifier._(); + + /// Verify a COSE_Sign1 AWS Nitro attestation document. + /// + /// [docBytes] — raw bytes of the attestation document (already + /// base64-decoded by the caller, JSON envelope unwrapped). + /// [expectedPcr0] — hex-encoded PCR0 to enforce. + /// [nonce] — 20-byte nonce the caller used when fetching the document. /// - /// [baseUrl] - HTTPS endpoint of the enclave (e.g. "https://1.2.3.4") - /// [expectedPcr0] - Expected PCR0 hex string (96 chars, SHA-384) - /// [cacheTtlSecs] - Attestation cache TTL in seconds (default: 60) - NativeEnclaveClient(String baseUrl, String expectedPcr0, - {int cacheTtlSecs = 60}) { - final lib = nativeLib; - - final newFn = lib.lookupFunction<_NewNative, _NewDart>('enclave_client_new'); - _post = lib.lookupFunction<_PostNative, _PostDart>('enclave_client_post'); - _get = lib.lookupFunction<_GetNative, _GetDart>('enclave_client_get'); - _status = - lib.lookupFunction<_StatusNative, _StatusDart>('enclave_client_attestation_status'); - _verify = - lib.lookupFunction<_VerifyNative, _VerifyDart>('enclave_client_verify'); - _freeClient = - lib.lookupFunction<_FreeClientNative, _FreeClientDart>('enclave_client_free'); - _freeString = - lib.lookupFunction<_FreeStringNative, _FreeStringDart>('enclave_string_free'); - - final urlPtr = baseUrl.toNativeUtf8(); - final pcrPtr = expectedPcr0.toNativeUtf8(); + /// Returns the verified attestation summary. Callers compare + /// `SHA256(attestation_pubkey_bytes)` to `appKeyHash` to bind the + /// fetched attestation pubkey to the document. + static AttestationVerifyResult verifyAttestationDoc({ + required Uint8List docBytes, + required String expectedPcr0, + required Uint8List nonce, + }) { + _ensureLoaded(); + + final docPtr = calloc(docBytes.length == 0 ? 1 : docBytes.length); + final noncePtr = calloc(nonce.length == 0 ? 1 : nonce.length); + final pcr0Ptr = expectedPcr0.toNativeUtf8(); try { - _ptr = newFn(urlPtr, pcrPtr, cacheTtlSecs); - if (_ptr == nullptr) { - throw StateError('Failed to create enclave client'); + docPtr.asTypedList(docBytes.length).setAll(0, docBytes); + noncePtr.asTypedList(nonce.length).setAll(0, nonce); + + final resultPtr = _verifyDocFn!( + docPtr, docBytes.length, pcr0Ptr, noncePtr, nonce.length); + try { + final json = jsonDecode(resultPtr.toDartString()) + as Map; + return AttestationVerifyResult.fromJson(json); + } finally { + _freeStringFn!(resultPtr); } } finally { - calloc.free(urlPtr); - calloc.free(pcrPtr); + calloc.free(docPtr); + calloc.free(noncePtr); + calloc.free(pcr0Ptr); } } - /// Make a verified POST request. - EnclaveResponse post(String path, String body) { - _checkDisposed(); - final pathPtr = path.toNativeUtf8(); - final bodyPtr = body.toNativeUtf8(); - Pointer resultPtr; + /// Verify a BIP-340 Schnorr signature over `SHA256(body)` using a + /// hex-encoded secp256k1 attestation pubkey (compressed or x-only). + /// + /// Returns `(ok, error?)`. Sub-millisecond — safe to call inline on + /// the main isolate per response. + static ({bool ok, String? error}) verifySchnorrSignature({ + required List body, + required String sigHex, + required String pubkeyHex, + }) { + _ensureLoaded(); + + final bodyPtr = calloc(body.isEmpty ? 1 : body.length); + final sigPtr = sigHex.toNativeUtf8(); + final pkPtr = pubkeyHex.toNativeUtf8(); try { - resultPtr = _post(_ptr, pathPtr, bodyPtr); + bodyPtr.asTypedList(body.length).setAll(0, body); + + final resultPtr = _verifySigFn!(bodyPtr, body.length, sigPtr, pkPtr); + try { + final json = jsonDecode(resultPtr.toDartString()) + as Map; + return ( + ok: json['ok'] as bool? ?? false, + error: json['error'] as String?, + ); + } finally { + _freeStringFn!(resultPtr); + } } finally { - calloc.free(pathPtr); calloc.free(bodyPtr); + calloc.free(sigPtr); + calloc.free(pkPtr); } - try { - final json = jsonDecode(resultPtr.toDartString()) as Map; - return EnclaveResponse.fromJson(json); - } finally { - _freeString(resultPtr); - } - } - - /// Make a verified GET request. - EnclaveResponse get(String path) { - _checkDisposed(); - final pathPtr = path.toNativeUtf8(); - Pointer resultPtr; - try { - resultPtr = _get(_ptr, pathPtr); - } finally { - calloc.free(pathPtr); - } - try { - final json = jsonDecode(resultPtr.toDartString()) as Map; - return EnclaveResponse.fromJson(json); - } finally { - _freeString(resultPtr); - } - } - - /// Get the current attestation status. - AttestationStatus get attestationStatus { - _checkDisposed(); - final resultPtr = _status(_ptr); - try { - final json = jsonDecode(resultPtr.toDartString()) as Map; - return AttestationStatus.fromJson(json); - } finally { - _freeString(resultPtr); - } - } - - /// Force re-verification of attestation and update the cache. - AttestationStatus verify() { - _checkDisposed(); - final resultPtr = _verify(_ptr); - try { - final json = jsonDecode(resultPtr.toDartString()) as Map; - return AttestationStatus.fromJson(json); - } finally { - _freeString(resultPtr); - } - } - - /// Dispose the client and free native resources. - void dispose() { - if (!_disposed) { - _freeClient(_ptr); - _disposed = true; - } - } - - void _checkDisposed() { - if (_disposed) throw StateError('EnclaveClient has been disposed'); } } diff --git a/app-core/pubspec.lock b/app-core/pubspec.lock index e04f5537..b680d617 100644 --- a/app-core/pubspec.lock +++ b/app-core/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c + sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" url: "https://pub.dev" source: hosted - version: "99.0.0" + version: "68.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.1.0" analyzer: dependency: transitive description: name: analyzer - sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" + sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "6.5.0" archive: dependency: transitive description: @@ -233,6 +238,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" lints: dependency: "direct dev" description: @@ -249,22 +262,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + url: "https://pub.dev" + source: hosted + version: "0.1.0-main.0" matcher: dependency: transitive description: name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.20" + version: "0.12.16+1" meta: dependency: transitive description: name: meta - sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.18.2" + version: "1.16.0" mime: dependency: transitive description: @@ -356,10 +377,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" source_map_stack_trace: dependency: transitive description: @@ -412,10 +433,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -428,42 +449,42 @@ packages: dependency: "direct dev" description: name: test - sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.31.1" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.5" typed_data: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.2" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -513,4 +534,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index c9d08ba8..2a984497 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -26,6 +26,14 @@ if (localPropertiesFile.exists()) { } } +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystorePropertiesFile.withReader("UTF-8") { reader -> + keystoreProperties.load(reader) + } +} + def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { flutterVersionCode = "1" @@ -57,11 +65,26 @@ android { versionName = flutterVersionName } + signingConfigs { + release { + if (keystoreProperties['storeFile']) { + keyAlias = keystoreProperties['keyAlias'] + keyPassword = keystoreProperties['keyPassword'] + storeFile = file(keystoreProperties['storeFile']) + storePassword = keystoreProperties['storePassword'] + } + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug + // Use the release keystore when key.properties is present (the + // upload-key setup). Fall back to debug for devs without it so + // `flutter run --release` still works locally. Debug-signed AABs + // cannot be uploaded to Play Console. + signingConfig = keystoreProperties['storeFile'] + ? signingConfigs.release + : signingConfigs.debug } } } diff --git a/app/android/app/google-services.json b/app/android/app/google-services.json new file mode 100644 index 00000000..dd8f8e21 --- /dev/null +++ b/app/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "575541915148", + "project_id": "vtxos-7afb3", + "storage_bucket": "vtxos-7afb3.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:575541915148:android:5fbaa581de7d4686378829", + "android_client_info": { + "package_name": "com.example.ap" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDr3wlKcGDo90lzqCctChmbrMwuwFemNh0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 95b4ae3b..c9e61838 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ do NOT require this permission, but anything user-visible does. --> + + + + diff --git a/app/devtools_options.yaml b/app/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/app/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/app/integration_test/app_test.dart b/app/integration_test/app_test.dart index 6ef58fb8..676a5cc0 100644 --- a/app/integration_test/app_test.dart +++ b/app/integration_test/app_test.dart @@ -404,8 +404,8 @@ void main() { await RestorePage.enterPassword(tester, password); await RestorePage.tapContinue(tester); - await pumpUntilFound(tester, find.byKey(const Key('serverConnectBtn'))); - await ServerConnectPage.useDefault(tester); + await pumpUntilFound(tester, find.byKey(const Key('serverPresetRegtest'))); + await ServerConnectPage.pickRegtest(tester); await tester.pumpAndSettle(); await DkgProgressPage.waitForReady(tester, timeout: const Duration(minutes: 2)); diff --git a/app/integration_test/test_helpers/flows.dart b/app/integration_test/test_helpers/flows.dart index 913a0094..65dc32ba 100644 --- a/app/integration_test/test_helpers/flows.dart +++ b/app/integration_test/test_helpers/flows.dart @@ -23,7 +23,7 @@ class Flows { await tester.pumpAndSettle(); await GoogleSignInPage.signIn(tester); await tester.pumpAndSettle(); - await ServerConnectPage.useDefault(tester); + await ServerConnectPage.pickRegtest(tester); // Don't pumpAndSettle here — the DKG screen has a CircularProgressIndicator // that never "settles" until DKG completes, so pumpAndSettle would block // (up to its 10-min cap). waitForReady polls via pump() instead, which is diff --git a/app/integration_test/test_helpers/page_objects.dart b/app/integration_test/test_helpers/page_objects.dart index bb55044a..df07793f 100644 --- a/app/integration_test/test_helpers/page_objects.dart +++ b/app/integration_test/test_helpers/page_objects.dart @@ -80,10 +80,10 @@ class GoogleSignInPage { } class ServerConnectPage { - static Future useDefault(WidgetTester tester) => - _tapKey(tester, 'serverConnectBtn'); - static Future setHost(WidgetTester tester, String host) => - _enterText(tester, 'serverUrlField', host); + static Future pickRegtest(WidgetTester tester) => + _tapKey(tester, 'serverPresetRegtest'); + static Future pickMutiny(WidgetTester tester) => + _tapKey(tester, 'serverPresetMutiny'); } class DkgProgressPage { diff --git a/app/integration_test/test_helpers/test_setup.dart b/app/integration_test/test_helpers/test_setup.dart index e8207a81..cac5213d 100644 --- a/app/integration_test/test_helpers/test_setup.dart +++ b/app/integration_test/test_helpers/test_setup.dart @@ -12,14 +12,6 @@ import 'package:provider/provider.dart'; import 'test_app.dart'; -class _AllowSelfSignedCerts extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context) - ..badCertificateCallback = (cert, host, port) => true; - } -} - /// Force-disposes the prior widget tree so any Provider-held MpcService /// releases its Hive boxes before we try to wipe them. Without this, the /// old MpcService keeps `_identityBox` open and Hive's box cache returns @@ -62,7 +54,6 @@ Future bootApp( WidgetTester tester, { required BackupStore backupStore, }) async { - HttpOverrides.global = _AllowSelfSignedCerts(); GoogleFonts.config.allowRuntimeFetching = false; // Suppress RenderFlex overflow warnings in tests. They show as yellow/black // stripes in production but fail integration_test runs because the binding diff --git a/app/lib/main.dart b/app/lib/main.dart index 6e0bb6b8..b5bdddf5 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'theme/app_theme.dart'; @@ -28,18 +27,8 @@ import 'package:provider/provider.dart'; import 'services/mpc_service.dart'; import 'services/push_service.dart'; -class _AllowSelfSignedCerts extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context) - ..badCertificateCallback = (cert, host, port) => true; - } -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Accept self-signed TLS certificates (enclave uses self-signed certs). - HttpOverrides.global = _AllowSelfSignedCerts(); // Best-effort push init. No-ops gracefully when Firebase config is missing // (e.g. CI builds without google-services.json). await PushService.initialize(); diff --git a/app/lib/screens/onboarding/server_connect_screen.dart b/app/lib/screens/onboarding/server_connect_screen.dart index 7667483f..d5912eea 100644 --- a/app/lib/screens/onboarding/server_connect_screen.dart +++ b/app/lib/screens/onboarding/server_connect_screen.dart @@ -1,9 +1,13 @@ +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import '../../services/mpc_service.dart'; +const String _regtestHost = '10.0.2.2'; +const String _mutinyHost = 'mutiny.vtxos.network'; + class ServerConnectionScreen extends StatefulWidget { const ServerConnectionScreen({super.key}); @@ -12,44 +16,30 @@ class ServerConnectionScreen extends StatefulWidget { } class _ServerConnectionScreenState extends State { - late final TextEditingController _urlController; - bool _isChecking = false; - - @override - void initState() { - super.initState(); - // Physical device with USB signer uses ADB reverse → 127.0.0.1 - _urlController = TextEditingController(text: '127.0.0.1'); - } - - void _connect() async { - final extras = GoRouterState.of(context).extra as Map? ?? {}; - - setState(() { - _isChecking = true; - }); + String? _selecting; - final host = _urlController.text.trim(); - if (host.isNotEmpty) { + Future _pick(String host) async { + if (_selecting != null) return; + final extras = + GoRouterState.of(context).extra as Map? ?? {}; + setState(() => _selecting = host); + try { await context.read().setHost(host); - } - - // Simulate network check (or in real app, we might check connectivity here) - await Future.delayed(const Duration(seconds: 1)); - - if (mounted) { - setState(() { - _isChecking = false; - }); - // Forward signerKind / password / isRestore to the DKG screen. + if (!mounted) return; context.push('/onboarding/dkg', extra: extras); + } catch (e) { + if (!mounted) return; + setState(() => _selecting = null); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Connection failed: $e')), + ); } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Connect to Server')), + appBar: AppBar(title: const Text('Choose a Server')), body: Padding( padding: const EdgeInsets.all(24.0), child: Column( @@ -62,35 +52,100 @@ class _ServerConnectionScreenState extends State { ), const SizedBox(height: 8), Text( - 'Enter the URL of the MPC co-signing server.', + 'Pick where the MPC co-signing server is running.', style: GoogleFonts.inter(color: Colors.white70), ), const SizedBox(height: 24), - TextField( - key: const Key('serverUrlField'), - controller: _urlController, - decoration: const InputDecoration( - labelText: 'Server Host / IP', - prefixIcon: Icon(Icons.dns), - hintText: 'e.g. 10.0.2.2 or 192.168.1.x', - ), - style: GoogleFonts.inter(color: Colors.white), - ), - const Spacer(), - ElevatedButton( - key: const Key('serverConnectBtn'), - onPressed: _isChecking ? null : _connect, - child: _isChecking - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Connect & Start DKG'), + _PresetCard( + keyName: 'serverPresetMutiny', + icon: Icons.shield_outlined, + title: 'Mutiny', + subtitle: 'Production enclave at mutiny.vtxos.network', + busy: _selecting == _mutinyHost, + disabled: _selecting != null && _selecting != _mutinyHost, + onTap: () => _pick(_mutinyHost), ), + if (kDebugMode) ...[ + const SizedBox(height: 16), + _PresetCard( + keyName: 'serverPresetRegtest', + icon: Icons.developer_board, + title: 'Regtest (local)', + subtitle: + 'Local cosigner-runtime on $_regtestHost — for development', + busy: _selecting == _regtestHost, + disabled: _selecting != null && _selecting != _regtestHost, + onTap: () => _pick(_regtestHost), + ), + ], ], ), ), ); } } + +class _PresetCard extends StatelessWidget { + const _PresetCard({ + required this.keyName, + required this.icon, + required this.title, + required this.subtitle, + required this.busy, + required this.disabled, + required this.onTap, + }); + + final String keyName; + final IconData icon; + final String title; + final String subtitle; + final bool busy; + final bool disabled; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Card( + key: Key(keyName), + child: InkWell( + onTap: disabled ? null : onTap, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(icon, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.inter( + fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: GoogleFonts.inter( + fontSize: 13, color: Colors.white70), + ), + ], + ), + ), + if (busy) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + const Icon(Icons.chevron_right), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/screens/spending/signing_screen.dart b/app/lib/screens/spending/signing_screen.dart index 371f1e32..d9cde464 100644 --- a/app/lib/screens/spending/signing_screen.dart +++ b/app/lib/screens/spending/signing_screen.dart @@ -268,7 +268,7 @@ class _SigningScreenState extends State { try { await wallet.sync(); } catch (e) { - print("Sync failed before sign: $e"); + debugPrint("Sync failed before sign: $e"); } final balance = await wallet.getBalance(); @@ -319,7 +319,7 @@ class _SigningScreenState extends State { policyId = resolvedPolicyId; } } catch (e) { - print("getPolicyId failed: $e — proceeding without policy"); + debugPrint("getPolicyId failed: $e — proceeding without policy"); } setState(() { diff --git a/app/lib/screens/splash_screen.dart b/app/lib/screens/splash_screen.dart index 0e4b0728..0d00d8bd 100644 --- a/app/lib/screens/splash_screen.dart +++ b/app/lib/screens/splash_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; @@ -48,18 +49,10 @@ class _SplashScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - width: 80, - height: 80, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.account_balance_wallet_outlined, - color: Colors.black, - size: 40, - ), + SvgPicture.asset( + 'assets/logo/mark-a-hat.svg', + width: 96, + height: 96, ), const SizedBox(height: 24), Text( diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 432da937..838c102f 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -58,7 +58,8 @@ dependencies: synchronized: ^3.1.0 # Push notifications: wake the app on receive so it can re-delegate. firebase_core: ^3.6.0 - firebase_messaging: ^15.1.3 + firebase_messaging: ^15.2.10 + flutter_svg: ^2.0.10 dev_dependencies: flutter_test: @@ -87,6 +88,7 @@ flutter: assets: - assets/google_fonts/ + - assets/logo/ # To add assets to your application, add an assets section, like this: # assets: diff --git a/cosigner-runtime/enclave/enclave.yaml b/cosigner-runtime/enclave/enclave.yaml index 7e5f85e7..4bd45701 100644 --- a/cosigner-runtime/enclave/enclave.yaml +++ b/cosigner-runtime/enclave/enclave.yaml @@ -9,11 +9,13 @@ prefix: dev # Deployment prefix (stack = {prefix}Nitro{Name instance_type: m6i.xlarge # EC2 instance type migration_cooldown: "1m" # Cooldown before migration proceeds previous_pcr0: "b1686b865467994579c99b6b3f61bf051184fed7eaa018a9b99c20415d1821590e862cce34db32bc4516bdafc9a7eb56" +is_kms_key_locked: false + runtime: - rev: "v0.0.74" - hash: "sha256-9cXFGEcexHZr1JEqU004Okxs55kQdvb7fm4221qgWMU=" - vendor_hash: "sha256-2ZX9x2voIwVkUsmrZvhJssAQqnCh8/HLwopL5HpiaOg=" + rev: "v0.0.79" + hash: "sha256-sIDpJVIuOzyl1NlmPA0qOks61ppsTtyc3pJYIq/4sWY=" + vendor_hash: "sha256-kkgp+uxc9pohZiCnz84nyXroUzsPhhtb0VgGa3dQgUo=" app: language: "rust" # App language: go, nodejs, dotnet, rust @@ -24,9 +26,9 @@ app: # Secrets are passed as environment variables. No SDK imports needed. nix_owner: "BitspendPayment" # GitHub owner (required) nix_repo: "MPCWallet" # GitHub repo name (required) - nix_rev: "7edf56bd2724404b1d7dc84c9219f76d7afb86ef" # Git commit SHA (required) - nix_hash: "sha256-xu5GDMZsomXm0mvnh+U1V+4NolOq6GoIZ/nzsEhrEaA=" # Nix source hash: nix-prefetch-url --unpack (required) - nix_vendor_hash: "sha256-FoCcAI8Fgj/GLbOWvmC42L+JbT6xNXkwokjVHAQk560=" # Go vendor hash (required) + nix_rev: "4ddc35ba459d57806994df559c0800f51e94f022" # Git commit SHA (required) + nix_hash: "sha256-TvT2/x+s0np+e65v0SxzHp+npQip20heLPCz/kRYc0k=" # Nix source hash: nix-prefetch-url --unpack (required) + nix_vendor_hash: "sha256-NNLxGetud2k0OXQAgbRc5QFL2fDWyVxFkt6GB6HqpZA=" # Go vendor hash (required) nix_sub_packages: # Go sub-packages to build - "." nix_subdir: "cosigner-runtime" # Subdirectory for monorepo @@ -39,6 +41,7 @@ app: # Environment variables baked into the EIF. # Template vars: {{region}}, {{prefix}}, {{version}} env: + ENCLAVE_NITRIDING_UPSTREAM: "h1" PORT: "7074" COSIGNER_WASM_PATH: "/app/cosigner.wasm" BITCOIN_NETWORK: "signet" @@ -46,7 +49,7 @@ app: ELECTRUM_PORT: "50001" ASP_URL: "https://mutinynet.arkade.sh" PERSISTENCE_BACKEND: "enclave" - SUPERVISOR_URL: "http://127.0.0.1:7073" + SUPERVISOR_URL: "http://127.0.0.1:8080" # Secrets managed by KMS inside the enclave. diff --git a/cosigner-runtime/src/config.rs b/cosigner-runtime/src/config.rs index 13424531..414c7b24 100644 --- a/cosigner-runtime/src/config.rs +++ b/cosigner-runtime/src/config.rs @@ -65,7 +65,7 @@ impl ServerConfig { persistence_backend: env::var("PERSISTENCE_BACKEND") .unwrap_or_else(|_| "sled".to_string()), supervisor_url: env::var("SUPERVISOR_URL") - .unwrap_or_else(|_| "http://127.0.0.1:7073".to_string()), + .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()), enclave_mgmt_token: env::var("ENCLAVE_RUNTIME_TOKEN").unwrap_or_default(), cosigner_wasm_path: env::var("COSIGNER_WASM_PATH").unwrap_or_else(|_| { "../cosigner/target/wasm32-wasip1/release/cosigner.wasm".to_string() diff --git a/cosigner-runtime/src/persistence/enclave_store.rs b/cosigner-runtime/src/persistence/enclave_store.rs index 748b6468..2e74fd66 100644 --- a/cosigner-runtime/src/persistence/enclave_store.rs +++ b/cosigner-runtime/src/persistence/enclave_store.rs @@ -4,7 +4,7 @@ use super::traits::{KvStore, PersistenceError, SecretStore}; /// Enclave supervisor-backed persistence store. /// -/// Talks to the enclave supervisor at `http://127.0.0.1:7073` (or configured URL). +/// Talks to the enclave supervisor at `http://127.0.0.1:8080` (or configured URL). /// All requests carry `Authorization: Bearer {mgmt_token}`. /// /// Storage API (`/v1/storage/`): diff --git a/cosigner-runtime/src/telemetry.rs b/cosigner-runtime/src/telemetry.rs index 16986ebe..74f25b5b 100644 --- a/cosigner-runtime/src/telemetry.rs +++ b/cosigner-runtime/src/telemetry.rs @@ -1,12 +1,13 @@ //! OpenTelemetry initialization for the MPC Wallet server. //! -//! When `ENCLAVE_MGMT_TOKEN` is set, wires three OTLP/HTTP protobuf exporters -//! targeting the enclave supervisor on 127.0.0.1:{ENCLAVE_PROXY_PORT|7073}: -//! - traces -> POST /v1/enclave-traces -//! - metrics -> POST /v1/enclave-metrics -//! - logs -> POST /v1/logs (supervisor ingest path) +//! When `ENCLAVE_RUNTIME_TOKEN` is set, wires three OTLP/HTTP protobuf exporters +//! targeting the enclave supervisor on 127.0.0.1:{ENCLAVE_PROXY_PORT|8080}: +//! - traces -> POST /v1/traces +//! - metrics -> POST /v1/metrics +//! - logs -> POST /v1/logs //! -//! When the token is empty, only a stderr `fmt` layer is installed. +//! All three paths are the OTLP/HTTP spec defaults. When the token is empty, +//! only a stderr `fmt` layer is installed. use std::time::Duration; @@ -87,7 +88,7 @@ pub fn init() -> TelemetryGuard { }; } - let proxy_port = std::env::var("ENCLAVE_PROXY_PORT").unwrap_or_else(|_| "7073".into()); + let proxy_port = std::env::var("ENCLAVE_PROXY_PORT").unwrap_or_else(|_| "8080".into()); let base = format!("http://127.0.0.1:{proxy_port}"); let mut auth_headers = std::collections::HashMap::new(); @@ -98,7 +99,7 @@ pub fn init() -> TelemetryGuard { let trace_exporter = SpanExporter::builder() .with_http() .with_protocol(Protocol::HttpBinary) - .with_endpoint(format!("{base}/v1/enclave-traces")) + .with_endpoint(format!("{base}/v1/traces")) .with_headers(auth_headers.clone()) .with_timeout(Duration::from_secs(10)) .build() @@ -113,7 +114,7 @@ pub fn init() -> TelemetryGuard { let metric_exporter = MetricExporter::builder() .with_http() .with_protocol(Protocol::HttpBinary) - .with_endpoint(format!("{base}/v1/enclave-metrics")) + .with_endpoint(format!("{base}/v1/metrics")) .with_headers(auth_headers.clone()) .with_timeout(Duration::from_secs(10)) .build() diff --git a/crates/enclave-client/Cargo.lock b/crates/enclave-client/Cargo.lock index 71f4b9c4..344cf56d 100644 --- a/crates/enclave-client/Cargo.lock +++ b/crates/enclave-client/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "base16ct" version = "0.2.0" @@ -26,12 +20,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - [[package]] name = "block-buffer" version = "0.10.4" @@ -41,40 +29,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "ciborium" version = "0.2.2" @@ -140,7 +100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -191,17 +151,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -231,7 +180,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -249,14 +198,11 @@ dependencies = [ "hex", "k256", "p384", - "rand", - "reqwest", "serde", "serde_json", "sha2", "spki", "thiserror", - "tokio", "x509-cert", ] @@ -266,64 +212,16 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - [[package]] name = "flagset" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - [[package]] name = "generic-array" version = "0.14.9" @@ -342,24 +240,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", ] [[package]] @@ -369,7 +251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -408,242 +290,12 @@ dependencies = [ "digest", ] -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "js-sys" -version = "0.3.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - [[package]] name = "k256" version = "0.13.4" @@ -664,41 +316,12 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -726,18 +349,6 @@ dependencies = [ "base64ct", ] -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "pkcs8" version = "0.10.2" @@ -748,24 +359,6 @@ dependencies = [ "spki", ] -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -785,295 +378,88 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", + "proc-macro2", ] [[package]] -name = "quinn-proto" -version = "0.11.14" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", + "getrandom", ] [[package]] -name = "quinn-udp" -version = "0.5.14" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", + "hmac", + "subtle", ] [[package]] -name = "quote" -version = "1.0.45" +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "proc-macro2", + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] [[package]] -name = "rand" -version = "0.9.2" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "rand_chacha", - "rand_core 0.9.5", + "serde_derive", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] @@ -1087,12 +473,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signature" version = "2.2.0" @@ -1100,29 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", + "rand_core", ] [[package]] @@ -1135,12 +493,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "subtle" version = "2.6.1" @@ -1158,26 +510,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -1198,31 +530,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tls_codec" version = "0.4.2" @@ -1244,100 +551,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio" -version = "1.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typenum" version = "1.19.0" @@ -1350,318 +563,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - [[package]] name = "x509-cert" version = "0.2.5" @@ -1674,29 +587,6 @@ dependencies = [ "tls_codec", ] -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.48" @@ -1717,27 +607,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.2" @@ -1758,39 +627,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/crates/enclave-client/Cargo.toml b/crates/enclave-client/Cargo.toml index 3cfe09ed..4d2f162e 100644 --- a/crates/enclave-client/Cargo.toml +++ b/crates/enclave-client/Cargo.toml @@ -2,11 +2,9 @@ name = "enclave-client" version = "0.1.0" edition = "2021" -description = "Verified HTTP client for AWS Nitro Enclave attestation (forked for FFI)" +description = "Pure-verification library for AWS Nitro Enclave attestation (COSE_Sign1 / X.509 / NIST P-384 / BIP-340 Schnorr). Callers own HTTP." [dependencies] -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } -tokio = { version = "1", features = ["sync"] } serde = { version = "1", features = ["derive"] } serde_json = "1" ciborium = "0.2" @@ -14,7 +12,6 @@ coset = "0.3" sha2 = "0.10" hex = "0.4" base64 = "0.22" -rand = "0.9" thiserror = "2" k256 = { version = "0.13", features = ["schnorr"] } p384 = { version = "0.13", features = ["ecdsa"] } @@ -22,3 +19,9 @@ x509-cert = { version = "0.2", features = ["std"] } der = { version = "0.7", features = ["std"] } ecdsa = { version = "0.16", features = ["verifying"] } spki = "0.7" + +[features] +# Enable tests that need a captured AWS Nitro attestation document at +# tests/fixtures/attestation.b64 (not committed). Capture from a running +# enclave with `curl https://.../enclave/attestation?nonce=deadbeef...`. +fixture-tests = [] diff --git a/crates/enclave-client/src/client.rs b/crates/enclave-client/src/client.rs deleted file mode 100644 index 041ae2a9..00000000 --- a/crates/enclave-client/src/client.rs +++ /dev/null @@ -1,276 +0,0 @@ -//! Main verified HTTP client for AWS Nitro Enclaves. -//! -//! Forked from ArkLabsHQ/introspector-enclave client-rs with FFI-friendly -//! additions: synchronous attestation_status(), eager init(), std::sync::RwLock. - -use crate::error::{Error, Result}; -use crate::manifest; -use crate::types::{AttestationResult, AttestationStatus, Options, Response, now_epoch_secs}; -use crate::verify; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, Instant}; - -/// A verified HTTP client for an AWS Nitro Enclave. -/// -/// Every request first verifies the enclave's attestation document (PCR0, -/// optional secret PCRs, attestation key binding) and then verifies the -/// Schnorr response signature. -pub struct Client { - base_url: String, - http_client: reqwest::Client, - opts: Options, - cached_state: Arc>>, -} - -impl Client { - /// Create a new enclave client without verifying attestation. - /// Call `init()` instead for eager verification + connection warmup. - pub fn new(base_url: &str, opts: Options) -> Result { - if opts.expected_pcr0.is_empty() { - return Err(Error::MissingPCR0); - } - - let mut builder = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .use_rustls_tls(); - - if opts.insecure_tls { - builder = builder.danger_accept_invalid_certs(true); - } - - let http_client = builder.build()?; - - Ok(Self { - base_url: base_url.trim_end_matches('/').to_string(), - http_client, - opts, - cached_state: Arc::new(RwLock::new(None)), - }) - } - - /// Create and initialize: verify attestation + warm connection pool. - /// Recommended for FFI use -- the client is ready for requests immediately. - pub async fn init(base_url: &str, opts: Options) -> Result { - let client = Self::new(base_url, opts)?; - - // Step 1: Verify attestation (populates cache). - client.verify_inner().await?; - - // Step 2: Warm TLS connection pool with a lightweight request. - let _ = client.http_client - .get(format!("{}/api/health", client.base_url)) - .send() - .await; - - Ok(client) - } - - /// Create a client from a GitHub Release deployment manifest. - pub async fn from_manifest(manifest_url: &str, mut opts: Options) -> Result { - let (m, raw) = manifest::fetch_manifest_raw(manifest_url).await?; - - if opts.verify_provenance { - if m.repo.is_empty() { - return Err(Error::Manifest( - "missing repo field, cannot verify provenance".into(), - )); - } - manifest::verify_manifest_provenance(&m.repo, &raw, opts.github_token.as_deref()) - .await?; - } - - if !opts.expected_pcr0.is_empty() - && !opts.expected_pcr0.eq_ignore_ascii_case(&m.pcr0) - { - return Err(Error::Manifest(format!( - "manifest PCR0 {} does not match expected {}", - m.pcr0, opts.expected_pcr0 - ))); - } - opts.expected_pcr0 = m.pcr0; - - Self::new(&m.base_url, opts) - } - - /// Read cached attestation status without triggering network verification. - /// Returns `AttestationStatus::unverified()` if cache is empty or lock contended. - pub fn attestation_status(&self) -> AttestationStatus { - match self.cached_state.try_read() { - Ok(guard) => match &*guard { - Some(state) if state.verified => { - let elapsed = state.verified_at.elapsed(); - let remaining = self.opts.cache_ttl.as_secs() as i64 - - elapsed.as_secs() as i64; - - AttestationStatus { - // Attestation was verified. The TTL only controls when - // the Rust client re-verifies on the next request -- - // it doesn't invalidate the previous verification. - verified: true, - pcr0: state.pcr0.clone(), - attestation_key: state.attestation_key.clone(), - verified_at_epoch_secs: state.verified_at_epoch, - ttl_remaining_secs: remaining.max(0), - error: None, - } - } - _ => AttestationStatus::unverified(), - }, - Err(_) => AttestationStatus::unverified(), - } - } - - /// Make a verified GET request to the enclave. - pub async fn get(&self, path: &str) -> Result { - let url = self.url(path); - let req = self.http_client.get(&url); - self.execute(req).await - } - - /// Make a verified POST request to the enclave with a body. - pub async fn post(&self, path: &str, body: impl Into) -> Result { - let url = self.url(path); - let req = self - .http_client - .post(&url) - .header("Content-Type", "application/json") - .body(body); - self.execute(req).await - } - - /// Make a verified HTTP request to the enclave (generic). - pub async fn request(&self, req: reqwest::RequestBuilder) -> Result { - self.execute(req).await - } - - /// Manually trigger attestation verification, bypassing the cache. - pub async fn verify_attestation(&self) -> Result { - self.verify_inner().await - } - - /// Execute a request with attestation verification and signature checking. - async fn execute(&self, req: reqwest::RequestBuilder) -> Result { - // Step 1: Verify attestation (uses cache if valid). - let attest = self.ensure_verified().await?; - - // Step 2: Execute the actual request. - // Retry once on transient connection errors. - let resp = match req.try_clone() { - Some(retry_req) => match req.send().await { - Ok(r) => r, - Err(first_err) if first_err.is_connect() || first_err.is_request() => { - retry_req.send().await? - } - Err(e) => return Err(e.into()), - }, - None => req.send().await?, - }; - let status = resp.status().as_u16(); - let headers = resp.headers().clone(); - let body = resp.bytes().await?.to_vec(); - - // Step 3: Verify response signature. - let mut sig_verified = false; - if !attest.attestation_key.is_empty() { - if let Some(sig_val) = headers.get("X-Attestation-Signature") { - if let Ok(sig_hex) = sig_val.to_str() { - if verify::verify_schnorr_signature(&body, sig_hex, &attest.attestation_key) - .is_ok() - { - sig_verified = true; - } - } - } - } - - Ok(Response { - status_code: status, - headers, - body, - signature_verified: sig_verified, - }) - } - - /// Return cached attestation or re-verify. - async fn ensure_verified(&self) -> Result { - { - let cached = self.cached_state.read() - .unwrap_or_else(|e| e.into_inner()); - if let Some(ref state) = *cached { - if state.verified && state.verified_at.elapsed() < self.opts.cache_ttl { - return Ok(state.clone()); - } - } - } - self.verify_inner().await - } - - /// Perform full attestation verification and cache the result. - async fn verify_inner(&self) -> Result { - // 1. Fetch and verify attestation document. - let nitro_result = verify::fetch_and_verify_attestation( - &self.http_client, - &self.base_url, - &self.opts.expected_pcr0, - ) - .await?; - - // 2. Verify additional PCRs (secret pubkey hashes). - let mut pcrs = HashMap::new(); - for (idx, bytes) in &nitro_result.document.pcrs { - if !bytes.is_empty() { - pcrs.insert(*idx, hex::encode(bytes)); - } - } - - for (i, expected_hash) in self.opts.expected_pcrs.iter().enumerate() { - let pcr_index = 16 + i as u32; - let actual = pcrs - .get(&pcr_index) - .ok_or(Error::PcrNotFound(pcr_index))?; - if !actual.eq_ignore_ascii_case(expected_hash) { - return Err(Error::PcrMismatch { - index: pcr_index, - expected: expected_hash.clone(), - actual: actual.clone(), - }); - } - } - - // 3. Verify attestation key binding. - let attest_key = verify::verify_key_binding( - &self.http_client, - &self.base_url, - &nitro_result.document, - ) - .await?; - - let result = AttestationResult { - pcr0: self.opts.expected_pcr0.clone(), - pcrs, - attestation_key: attest_key, - verified: true, - verified_at: Instant::now(), - verified_at_epoch: now_epoch_secs(), - }; - - // Cache the result. - { - let mut cached = self.cached_state.write() - .unwrap_or_else(|e| e.into_inner()); - *cached = Some(result.clone()); - } - - Ok(result) - } - - fn url(&self, path: &str) -> String { - let path = if path.starts_with('/') { - path.to_string() - } else { - format!("/{path}") - }; - format!("{}{}", self.base_url, path) - } -} diff --git a/crates/enclave-client/src/error.rs b/crates/enclave-client/src/error.rs index c1427582..dde13357 100644 --- a/crates/enclave-client/src/error.rs +++ b/crates/enclave-client/src/error.rs @@ -33,18 +33,6 @@ pub enum Error { #[error("signature verification failed")] SignatureVerification, - #[error("manifest error: {0}")] - Manifest(String), - - #[error("provenance verification failed: {0}")] - Provenance(String), - - #[error("HTTP error: {0}")] - Http(#[from] reqwest::Error), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), - #[error("CBOR error: {0}")] Cbor(String), diff --git a/crates/enclave-client/src/lib.rs b/crates/enclave-client/src/lib.rs index cc3529ab..db8316e7 100644 --- a/crates/enclave-client/src/lib.rs +++ b/crates/enclave-client/src/lib.rs @@ -1,44 +1,34 @@ -//! Verified HTTP client for AWS Nitro Enclaves. +//! Pure-verification library for AWS Nitro Enclave attestation. //! -//! Every request first verifies the enclave's attestation document (PCR0, -//! optional secret PCRs, attestation key binding) and then verifies the -//! Schnorr response signature. This ensures the enclave is running the -//! expected code and that responses haven't been tampered with. +//! No HTTP, no async — callers (Dart, integration tests, other Rust +//! crates) fetch attestation documents and response signatures +//! themselves and pass bytes in. //! -//! # Usage +//! # Example //! //! ```no_run -//! use enclave_client::{Client, Options}; +//! use enclave_client::{verify_attestation_doc, verify_schnorr_signature}; //! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! // Option A: Manual configuration. -//! let client = Client::new("https://1.2.3.4", Options { -//! expected_pcr0: "79f5fb125b00ad80...".into(), -//! ..Default::default() -//! })?; +//! # fn main() -> Result<(), Box> { +//! let doc_bytes: &[u8] = &[/* base64-decoded attestation doc */]; +//! let pcr0 = "79f5fb125b00ad80..."; +//! let nonce = [0u8; 20]; //! -//! let resp = client.get("/my-endpoint").await?; -//! println!("status: {}, verified: {}", resp.status_code, resp.signature_verified); -//! -//! // Option B: From deployment manifest (GitHub Releases). -//! let client = Client::from_manifest( -//! &enclave_client::manifest_url("myorg/my-app", "latest"), -//! Options::default(), -//! ).await?; -//! -//! Ok(()) -//! } +//! let result = verify_attestation_doc(doc_bytes, pcr0, &nonce)?; +//! let app_key_hash_hex = +//! enclave_client::extract_app_key_hash(&result.document)?; +//! // …caller fetches /v1/enclave-info to get the attestation pubkey, +//! // compares SHA256(pubkey) to app_key_hash, then per response: +//! let body: &[u8] = b"{\"status\":\"ready\"}"; +//! verify_schnorr_signature(body, "sig_hex…", "pubkey_hex…")?; +//! # Ok(()) +//! # } //! ``` -mod client; mod error; -mod manifest; mod nitro; -mod types; mod verify; -pub use client::Client; pub use error::{Error, Result}; -pub use manifest::{fetch_manifest, manifest_url, verify_manifest_provenance}; -pub use types::{AttestationResult, AttestationStatus, Manifest, Options, Response}; +pub use nitro::{AttestationDocument, NitroVerifyResult}; +pub use verify::{extract_app_key_hash, verify_attestation_doc, verify_schnorr_signature}; diff --git a/crates/enclave-client/src/manifest.rs b/crates/enclave-client/src/manifest.rs deleted file mode 100644 index ceb79b9d..00000000 --- a/crates/enclave-client/src/manifest.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Manifest fetching and GitHub provenance verification. - -use crate::error::{Error, Result}; -use crate::types::Manifest; -use sha2::{Digest, Sha256}; - -/// Construct the GitHub Releases URL for a deployment manifest. -/// -/// ``` -/// use enclave_client::manifest_url; -/// let url = manifest_url("myorg/my-app", "latest"); -/// assert_eq!(url, "https://github.com/myorg/my-app/releases/download/latest/deployment.json"); -/// ``` -pub fn manifest_url(repo: &str, tag: &str) -> String { - format!("https://github.com/{repo}/releases/download/{tag}/deployment.json") -} - -/// Fetch and parse a deployment manifest from the given URL. -pub async fn fetch_manifest(url: &str) -> Result { - let (manifest, _raw) = fetch_manifest_raw(url).await?; - Ok(manifest) -} - -/// Fetch manifest returning both parsed struct and raw bytes. -pub(crate) async fn fetch_manifest_raw(url: &str) -> Result<(Manifest, Vec)> { - let resp = reqwest::get(url).await?; - let status = resp.status(); - - if status != reqwest::StatusCode::OK { - let body = resp.text().await.unwrap_or_default(); - return Err(Error::Manifest(format!( - "status {status}: {}", - body.trim() - ))); - } - - let raw = resp.bytes().await?.to_vec(); - - let manifest: Manifest = - serde_json::from_slice(&raw).map_err(|e| Error::Manifest(format!("decode: {e}")))?; - - if manifest.base_url.is_empty() { - return Err(Error::Manifest("missing base_url".into())); - } - if manifest.pcr0.is_empty() { - return Err(Error::Manifest("missing pcr0".into())); - } - - Ok((manifest, raw)) -} - -/// Verify manifest provenance via the GitHub Attestations API. -/// -/// Checks that the manifest has a valid build provenance attestation from -/// GitHub Actions, proving it was produced by CI — not manually uploaded. -/// For public repos, token can be None. -pub async fn verify_manifest_provenance( - repo: &str, - manifest_bytes: &[u8], - token: Option<&str>, -) -> Result<()> { - let digest = Sha256::digest(manifest_bytes); - let digest_str = format!("sha256:{}", hex::encode(digest)); - - let api_url = format!( - "https://api.github.com/repos/{repo}/attestations/{digest_str}" - ); - - let client = reqwest::Client::new(); - let mut req = client - .get(&api_url) - .header("Accept", "application/json") - .header("User-Agent", "introspector-enclave-client"); - - if let Some(t) = token { - req = req.header("Authorization", format!("Bearer {t}")); - } - - let resp = req.send().await?; - - let status = resp.status(); - - if status == reqwest::StatusCode::NOT_FOUND { - return Err(Error::Provenance(format!( - "no attestations found for manifest (repo: {repo})" - ))); - } - - if status != reqwest::StatusCode::OK { - let body = resp.text().await.unwrap_or_default(); - return Err(Error::Provenance(format!( - "attestations API status {status}: {}", - body.trim() - ))); - } - - #[derive(serde::Deserialize)] - struct AttestationResponse { - attestations: Vec, - } - - let body = resp.bytes().await?; - let result: AttestationResponse = serde_json::from_slice(&body) - .map_err(|e| Error::Provenance(format!("decode attestations: {e}")))?; - - if result.attestations.is_empty() { - return Err(Error::Provenance(format!( - "no attestations found for manifest (repo: {repo})" - ))); - } - - Ok(()) -} diff --git a/crates/enclave-client/src/nitro.rs b/crates/enclave-client/src/nitro.rs index 26d610d3..fcd42f34 100644 --- a/crates/enclave-client/src/nitro.rs +++ b/crates/enclave-client/src/nitro.rs @@ -29,7 +29,7 @@ IwLz3/Y=\n\ /// Attestation document fields from the CBOR payload. #[derive(Debug)] -pub(crate) struct AttestationDocument { +pub struct AttestationDocument { pub module_id: String, pub timestamp: u64, pub digest: String, @@ -43,7 +43,7 @@ pub(crate) struct AttestationDocument { /// Result of verifying an attestation document. #[allow(dead_code)] -pub(crate) struct NitroVerifyResult { +pub struct NitroVerifyResult { pub document: AttestationDocument, pub signature_ok: bool, } @@ -438,6 +438,11 @@ mod tests { assert!(key.is_ok()); } + // Gated on the `fixture-tests` feature: needs a captured AWS Nitro + // attestation document at tests/fixtures/attestation.b64 which we don't + // commit. Enable with `cargo test --features fixture-tests` after + // capturing one from a running enclave. + #[cfg(feature = "fixture-tests")] #[test] fn test_verify_real_attestation_document() { // Real attestation document captured from a running AWS Nitro Enclave. @@ -478,6 +483,7 @@ mod tests { assert!(result.signature_ok); } + #[cfg(feature = "fixture-tests")] #[test] fn test_verify_attestation_rejects_tampered_document() { let b64 = include_str!("../tests/fixtures/attestation.b64"); diff --git a/crates/enclave-client/src/types.rs b/crates/enclave-client/src/types.rs deleted file mode 100644 index 829ac4e6..00000000 --- a/crates/enclave-client/src/types.rs +++ /dev/null @@ -1,128 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - -/// Configuration for the enclave client. -#[derive(Debug, Clone)] -pub struct Options { - /// Hex-encoded PCR0 value to verify against. - pub expected_pcr0: String, - - /// Hex-encoded SHA256 hashes of secret compressed public keys. - /// Index 0 maps to PCR16, index 1 to PCR17, etc. - pub expected_pcrs: Vec, - - /// How long a verified attestation is cached. Default: 60s. - pub cache_ttl: Duration, - - /// Skip TLS certificate verification. Default: true. - pub insecure_tls: bool, - - /// Enable GitHub artifact attestation verification for the deployment manifest. - pub verify_provenance: bool, - - /// Optional GitHub API token for verifying attestations on private repositories. - pub github_token: Option, -} - -impl Default for Options { - fn default() -> Self { - Self { - expected_pcr0: String::new(), - expected_pcrs: Vec::new(), - cache_ttl: Duration::from_secs(60), - insecure_tls: true, - verify_provenance: false, - github_token: None, - } - } -} - -/// Wraps an HTTP response with attestation verification metadata. -#[derive(Debug, Clone)] -pub struct Response { - pub status_code: u16, - pub headers: reqwest::header::HeaderMap, - pub body: Vec, - pub signature_verified: bool, -} - -/// Contains the verified attestation state (internal cache). -#[derive(Debug, Clone)] -pub struct AttestationResult { - pub pcr0: String, - pub pcrs: HashMap, - /// Hex-encoded compressed secp256k1 pubkey. - pub attestation_key: String, - pub verified: bool, - /// Monotonic timestamp for TTL calculation. - pub verified_at: Instant, - /// Unix epoch seconds for serialization across FFI. - pub verified_at_epoch: u64, -} - -/// FFI-friendly attestation status. Serializable, no Instant. -/// Returned by `Client::attestation_status()` without network calls. -#[derive(Debug, Clone, Serialize)] -pub struct AttestationStatus { - pub verified: bool, - pub pcr0: String, - pub attestation_key: String, - pub verified_at_epoch_secs: u64, - pub ttl_remaining_secs: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl AttestationStatus { - /// Status when no attestation has been performed yet. - pub fn unverified() -> Self { - Self { - verified: false, - pcr0: String::new(), - attestation_key: String::new(), - verified_at_epoch_secs: 0, - ttl_remaining_secs: 0, - error: None, - } - } -} - -/// Helper to get current Unix epoch seconds. -pub(crate) fn now_epoch_secs() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} - -/// Deployment metadata published as a GitHub Release asset (deployment.json). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest { - pub base_url: String, - pub pcr0: String, - #[serde(default)] - pub pcr1: String, - #[serde(default)] - pub pcr2: String, - #[serde(default)] - pub timestamp: String, - #[serde(default)] - pub commit: String, - #[serde(default)] - pub repo: String, -} - -/// JSON structure returned by /v1/enclave-info. -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct EnclaveInfoResponse { - #[serde(default)] - pub version: String, - #[serde(default)] - pub previous_pcr0: String, - #[serde(default)] - pub attestation_pubkey: String, - #[serde(default)] - pub error: String, -} diff --git a/crates/enclave-client/src/verify.rs b/crates/enclave-client/src/verify.rs index aa9c7a70..78971269 100644 --- a/crates/enclave-client/src/verify.rs +++ b/crates/enclave-client/src/verify.rs @@ -1,90 +1,43 @@ -//! Attestation verification, key binding, and Schnorr signature verification. +//! Pure attestation + signature verifiers. +//! +//! No HTTP, no async, no global state — callers fetch documents and +//! pass bytes in. The Rust side handles only the crypto-heavy work +//! (COSE_Sign1, X.509 chain, NIST P-384, BIP-340 Schnorr). use crate::error::{Error, Result}; use crate::nitro::{self, AttestationDocument, NitroVerifyResult}; -use crate::types::EnclaveInfoResponse; -use base64::Engine; use sha2::{Digest, Sha256}; -/// Fetch the attestation document from the enclave, verify it against the -/// AWS Nitro root certificate chain, check the nonce, and validate PCR0. -pub(crate) async fn fetch_and_verify_attestation( - http_client: &reqwest::Client, - base_url: &str, +/// Verify a CBOR-encoded COSE_Sign1 attestation document against the +/// expected PCR0 and a freshly-generated nonce. +/// +/// The caller is responsible for fetching the document +/// (`GET /enclave/attestation?nonce=`) and unwrapping any JSON +/// envelope (`{"document": ""}`) into raw bytes before calling. +pub fn verify_attestation_doc( + doc_bytes: &[u8], expected_pcr0: &str, + expected_nonce: &[u8], ) -> Result { - // Generate a random nonce to prevent replay attacks. - let nonce: [u8; 20] = rand::random(); - let nonce_hex = hex::encode(nonce); - - let url = format!( - "{}/enclave/attestation?nonce={nonce_hex}", - base_url.trim_end_matches('/') - ); - - let resp = http_client.get(&url).send().await?; - let status = resp.status(); - - if status != reqwest::StatusCode::OK { - let body = resp.text().await.unwrap_or_default(); - return Err(Error::AttestationVerification(format!( - "status {status}: {}", - body.trim() - ))); - } - - let payload = resp.text().await?; - let payload = payload.trim(); - - // The attestation may be returned as raw base64 or as JSON {"document":"..."}. - let doc_b64 = if payload.starts_with('{') { - #[derive(serde::Deserialize)] - struct DocWrapper { - #[serde(default)] - document: String, - } - if let Ok(parsed) = serde_json::from_str::(payload) { - if !parsed.document.is_empty() { - parsed.document - } else { - payload.to_string() - } - } else { - payload.to_string() - } - } else { - payload.to_string() - }; - - let doc_bytes = base64::engine::general_purpose::STANDARD - .decode(&doc_b64) - .map_err(|e| Error::AttestationVerification(format!("decode attestation: {e}")))?; + let result = nitro::verify_attestation_document(doc_bytes)?; - // Verify the COSE Sign1 document against the AWS Nitro root certs. - let result = nitro::verify_attestation_document(&doc_bytes)?; - - // Verify the nonce to confirm freshness. let doc_nonce = result .document .nonce .as_ref() .ok_or(Error::MissingNonce)?; - - if doc_nonce.len() == 0 { + if doc_nonce.is_empty() { return Err(Error::MissingNonce); } - - if *doc_nonce != nonce { + if doc_nonce.as_slice() != expected_nonce { return Err(Error::NonceMismatch); } - // Verify PCR0 matches the expected enclave build measurement. let pcr0 = result .document .pcrs .get(&0) .ok_or(Error::MissingPCR0InDocument)?; - if !hex::encode(pcr0).eq_ignore_ascii_case(expected_pcr0) { return Err(Error::PcrMismatch { index: 0, @@ -96,105 +49,66 @@ pub(crate) async fn fetch_and_verify_attestation( Ok(result) } -/// Verify the enclave's ephemeral attestation key by checking that the -/// pubkey from /v1/enclave-info matches the appKeyHash in the attestation -/// document's UserData. -/// -/// UserData format (nitriding v1.4.2): +/// Extract the `appKeyHash` from the attestation document's UserData, +/// validating the layout introduced by nitriding v1.4.2: /// -/// "sha256:" ++ tlsKeyHash(32) ++ ";" ++ "sha256:" ++ appKeyHash(32) +/// ```text +/// "sha256:" ++ tlsKeyHash(32) ++ ";" ++ "sha256:" ++ appKeyHash(32) +/// ``` /// -/// Total 79 bytes. appKeyHash is at bytes 47:79. -pub(crate) async fn verify_key_binding( - http_client: &reqwest::Client, - base_url: &str, - document: &AttestationDocument, -) -> Result { +/// Returns the 32-byte appKeyHash as a hex string. Callers fetch the +/// enclave's attestation pubkey separately (`GET /v1/enclave-info`) +/// and compare SHA256(pubkey_bytes) to this value — that comparison +/// is a single line of Dart and not worth a second FFI call. +pub fn extract_app_key_hash(document: &AttestationDocument) -> Result { const HASH_PREFIX: &[u8] = b"sha256:"; const HASH_SEP: &[u8] = b";"; - const TLS_START: usize = 0; - const TLS_HASH_START: usize = HASH_PREFIX.len(); // 7 - const TLS_HASH_END: usize = TLS_HASH_START + 32; // 39 - const SEP_START: usize = TLS_HASH_END; // 39 - const APP_PREFIX_START: usize = SEP_START + HASH_SEP.len(); // 40 - const APP_HASH_START: usize = APP_PREFIX_START + HASH_PREFIX.len(); // 47 - const APP_HASH_END: usize = APP_HASH_START + 32; // 79 + const TLS_HASH_END: usize = HASH_PREFIX.len() + 32; + const APP_PREFIX_START: usize = TLS_HASH_END + HASH_SEP.len(); + const APP_HASH_START: usize = APP_PREFIX_START + HASH_PREFIX.len(); + const APP_HASH_END: usize = APP_HASH_START + 32; let user_data = match &document.user_data { Some(ud) if ud.len() >= APP_HASH_END => ud, - _ => return Ok(String::new()), // UserData too short, key binding not supported + _ => { + return Err(Error::KeyBinding( + "UserData too short for sha256:tls;sha256:app layout".into(), + )) + } }; - if &user_data[TLS_START..TLS_HASH_START] != HASH_PREFIX { - return Err(Error::KeyBinding(format!( - "UserData missing {:?} prefix at offset 0 (got {:?})", - std::str::from_utf8(HASH_PREFIX).unwrap(), - String::from_utf8_lossy(&user_data[TLS_START..TLS_HASH_START]) - ))); + if &user_data[0..HASH_PREFIX.len()] != HASH_PREFIX { + return Err(Error::KeyBinding("UserData missing leading 'sha256:'".into())); } - if &user_data[SEP_START..APP_PREFIX_START] != HASH_SEP { - return Err(Error::KeyBinding(format!( - "UserData missing {:?} separator at offset {} (got {:?})", - std::str::from_utf8(HASH_SEP).unwrap(), - SEP_START, - String::from_utf8_lossy(&user_data[SEP_START..APP_PREFIX_START]) - ))); + if &user_data[TLS_HASH_END..APP_PREFIX_START] != HASH_SEP { + return Err(Error::KeyBinding("UserData missing ';' separator".into())); } if &user_data[APP_PREFIX_START..APP_HASH_START] != HASH_PREFIX { - return Err(Error::KeyBinding(format!( - "UserData missing {:?} prefix at offset {} (got {:?})", - std::str::from_utf8(HASH_PREFIX).unwrap(), - APP_PREFIX_START, - String::from_utf8_lossy(&user_data[APP_PREFIX_START..APP_HASH_START]) - ))); + return Err(Error::KeyBinding("UserData missing second 'sha256:'".into())); } let app_key_hash = &user_data[APP_HASH_START..APP_HASH_END]; - - // Check if appKeyHash is all zeros (key not yet registered). if app_key_hash.iter().all(|&b| b == 0) { return Err(Error::KeyBinding( "attestation key not yet registered (appKeyHash is all zeros)".into(), )); } - // Fetch the attestation pubkey from the enclave. - let info = fetch_enclave_info(http_client, base_url).await?; - - if info.attestation_pubkey.is_empty() { - return Err(Error::KeyBinding( - "enclave reports no attestation pubkey but appKeyHash is set".into(), - )); - } - - let attest_pubkey_bytes = hex::decode(&info.attestation_pubkey)?; - - // Verify that SHA256(pubkey) matches the appKeyHash from attestation. - let expected_hash = Sha256::digest(&attest_pubkey_bytes); - - if &expected_hash[..] != app_key_hash { - return Err(Error::KeyBinding(format!( - "appKeyHash mismatch: expected SHA256({}) = {}, got {}", - info.attestation_pubkey, - hex::encode(expected_hash), - hex::encode(app_key_hash) - ))); - } - - Ok(info.attestation_pubkey) + Ok(hex::encode(app_key_hash)) } -/// Verify a BIP-340 Schnorr signature over SHA256(body) using the -/// hex-encoded compressed secp256k1 pubkey. -pub(crate) fn verify_schnorr_signature( +/// Verify a BIP-340 Schnorr signature over `SHA256(body)` using the +/// hex-encoded compressed or x-only secp256k1 pubkey. +/// +/// The server (`introspector-enclave`) signs `SHA256(response_body)` +/// using btcec's BIP-340 implementation, which treats the 32-byte hash +/// as the raw message — hence `verify_raw` here. +pub fn verify_schnorr_signature( body: &[u8], sig_hex: &str, attest_pubkey_hex: &str, ) -> Result<()> { let mut pubkey_bytes = hex::decode(attest_pubkey_hex)?; - - // The attestation pubkey is compressed (33 bytes). Extract x-only (32 bytes) - // by dropping the prefix byte for Schnorr verification. if pubkey_bytes.len() == 33 { pubkey_bytes = pubkey_bytes[1..].to_vec(); } @@ -208,9 +122,6 @@ pub(crate) fn verify_schnorr_signature( let msg_hash = Sha256::digest(body); - // The Go btcec library passes the SHA256 hash as the raw 32-byte message - // to BIP-340 Schnorr verify. Use verify_raw which treats the input as - // the pre-hashed message (BIP-340 then applies tagged_hash("BIP0340/challenge", ...)). verifying_key .verify_raw(&msg_hash, &signature) .map_err(|_| Error::SignatureVerification)?; @@ -218,42 +129,12 @@ pub(crate) fn verify_schnorr_signature( Ok(()) } -/// Fetch the /v1/enclave-info endpoint. -pub(crate) async fn fetch_enclave_info( - http_client: &reqwest::Client, - base_url: &str, -) -> Result { - let url = format!("{}/v1/enclave-info", base_url.trim_end_matches('/')); - - let resp = http_client.get(&url).send().await?; - let status = resp.status(); - - if status != reqwest::StatusCode::OK { - let body = resp.text().await.unwrap_or_default(); - return Err(Error::Other(format!( - "enclave-info status {status}: {}", - body.trim() - ))); - } - - let body = resp.bytes().await?; - let info: EnclaveInfoResponse = serde_json::from_slice(&body)?; - - if !info.error.is_empty() { - return Err(Error::Other(format!("enclave init error: {}", info.error))); - } - - Ok(info) -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_schnorr_signature_verification() { - // Real test vector captured from a running enclave. - // Note: response body includes trailing newline. let body = b"{\"status\":\"ready\"}\n"; let sig_hex = "f2f2c20eff91556cd3614fcf230a6e14b4d0574d3f91f8bf33b9acc76d61da40a691821f451e364385cbfed6ba6338e650f1de6fbca58c4a0cc7144185fa6eff"; let pubkey_hex = "028d0bcf2b3384781e74e647351c01c0852775b59f063cde314d67328927d20dd0"; @@ -268,19 +149,15 @@ mod tests { let sig_hex = "f2f2c20eff91556cd3614fcf230a6e14b4d0574d3f91f8bf33b9acc76d61da40a691821f451e364385cbfed6ba6338e650f1de6fbca58c4a0cc7144185fa6eff"; let pubkey_hex = "028d0bcf2b3384781e74e647351c01c0852775b59f063cde314d67328927d20dd0"; - let result = verify_schnorr_signature(body, sig_hex, pubkey_hex); - assert!(result.is_err()); + assert!(verify_schnorr_signature(body, sig_hex, pubkey_hex).is_err()); } #[test] fn test_schnorr_rejects_wrong_pubkey() { let body = b"{\"status\":\"ready\"}"; let sig_hex = "f2f2c20eff91556cd3614fcf230a6e14b4d0574d3f91f8bf33b9acc76d61da40a691821f451e364385cbfed6ba6338e650f1de6fbca58c4a0cc7144185fa6eff"; - // Completely different pubkey (different x-coordinate). let pubkey_hex = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let result = verify_schnorr_signature(body, sig_hex, pubkey_hex); - assert!(result.is_err()); + assert!(verify_schnorr_signature(body, sig_hex, pubkey_hex).is_err()); } } - diff --git a/ffi/Cargo.lock b/ffi/Cargo.lock index 9d282dd5..2dc96b30 100644 --- a/ffi/Cargo.lock +++ b/ffi/Cargo.lock @@ -37,12 +37,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "base16ct" version = "0.2.0" @@ -138,12 +132,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - [[package]] name = "block-buffer" version = "0.10.4" @@ -168,12 +156,6 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - [[package]] name = "cc" version = "1.2.61" @@ -190,12 +172,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "ciborium" version = "0.2.2" @@ -312,17 +288,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -370,14 +335,11 @@ dependencies = [ "hex", "k256", "p384", - "rand 0.9.4", - "reqwest", "serde", "serde_json", "sha2", "spki", "thiserror 2.0.18", - "tokio", "x509-cert", ] @@ -403,24 +365,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - [[package]] name = "futures-core" version = "0.3.32" @@ -544,223 +488,6 @@ dependencies = [ "digest", ] -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "itoa" version = "1.0.18" @@ -799,41 +526,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "mpcwallet-ffi" version = "0.1.0" @@ -842,6 +540,7 @@ dependencies = [ "ark-core", "bitcoin", "enclave-client", + "hex", "k256", "libc", "rand 0.8.6", @@ -849,7 +548,6 @@ dependencies = [ "serde_json", "sha2", "threshold", - "tokio", ] [[package]] @@ -879,12 +577,6 @@ dependencies = [ "base64ct", ] -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -901,15 +593,6 @@ dependencies = [ "spki", ] -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -937,61 +620,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -1066,44 +694,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -1114,73 +704,12 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustls" -version = "0.23.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "sec1" version = "0.7.3" @@ -1279,18 +808,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1324,22 +841,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "spki" version = "0.7.3" @@ -1350,12 +851,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "subtle" version = "2.6.1" @@ -1373,26 +868,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1446,16 +921,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.11.0" @@ -1492,75 +957,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio" -version = "1.52.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.44" @@ -1592,12 +988,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typenum" version = "1.20.0" @@ -1610,45 +1000,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1677,16 +1034,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.120" @@ -1729,199 +1076,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - [[package]] name = "x509-cert" version = "0.2.5" @@ -1934,29 +1094,6 @@ dependencies = [ "tls_codec", ] -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.48" @@ -1977,27 +1114,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.2" @@ -2018,39 +1134,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 29f47f51..1706e8d5 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -17,6 +17,6 @@ rand = "0.8" sha2 = "0.10" libc = "0.2" enclave-client = { path = "../crates/enclave-client" } -tokio = { version = "1", features = ["rt"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +hex = "0.4" diff --git a/ffi/src/enclave/mod.rs b/ffi/src/enclave/mod.rs index 2df35458..29a7d3a3 100644 --- a/ffi/src/enclave/mod.rs +++ b/ffi/src/enclave/mod.rs @@ -1,26 +1,40 @@ -//! FFI bindings for the forked enclave-client crate. +//! FFI bindings for the pure-verification `enclave-client` crate. //! -//! Thin C-ABI layer -- most logic lives in enclave-client. -//! The client manages its own attestation cache; no shadow cache here. +//! Three stateless C-ABI functions — no handles, no async, no HTTP. +//! Callers (Dart, integration tests) drive the attestation and request +//! protocol themselves and pass bytes in for crypto verification. +use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::os::raw::c_char; +use std::slice; -use enclave_client::{Client, Options}; +use enclave_client::{ + extract_app_key_hash, verify_attestation_doc as verify_doc, + verify_schnorr_signature as verify_sig, +}; use serde::Serialize; -/// Opaque client handle. -pub struct ClientHandle { - client: Client, - rt: tokio::runtime::Runtime, +#[derive(Serialize)] +struct AttestationVerifyResult { + ok: bool, + /// Hex-encoded PCR0 from the verified document. + #[serde(skip_serializing_if = "String::is_empty")] + pcr0: String, + /// Map of PCR index → hex-encoded PCR value (only non-empty PCRs). + #[serde(skip_serializing_if = "HashMap::is_empty")] + pcrs: HashMap, + /// SHA-256 hex of the enclave's attestation pubkey, extracted from + /// the document's UserData. Empty when key binding isn't supported. + #[serde(skip_serializing_if = "String::is_empty")] + app_key_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, } -/// JSON response for HTTP requests. #[derive(Serialize)] -struct FfiResponse { - status_code: u16, - body: String, - signature_verified: bool, +struct SignatureVerifyResult { + ok: bool, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } @@ -38,149 +52,118 @@ fn from_c_str(ptr: *const c_char) -> String { .into_owned() } -/// Create a new enclave client with eager attestation verification. -/// -/// Returns null on error (enclave unreachable, PCR0 mismatch, etc.) -#[no_mangle] -pub extern "C" fn enclave_client_new( - base_url: *const c_char, - pcr0: *const c_char, - cache_ttl_secs: u32, -) -> *mut ClientHandle { - let base_url = from_c_str(base_url); - let pcr0 = from_c_str(pcr0); - let ttl = if cache_ttl_secs == 0 { 60 } else { cache_ttl_secs as u64 }; - - let rt = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(rt) => rt, - Err(_) => return std::ptr::null_mut(), - }; - - let opts = Options { - expected_pcr0: pcr0, - cache_ttl: std::time::Duration::from_secs(ttl), - insecure_tls: true, - ..Default::default() - }; - - // Use init() for eager verification + connection warmup. - match rt.block_on(Client::init(&base_url, opts)) { - Ok(client) => Box::into_raw(Box::new(ClientHandle { client, rt })), - Err(_) => std::ptr::null_mut(), +fn from_bytes<'a>(ptr: *const u8, len: usize) -> &'a [u8] { + if ptr.is_null() || len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(ptr, len) } } } -/// Make a verified POST request. Returns JSON string. +/// Verify a CBOR-encoded COSE_Sign1 AWS Nitro attestation document. +/// +/// `doc_ptr`/`doc_len` — raw bytes of the attestation document (already +/// base64-decoded by the caller). +/// `expected_pcr0` — hex-encoded PCR0 the caller wants enforced. +/// `nonce_ptr`/`nonce_len` — the 20-byte nonce the caller put in the +/// GET query when fetching the document; the verifier asserts the +/// embedded nonce matches. +/// +/// Returns a JSON string: +/// `{ "ok": true, "pcr0": "...", "pcrs": {...}, "app_key_hash": "..." }` +/// `{ "ok": false, "error": "..." }` +/// +/// The returned string must be freed with `enclave_string_free`. #[no_mangle] -pub extern "C" fn enclave_client_post( - handle: *mut ClientHandle, - path: *const c_char, - body: *const c_char, +pub extern "C" fn enclave_verify_attestation_doc( + doc_ptr: *const u8, + doc_len: usize, + expected_pcr0: *const c_char, + nonce_ptr: *const u8, + nonce_len: usize, ) -> *mut c_char { - let handle = unsafe { - if handle.is_null() { - return to_c_string(r#"{"error":"null client handle"}"#); + let doc = from_bytes(doc_ptr, doc_len); + let pcr0 = from_c_str(expected_pcr0); + let nonce = from_bytes(nonce_ptr, nonce_len); + + let result = match verify_doc(doc, &pcr0, nonce) { + Ok(r) => r, + Err(e) => { + return to_c_string( + &serde_json::to_string(&AttestationVerifyResult { + ok: false, + pcr0: String::new(), + pcrs: HashMap::new(), + app_key_hash: String::new(), + error: Some(e.to_string()), + }) + .unwrap_or_default(), + ); } - &*handle - }; - let path = from_c_str(path); - let body = from_c_str(body); - - let resp = match handle.rt.block_on(handle.client.post(&path, body)) { - Ok(r) => FfiResponse { - status_code: r.status_code, - body: String::from_utf8_lossy(&r.body).into_owned(), - signature_verified: r.signature_verified, - error: None, - }, - Err(e) => FfiResponse { - status_code: 0, - body: String::new(), - signature_verified: false, - error: Some(e.to_string()), - }, }; - to_c_string(&serde_json::to_string(&resp).unwrap_or_default()) -} + // Try to extract the appKeyHash — if the document predates the + // nitriding key-binding format, return ok with empty app_key_hash + // and let the caller decide how to handle it. + let app_key_hash = extract_app_key_hash(&result.document).unwrap_or_default(); -/// Make a verified GET request. Returns JSON string. -#[no_mangle] -pub extern "C" fn enclave_client_get( - handle: *mut ClientHandle, - path: *const c_char, -) -> *mut c_char { - let handle = unsafe { - if handle.is_null() { - return to_c_string(r#"{"error":"null client handle"}"#); + let mut pcrs = HashMap::new(); + for (idx, bytes) in &result.document.pcrs { + if !bytes.is_empty() { + pcrs.insert(idx.to_string(), hex::encode(bytes)); } - &*handle - }; - let path = from_c_str(path); + } - let resp = match handle.rt.block_on(handle.client.get(&path)) { - Ok(r) => FfiResponse { - status_code: r.status_code, - body: String::from_utf8_lossy(&r.body).into_owned(), - signature_verified: r.signature_verified, + let pcr0_hex = result + .document + .pcrs + .get(&0) + .map(hex::encode) + .unwrap_or_default(); + + to_c_string( + &serde_json::to_string(&AttestationVerifyResult { + ok: true, + pcr0: pcr0_hex, + pcrs, + app_key_hash, error: None, - }, - Err(e) => FfiResponse { - status_code: 0, - body: String::new(), - signature_verified: false, - error: Some(e.to_string()), - }, - }; - - to_c_string(&serde_json::to_string(&resp).unwrap_or_default()) + }) + .unwrap_or_default(), + ) } -/// Read cached attestation status (no network call). -#[no_mangle] -pub extern "C" fn enclave_client_attestation_status( - handle: *mut ClientHandle, -) -> *mut c_char { - let handle = unsafe { - if handle.is_null() { - return to_c_string(r#"{"verified":false,"pcr0":"","attestation_key":"","verified_at_epoch_secs":0,"ttl_remaining_secs":0}"#); - } - &*handle - }; - - let status = handle.client.attestation_status(); - to_c_string(&serde_json::to_string(&status).unwrap_or_default()) -} - -/// Force re-verification and return updated status. +/// Verify a BIP-340 Schnorr signature over `SHA256(body)` using the +/// hex-encoded secp256k1 attestation pubkey (compressed or x-only). +/// +/// Returns a JSON string: +/// `{ "ok": true }` +/// `{ "ok": false, "error": "..." }` +/// +/// The returned string must be freed with `enclave_string_free`. #[no_mangle] -pub extern "C" fn enclave_client_verify( - handle: *mut ClientHandle, +pub extern "C" fn enclave_verify_schnorr_signature( + body_ptr: *const u8, + body_len: usize, + sig_hex: *const c_char, + pubkey_hex: *const c_char, ) -> *mut c_char { - let handle = unsafe { - if handle.is_null() { - return to_c_string(r#"{"verified":false,"error":"null client handle"}"#); - } - &*handle + let body = from_bytes(body_ptr, body_len); + let sig = from_c_str(sig_hex); + let pk = from_c_str(pubkey_hex); + + let result = match verify_sig(body, &sig, &pk) { + Ok(()) => SignatureVerifyResult { ok: true, error: None }, + Err(e) => SignatureVerifyResult { + ok: false, + error: Some(e.to_string()), + }, }; - let _ = handle.rt.block_on(handle.client.verify_attestation()); - let status = handle.client.attestation_status(); - to_c_string(&serde_json::to_string(&status).unwrap_or_default()) -} - -/// Free a client handle. -#[no_mangle] -pub extern "C" fn enclave_client_free(handle: *mut ClientHandle) { - if !handle.is_null() { - unsafe { drop(Box::from_raw(handle)); } - } + to_c_string(&serde_json::to_string(&result).unwrap_or_default()) } -/// Free a string returned by FFI functions. +/// Free a string returned by any of the verifier FFI functions. #[no_mangle] pub extern "C" fn enclave_string_free(s: *mut c_char) { if !s.is_null() { diff --git a/infrastructure/mutiny/enclave.yaml b/infrastructure/mutiny/enclave.yaml index f36c13d8..8e1dd565 100644 --- a/infrastructure/mutiny/enclave.yaml +++ b/infrastructure/mutiny/enclave.yaml @@ -8,11 +8,12 @@ account: "639920118099" # AWS account ID (required) prefix: dev # Deployment prefix (stack = {prefix}Nitro{Name}) instance_type: m6i.xlarge # EC2 instance type migration_cooldown: "1m" # Cooldown before migration proceeds +is_kms_key_locked: false runtime: - rev: "v0.0.74" - hash: "sha256-9cXFGEcexHZr1JEqU004Okxs55kQdvb7fm4221qgWMU=" - vendor_hash: "sha256-2ZX9x2voIwVkUsmrZvhJssAQqnCh8/HLwopL5HpiaOg=" + rev: "v0.0.79" + hash: "sha256-sIDpJVIuOzyl1NlmPA0qOks61ppsTtyc3pJYIq/4sWY=" + vendor_hash: "sha256-kkgp+uxc9pohZiCnz84nyXroUzsPhhtb0VgGa3dQgUo=" app: language: "rust" # App language: go, nodejs, dotnet, rust @@ -35,9 +36,9 @@ app: nix_build_inputs: ["openssl"] nix_native_build_inputs: ["pkg-config", "protobuf"] - # Environment variables baked into the EIF. - # Template vars: {{region}}, {{prefix}}, {{version}} + env: + ENCLAVE_NITRIDING_UPSTREAM: "h1" PORT: "7074" COSIGNER_WASM_PATH: "/app/cosigner.wasm" BITCOIN_NETWORK: "signet" @@ -45,7 +46,7 @@ app: ELECTRUM_PORT: "50001" ASP_URL: "https://mutinynet.arkade.sh" PERSISTENCE_BACKEND: "enclave" - SUPERVISOR_URL: "http://127.0.0.1:7073" + SUPERVISOR_URL: "http://127.0.0.1:8080" # Secrets managed by KMS inside the enclave. @@ -57,3 +58,9 @@ secrets: env_var: APP_SIGNING_KEY # - name: api_token # env_var: APP_API_TOKEN + +tls: + provider: letsencrypt + fqdn: "mutiny.vtxos.network" + email: "joshua@vtxos.app" + route53_zone_id: "Z0182614JSFDPB9F5ALY" diff --git a/infrastructure/mutiny/env.md b/infrastructure/mutiny/env.md new file mode 100644 index 00000000..53aea17b --- /dev/null +++ b/infrastructure/mutiny/env.md @@ -0,0 +1,101 @@ +# Enclave env-value overrides (FCM and friends) + +How to inject deploy-time values into the enclave's environment via SSM, +without baking them into the public EIF or committing them to git. + +The plumbing lives in [tofu/modules/enclave/main.tf](tofu/modules/enclave/main.tf) +(`env_values` map → per-key `aws_ssm_parameter.env_override`). At boot the +runtime scans every key under `///env/` and overlays it onto +the process env on top of `enclave.yaml`'s `app.env` defaults — no +pre-declaration required. + +## One-time setup + +Keep the source key file **outside** the repo. Default convention: + +``` +~/secrets/vtxos-fcm.json +``` + +[tofu/env_values.auto.tfvars.json](tofu/.gitignore) is gitignored — it never +gets committed. + +## Set the value (preferred: enclave CLI) + +```bash +enclave tofu env \ + --key FCM_SERVICE_ACCOUNT_JSON \ + --value "$(cat ~/secrets/vtxos-fcm.json)" +``` + +The CLI merges into `tofu/env_values.auto.tfvars.json`, preserves any +existing entries, and handles JSON-escaping (inner `"`, PEM newlines) +internally. Add more keys in the same invocation by repeating the pair: + +```bash +enclave tofu env \ + --key FCM_SERVICE_ACCOUNT_JSON --value "$(cat ~/secrets/vtxos-fcm.json)" \ + --key OTHER_KEY --value "$(cat ~/secrets/other.txt)" +``` + +## Set the value (fallback: jq when the CLI isn't around) + +```bash +jq -n --arg fcm "$(cat ~/secrets/vtxos-fcm.json)" \ + '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm}}' \ + > infrastructure/mutiny/tofu/env_values.auto.tfvars.json +``` + +The `*.auto.tfvars.json` suffix makes OpenTofu auto-load it; no `-var-file` +flag needed. **Overwrites** the whole file, so use the CLI form above when +you have other keys to preserve. + +## Deploy + +```bash +cd infrastructure/mutiny/tofu +tofu apply +``` + +Writes `///env/` per entry, e.g. +`/dev/bitspend-server/env/FCM_SERVICE_ACCOUNT_JSON`. + +## Make the enclave pick it up + +Restart the enclave — SSM overrides are read at boot. **EIF rebuild is never +required for env_values changes**, neither for new keys nor value updates. + +## Verify + +On a host with AWS creds for the same account: + +```bash +aws ssm get-parameter \ + --name /dev/bitspend-server/env/FCM_SERVICE_ACCOUNT_JSON \ + --region us-east-1 \ + --query 'Parameter.Value' --output text | head -c 80 +``` + +Should print the opening of the JSON, confirming the value is there. + +## Rotation + +```bash +# Refresh the source file from GCP, then: +enclave tofu env --key FCM_SERVICE_ACCOUNT_JSON \ + --value "$(cat ~/secrets/vtxos-fcm.json)" +cd infrastructure/mutiny/tofu && tofu apply +# Then restart the enclave. +``` + +## Notes + +- The SSM parameter is `type = "String"`, not `SecureString`. Anyone in AWS + account `639920118099` with `ssm:GetParameter` on that path can read the + plaintext. That's a big upgrade from "in public git" or "in public EIF", + but a step below the PCR0-locked KMS-ciphertext path (`enclave.yaml`'s + `secrets:` mechanism). For a Firebase-Messaging-only service account it's + acceptable; revisit before any value with custody blast radius lands here. +- `terraform.tfvars.json` and `env_values.auto.tfvars.json` are both + gitignored. Both contain account-specific values; neither should ever be + committed. diff --git a/infrastructure/mutiny/tofu/.gitignore b/infrastructure/mutiny/tofu/.gitignore index a0029b90..3e5d2a48 100644 --- a/infrastructure/mutiny/tofu/.gitignore +++ b/infrastructure/mutiny/tofu/.gitignore @@ -5,5 +5,8 @@ terraform.tfstate terraform.tfstate.backup .terraform/ backend.tf -.terraform.lock.hcl -.artifacts/ \ No newline at end of file +modules/enclave/.signing/ + +# Deploy-time env overrides — contains the plaintext FCM service-account +# JSON (and any other operator-provided env values). Holds real secrets. +env_values.auto.tfvars.json diff --git a/infrastructure/mutiny/tofu/main.tf b/infrastructure/mutiny/tofu/main.tf index 707b1b3d..560b0e26 100644 --- a/infrastructure/mutiny/tofu/main.tf +++ b/infrastructure/mutiny/tofu/main.tf @@ -37,7 +37,6 @@ module "enclave" { local = var.local secrets = var.secrets migration_cooldown = var.migration_cooldown - previous_pcr0 = var.previous_pcr0 expected_pcr0 = var.expected_pcr0 supervisor_url = var.supervisor_url github_owner = var.github_owner @@ -49,6 +48,7 @@ module "enclave" { supervisor_binary_path = var.supervisor_binary_path env_values = var.env_values + tls = var.tls } # ============================================================================= @@ -107,12 +107,6 @@ variable "migration_cooldown" { default = "0s" } -variable "previous_pcr0" { - description = "Previous PCR0 hash for migration chain validation." - type = string - default = "genesis" -} - variable "expected_pcr0" { description = "Expected PCR0 of the current EIF (from pcr.json). Used to trigger migrations." type = string @@ -174,6 +168,22 @@ variable "env_values" { default = {} } +variable "tls" { + description = "TLS settings for the enclave's public HTTPS listener, published to SSM as ///env/ENCLAVE_NITRIDING_* and read by the runtime at boot to select the cert source (self-signed or ACME). route53_zone_id, when set, opts into automatic A-record management in that zone for fqdn." + type = object({ + fqdn = string + provider = string + email = string + route53_zone_id = string + }) + default = { + fqdn = "" + provider = "self-signed" + email = "" + route53_zone_id = "" + } +} + # ============================================================================= # Outputs @@ -186,12 +196,6 @@ output "ec2_role_arn" { value = module.enclave.ec2_role_arn } -output "kms_key_id" { - description = "KMS encryption key ID." - value = module.enclave.kms_key_id - sensitive = true -} - output "instance_id" { description = "EC2 instance ID (empty in local mode)." value = module.enclave.instance_id @@ -207,3 +211,8 @@ output "storage_bucket" { value = module.enclave.storage_bucket } +output "pcr0_signing_key_arn" { + description = "ARN of the PCR0 signing key. The identity running 'tofu apply' must have kms:Sign + kms:GetPublicKey on this key." + value = module.enclave.pcr0_signing_key_arn +} + diff --git a/infrastructure/mutiny/tofu/modules/backend/.terraform.lock.hcl b/infrastructure/mutiny/tofu/modules/backend/.terraform.lock.hcl new file mode 100644 index 00000000..ae83221d --- /dev/null +++ b/infrastructure/mutiny/tofu/modules/backend/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:zef23ac/YWw9O2FepFWRs+my9iWWUkniL4dT4LnCKjU=", + "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", + "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", + "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", + "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", + "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", + "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", + "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", + "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", + "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", + "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", + ] +} diff --git a/infrastructure/mutiny/tofu/modules/enclave/main.tf b/infrastructure/mutiny/tofu/modules/enclave/main.tf index d6e5f8f4..2ea8f112 100644 --- a/infrastructure/mutiny/tofu/modules/enclave/main.tf +++ b/infrastructure/mutiny/tofu/modules/enclave/main.tf @@ -77,12 +77,6 @@ variable "migration_cooldown" { default = "0s" } -variable "previous_pcr0" { - description = "Previous PCR0 hash for migration chain validation." - type = string - default = "genesis" -} - variable "expected_pcr0" { description = "Expected PCR0 of the current EIF (from pcr.json). Used to trigger migrations." type = string @@ -145,135 +139,121 @@ variable "env_values" { default = {} } +variable "tls" { + description = "TLS settings for the enclave's public HTTPS listener, published to SSM as ///env/ENCLAVE_NITRIDING_* and read by the runtime at boot to select the cert source (self-signed or ACME). route53_zone_id, when set, opts into automatic A-record management in that zone for fqdn." + type = object({ + fqdn = string + provider = string + email = string + route53_zone_id = string + }) + default = { + fqdn = "" + provider = "self-signed" + email = "" + route53_zone_id = "" + } +} + # ============================================================================= # KMS # ============================================================================= -# KMS encryption key for enclave secrets. -# -# Created via AWS CLI (null_resource) instead of a native tofu resource -# because the enclave locks the key policy to PCR0 at first boot, and the -# supervisor replaces the key entirely during migration. Tofu cannot -# refresh a locked key (DescribeKey/GetKeyPolicy/GetKeyRotationStatus all -# fail with AccessDenied), so the key must not exist as a tofu resource. -# -# The key ID is stored in SSM and read back via a data source. All other -# resources reference locals.kms_key_id / locals.kms_key_arn. -# Key deletion is handled by the supervisor's destroy provisioner. +# KMSKeyID placeholder. The enclave creates the KMS key with a PCR0-locked +# policy at first boot (runtime EnsureKeyID) and writes the real ID here. +# ignore_changes keeps tofu apply from fighting that runtime write. On destroy +# the EC2 destroy provisioner shells out to the supervisor to schedule key +# deletion before the role is torn down. +resource "aws_ssm_parameter" "kms_key_id" { + name = "/${var.deployment}/${var.app_name}/KMSKeyID" + type = "String" + value = "UNSET" + overwrite = true -resource "null_resource" "kms_key" { - # Only runs once per deployment. The supervisor handles key rotation - # during migration (creates new keys, updates SSM). - triggers = { - deployment = var.deployment - app_name = var.app_name - region = var.region + lifecycle { + ignore_changes = [value] } +} - provisioner "local-exec" { - command = <<-EOT - set -e - - # Check if a key already exists in SSM (idempotent). - EXISTING=$(aws ssm get-parameter \ - --name "/${var.deployment}/${var.app_name}/KMSKeyID" \ - --region ${var.region} --query 'Parameter.Value' --output text 2>/dev/null || echo "UNSET") - if [ "$EXISTING" != "UNSET" ] && [ -n "$EXISTING" ]; then - echo "KMS key already exists in SSM: $EXISTING" - exit 0 - fi +# PCR0 signing key. Deleting it makes every past signature un-verifiable — +# the 30-day deletion window is the only safety net. +resource "aws_kms_key" "pcr0_signing" { + description = "${local.prefix} PCR0 signing key (ECC_NIST_P384)" + customer_master_key_spec = "ECC_NIST_P384" + key_usage = "SIGN_VERIFY" + enable_key_rotation = false + deletion_window_in_days = 30 - # Create the key. - KEY_ID=$(aws kms create-key \ - --description "${local.prefix} enclave encryption key" \ - --region ${var.region} \ - --tags TagKey=AppName,TagValue=${var.app_name} TagKey=Deployment,TagValue=${var.deployment} TagKey=ManagedBy,TagValue=opentofu \ - --query 'KeyMetadata.KeyId' --output text) - echo "Created KMS key: $KEY_ID" - - # Apply initial key policy. - POLICY='${jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowRootAccount" - Effect = "Allow" - Principal = { AWS = "arn:aws:iam::${var.account}:root" } - Action = "kms:*" - Resource = "*" - }, - { - Sid = "AllowInstanceRole" - Effect = "Allow" - Principal = { AWS = aws_iam_role.instance.arn } - Action = [ - "kms:Encrypt", - "kms:Decrypt", - "kms:GenerateDataKey", - "kms:DescribeKey", - "kms:PutKeyPolicy", - "kms:GetKeyPolicy", - ] - Resource = "*" - }, - ] - })}' - - aws kms put-key-policy --key-id "$KEY_ID" --policy-name default \ - --policy "$POLICY" --region ${var.region} - - # Store in SSM. - aws ssm put-parameter \ - --name "/${var.deployment}/${var.app_name}/KMSKeyID" \ - --value "$KEY_ID" --type String --overwrite \ - --region ${var.region} --no-cli-pager - - echo "KMS key $KEY_ID stored in SSM" - EOT + lifecycle { + prevent_destroy = false } +} + +resource "aws_kms_alias" "pcr0_signing" { + name = "alias/${local.prefix}-pcr0-signing" + target_key_id = aws_kms_key.pcr0_signing.key_id +} + +resource "terraform_data" "sign_pcr0" { + triggers_replace = [local.effective_pcr0, aws_kms_key.pcr0_signing.key_id] - # On destroy: schedule the KMS key for deletion and remove the SSM pointer - # so that a subsequent apply creates a fresh key. provisioner "local-exec" { - when = destroy - command = <<-EOT - set -e - REGION="${lookup(self.triggers, "region", "us-east-1")}" - DEPLOYMENT="${self.triggers.deployment}" - APP_NAME="${self.triggers.app_name}" - - KEY_ID=$(aws ssm get-parameter \ - --name "/$DEPLOYMENT/$APP_NAME/KMSKeyID" \ - --region "$REGION" --query 'Parameter.Value' --output text 2>/dev/null || echo "UNSET") - - if [ "$KEY_ID" != "UNSET" ] && [ -n "$KEY_ID" ]; then - echo "Scheduling KMS key $KEY_ID for deletion (7-day window)..." - aws kms schedule-key-deletion \ - --key-id "$KEY_ID" \ - --pending-window-in-days 7 \ - --region "$REGION" 2>/dev/null || echo "Key already pending deletion or not found" - - echo "Removing KMSKeyID SSM parameter..." - aws ssm delete-parameter \ - --name "/$DEPLOYMENT/$APP_NAME/KMSKeyID" \ - --region "$REGION" 2>/dev/null || echo "SSM parameter already removed" - else - echo "No KMS key found in SSM — nothing to clean up" - fi + # bash for pipefail; default /bin/sh is dash on slim images. + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + mkdir -p ${local.signing_dir} + # hex → bytes without xxd (not in slim images) + printf '%b' "$(printf '%s' "${local.effective_pcr0}" | sed 's/../\\x&/g')" \ + > ${local.signing_dir}/pcr0.bin + aws kms get-public-key \ + --key-id ${aws_kms_key.pcr0_signing.arn} \ + --query PublicKey --output text \ + | base64 -d > ${local.signing_dir}/pubkey.der + openssl ec -pubin -inform DER -outform PEM \ + < ${local.signing_dir}/pubkey.der \ + > ${local.signing_dir}/pubkey.pem + aws kms sign \ + --key-id ${aws_kms_key.pcr0_signing.arn} \ + --message fileb://${local.signing_dir}/pcr0.bin \ + --message-type DIGEST \ + --signing-algorithm ECDSA_SHA_384 \ + --query Signature --output text \ + > ${local.signing_dir}/signature.b64 EOT } } -# Read the KMS key ID from SSM (written by null_resource.kms_key or supervisor). -data "aws_ssm_parameter" "kms_key_id_lookup" { - name = "/${var.deployment}/${var.app_name}/KMSKeyID" - depends_on = [null_resource.kms_key] +data "local_file" "pcr0_pubkey_pem" { + filename = "${local.signing_dir}/pubkey.pem" + depends_on = [terraform_data.sign_pcr0] } -locals { - kms_key_id = data.aws_ssm_parameter.kms_key_id_lookup.value - kms_key_arn = "arn:aws:kms:${var.region}:${var.account}:key/${local.kms_key_id}" +data "local_file" "pcr0_signature_b64" { + filename = "${local.signing_dir}/signature.b64" + depends_on = [terraform_data.sign_pcr0] +} + +resource "aws_ssm_parameter" "pcr0_pubkey" { + name = "/${var.deployment}/${var.app_name}/Signing/PubkeyPEM" + type = "String" + value = data.local_file.pcr0_pubkey_pem.content + overwrite = true +} + +resource "aws_ssm_parameter" "pcr0_value" { + name = "/${var.deployment}/${var.app_name}/Signing/PCR0" + type = "String" + value = local.effective_pcr0 + overwrite = true +} + +resource "aws_ssm_parameter" "pcr0_signature" { + name = "/${var.deployment}/${var.app_name}/Signing/Signature" + type = "String" + value = trimspace(data.local_file.pcr0_signature_b64.content) + overwrite = true } # ============================================================================= @@ -344,7 +324,9 @@ data "aws_iam_policy_document" "enclave" { ] } - # SSM: read/write on secret ciphertext parameters. + # SSM: read/write on secret + DEK ciphertext parameters. Paths are key-scoped + # (/.../{secret}/Ciphertext/{kmsKeyId}) and created by the runtime at boot / + # migration, so the wildcard covers every KMS key generation. statement { sid = "SSMSecretParams" actions = [ @@ -352,29 +334,36 @@ data "aws_iam_policy_document" "enclave" { "ssm:PutParameter", ] resources = concat( - [for p in aws_ssm_parameter.secret_ciphertext : p.arn], - [for p in aws_ssm_parameter.secret_migration : p.arn], + [for s in var.secrets : "arn:aws:ssm:${var.region}:${var.account}:parameter/${var.deployment}/${var.app_name}/${s.name}/Ciphertext/*"], [ - aws_ssm_parameter.migration_kms_key_id.arn, + "arn:aws:ssm:${var.region}:${var.account}:parameter/${var.deployment}/${var.app_name}/StorageDEK/Ciphertext/*", aws_ssm_parameter.migration_previous_pcr0.arn, aws_ssm_parameter.migration_previous_pcr0_attestation.arn, - aws_ssm_parameter.migration_old_kms_key_id.arn, - aws_ssm_parameter.migration_target_pcr0.arn, aws_ssm_parameter.migration_requested_at.arn, - aws_ssm_parameter.storage_dek.arn, - aws_ssm_parameter.migration_storage_dek.arn, ], ) } - # SSM: read-only parameters. + # SSM: read-only known one-off parameters. statement { - sid = "SSMReadOnly" + sid = "SSMReadKnownParams" actions = ["ssm:GetParameter"] - resources = concat( - [aws_ssm_parameter.storage_bucket_name.arn], - [for p in aws_ssm_parameter.env_override : p.arn], - ) + resources = [ + aws_ssm_parameter.storage_bucket_name.arn, + aws_ssm_parameter.pcr0_pubkey.arn, + aws_ssm_parameter.pcr0_value.arn, + aws_ssm_parameter.pcr0_signature.arn, + ] + } + + # SSM: prefix-scoped env overrides. The runtime calls GetParametersByPath + # to fetch the whole map at boot — no need to enumerate keys in the EIF. + statement { + sid = "SSMReadEnvOverridesPrefix" + actions = ["ssm:GetParameter", "ssm:GetParametersByPath"] + resources = [ + "arn:aws:ssm:${var.region}:${var.account}:parameter/${var.deployment}/${var.app_name}/env/*", + ] } # SSM: KMSKeyID needs read+write (supervisor updates it during migration). @@ -386,7 +375,7 @@ data "aws_iam_policy_document" "enclave" { ] } - # KMS: encrypt/decrypt + policy management. + # KMS access. PutKeyPolicy is not granted — keys are policy-locked at CreateKey time. statement { sid = "KMSAccess" actions = [ @@ -394,7 +383,6 @@ data "aws_iam_policy_document" "enclave" { "kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey", - "kms:PutKeyPolicy", "kms:GetKeyPolicy", "kms:ScheduleKeyDeletion", "kms:CreateKey", @@ -403,7 +391,7 @@ data "aws_iam_policy_document" "enclave" { resources = ["*"] } - # STS: get caller identity for building transitional KMS policies. + # STS: get caller identity for role ARN / account ID. statement { sid = "STSAccess" actions = ["sts:GetCallerIdentity"] @@ -435,6 +423,7 @@ locals { # When local paths are set, use them directly. Otherwise download from GitHub Release. use_local = var.eif_path != "" artifacts_dir = "${path.module}/.artifacts" + signing_dir = "${path.module}/.signing" release_base = "https://github.com/${var.github_owner}/${var.github_repo}/releases/download/${var.release_tag}" eif_source = local.use_local ? var.eif_path : "${local.artifacts_dir}/image.eif" @@ -595,52 +584,8 @@ resource "aws_s3_bucket_policy" "storage_ssl" { # SSM # ============================================================================= -# SSM parameters for enclave secrets and migration state. - -locals { - secrets_map = { for s in var.secrets : s.name => s } -} - -# Per-secret ciphertext parameters. -resource "aws_ssm_parameter" "secret_ciphertext" { - for_each = local.secrets_map - - name = "/${var.deployment}/${var.app_name}/${each.key}/Ciphertext" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] - } -} - -# Per-secret migration ciphertext parameters. -resource "aws_ssm_parameter" "secret_migration" { - for_each = local.secrets_map - - name = "/${var.deployment}/${var.app_name}/Migration/${each.key}/Ciphertext" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] - } -} - -# Shared migration parameters (one per deployment, not per secret). - -resource "aws_ssm_parameter" "migration_kms_key_id" { - name = "/${var.deployment}/${var.app_name}/MigrationKMSKeyID" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] - } -} +# SSM parameters for migration state. Per-secret + DEK ciphertexts are key-scoped +# (/.../{secret}/Ciphertext/{kmsKeyId}) and created by the runtime, not here. resource "aws_ssm_parameter" "migration_previous_pcr0" { name = "/${var.deployment}/${var.app_name}/MigrationPreviousPCR0" @@ -676,31 +621,29 @@ resource "aws_ssm_parameter" "migration_requested_at" { } } -resource "aws_ssm_parameter" "migration_old_kms_key_id" { - name = "/${var.deployment}/${var.app_name}/MigrationOldKMSKeyID" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] +# Deletes the runtime-created key-scoped ciphertext params on tofu destroy +# (tofu doesn't track them). A var.secrets change replaces only this no-op +# resource. +resource "null_resource" "ciphertext_cleanup" { + triggers = { + region = var.region + deployment = var.deployment + app_name = var.app_name + secret_names = jsonencode([for s in var.secrets : s.name]) } -} - -resource "aws_ssm_parameter" "migration_target_pcr0" { - name = "/${var.deployment}/${var.app_name}/MigrationTargetPCR0" - type = "String" - value = "UNSET" - overwrite = true - lifecycle { - ignore_changes = [value] + provisioner "local-exec" { + when = destroy + command = <<-EOT + REGION="${self.triggers.region}"; DEP="${self.triggers.deployment}"; APP="${self.triggers.app_name}" + for S in $(echo '${self.triggers.secret_names}' | jq -r '.[]' 2>/dev/null); do + aws ssm delete-parameters-by-path --path "/$DEP/$APP/$S/Ciphertext" --recursive --region "$REGION" 2>/dev/null || true + done + aws ssm delete-parameters-by-path --path "/$DEP/$APP/StorageDEK/Ciphertext" --recursive --region "$REGION" 2>/dev/null || true + EOT } } -# KMS key ID — managed by null_resource.kms_key (kms.tf) and the supervisor -# during migration. Not a tofu resource because the value changes outside tofu. - # Storage bucket name. resource "aws_ssm_parameter" "storage_bucket_name" { name = "/${var.deployment}/${var.app_name}/StorageBucketName" @@ -709,35 +652,26 @@ resource "aws_ssm_parameter" "storage_bucket_name" { overwrite = true } -# Storage data encryption key (DEK). -resource "aws_ssm_parameter" "storage_dek" { - name = "/${var.deployment}/${var.app_name}/StorageDEK/Ciphertext" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] - } -} - -# Migration storage DEK. -resource "aws_ssm_parameter" "migration_storage_dek" { - name = "/${var.deployment}/${var.app_name}/Migration/StorageDEK/Ciphertext" - type = "String" - value = "UNSET" - overwrite = true - - lifecycle { - ignore_changes = [value] - } +# Deploy-time env overrides, published to SSM at ///env/. +# Merges two sources: +# - var.env_values: overrides for keys declared in app.env (enclave.yaml); +# the runtime overlays them on the EIF's baked defaults at boot. +# - local.tls_params: the ENCLAVE_NITRIDING_* TLS settings from var.tls, +# read by the runtime at boot to select the cert source. +locals { + # SSM rejects empty values, so each key is published only when it has one. + tls_params = merge( + var.tls.fqdn != "" ? { ENCLAVE_NITRIDING_FQDN = var.tls.fqdn } : {}, + var.tls.provider != "self-signed" ? { + ENCLAVE_NITRIDING_USE_ACME = "true" + ENCLAVE_NITRIDING_ACME_DIRECTORY = var.tls.provider + } : {}, + var.tls.provider != "self-signed" && var.tls.email != "" ? { ENCLAVE_NITRIDING_ACME_EMAIL = var.tls.email } : {} + ) } -# Deploy-time app.env overrides. The runtime reads each key listed in -# ENCLAVE_APP_ENV_KEYS (baked into the EIF) and overlays the SSM value -# on top of the baked default. Missing keys leave the default in place. resource "aws_ssm_parameter" "env_override" { - for_each = var.env_values + for_each = merge(var.env_values, local.tls_params) name = "/${var.deployment}/${var.app_name}/env/${each.key}" type = "String" @@ -1015,14 +949,12 @@ resource "aws_instance" "nitro" { } user_data = templatefile("${path.module}/templates/user_data.sh.tftpl", { - region = var.region - dev_mode = var.deployment - app_name = var.app_name - kms_key_id = local.kms_key_id - eif_s3_url = "s3://${aws_s3_bucket.assets.id}/${aws_s3_object.enclave_eif.key}" - supervisor_binary_s3_url = "s3://${aws_s3_bucket.assets.id}/${aws_s3_object.supervisor_binary.key}" - migration_cooldown = var.migration_cooldown - previous_pcr0 = var.previous_pcr0 + region = var.region + dev_mode = var.deployment + app_name = var.app_name + eif_s3_url = "s3://${aws_s3_bucket.assets.id}/${aws_s3_object.enclave_eif.key}" + supervisor_binary_s3_url = "s3://${aws_s3_bucket.assets.id}/${aws_s3_object.supervisor_binary.key}" + migration_cooldown = var.migration_cooldown }) tags = { @@ -1091,6 +1023,19 @@ resource "aws_eip_association" "instance" { instance_id = aws_instance.nitro[0].id } +# Optional Route53 A record. Skipped in local mode or when route53_zone_id is +# unset; operator-managed DNS providers (Cloudflare, registrar, etc.) keep +# pointing at the elastic_ip output manually. +resource "aws_route53_record" "enclave" { + count = var.local || var.tls.route53_zone_id == "" ? 0 : 1 + + zone_id = var.tls.route53_zone_id + name = var.tls.fqdn + type = "A" + ttl = 60 + records = [aws_eip.instance[0].public_ip] +} + # Automatic migration — triggers when the EIF changes (new PCR0). # On first apply this is a no-op (no running enclave to migrate). # On subsequent applies with a new EIF, it calls the supervisor to @@ -1101,7 +1046,7 @@ resource "null_resource" "enclave_migration" { count = var.local ? 0 : 1 triggers = { - eif_etag = aws_s3_object.enclave_eif.etag + eif_etag = data.local_file.eif.content_md5 expected_pcr0 = local.effective_pcr0 } @@ -1161,7 +1106,7 @@ resource "null_resource" "promote_supervisor_binary" { count = var.local ? 0 : 1 triggers = { - eif_etag = aws_s3_object.enclave_eif.etag + eif_etag = data.local_file.eif.content_md5 expected_pcr0 = local.effective_pcr0 } @@ -1184,7 +1129,7 @@ resource "null_resource" "enclave_migration_local" { count = var.local ? 1 : 0 triggers = { - eif_etag = aws_s3_object.enclave_eif.etag + eif_etag = data.local_file.eif.content_md5 expected_pcr0 = local.effective_pcr0 } @@ -1216,7 +1161,7 @@ resource "null_resource" "promote_supervisor_binary_local" { count = var.local ? 1 : 0 triggers = { - eif_etag = aws_s3_object.enclave_eif.etag + eif_etag = data.local_file.eif.content_md5 expected_pcr0 = local.effective_pcr0 } @@ -1241,12 +1186,6 @@ output "ec2_role_arn" { value = aws_iam_role.instance.arn } -output "kms_key_id" { - description = "KMS encryption key ID." - value = local.kms_key_id - sensitive = true -} - output "instance_id" { description = "EC2 instance ID (empty in local mode)." value = var.local ? "" : aws_instance.nitro[0].id @@ -1261,3 +1200,8 @@ output "storage_bucket" { description = "S3 storage bucket name." value = aws_s3_bucket.storage.id } + +output "pcr0_signing_key_arn" { + description = "ARN of the KMS key used by Tofu to sign each build's PCR0." + value = aws_kms_key.pcr0_signing.arn +} diff --git a/infrastructure/mutiny/tofu/modules/enclave/templates/user_data.sh.tftpl b/infrastructure/mutiny/tofu/modules/enclave/templates/user_data.sh.tftpl index 54df2e39..032adaac 100644 --- a/infrastructure/mutiny/tofu/modules/enclave/templates/user_data.sh.tftpl +++ b/infrastructure/mutiny/tofu/modules/enclave/templates/user_data.sh.tftpl @@ -95,15 +95,12 @@ cat <> /etc/environment ENCLAVE_APP_NAME=${app_name} EIF_PATH=/home/ec2-user/app/server/enclave.eif ENCLAVE_NITRIDING_ENABLED=true -ENCLAVE_NITRIDING_FQDN=example.com -ENCLAVE_KMS_KEY_ID=${kms_key_id} ENCLAVE_DEPLOYMENT=${dev_mode} ENCLAVE_AWS_REGION=${region} ENCLAVE_MIGRATION_COOLDOWN=${migration_cooldown} -ENCLAVE_PREVIOUS_PCR0=${previous_pcr0} MEMORY_MIB=4320 CPU_COUNT=2 -GVPROXY_FORWARD_PORTS=443 7073 +GVPROXY_FORWARD_PORTS=443 EOF systemctl enable --now enclave-supervisor.service