From eafc9afaf80adecc2f242d332b02d84acfff3421 Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Sat, 23 May 2026 14:32:15 +0100 Subject: [PATCH 1/6] feat(firebase): add google-services.json and update firebase_messaging version to 15.2.10 chore(enclave): add FCM_SERVICE_ACCOUNT_JSON placeholder in enclave.yaml chore(.gitignore): ignore env_values.auto.tfvars.json for sensitive data --- app/android/app/google-services.json | 29 +++++++++++++++++++++++++++ app/pubspec.yaml | 2 +- infrastructure/mutiny/enclave.yaml | 4 ++++ infrastructure/mutiny/tofu/.gitignore | 7 ++++++- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 app/android/app/google-services.json 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/pubspec.yaml b/app/pubspec.yaml index 432da937..931ff206 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -58,7 +58,7 @@ 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 dev_dependencies: flutter_test: diff --git a/infrastructure/mutiny/enclave.yaml b/infrastructure/mutiny/enclave.yaml index f36c13d8..9de4743b 100644 --- a/infrastructure/mutiny/enclave.yaml +++ b/infrastructure/mutiny/enclave.yaml @@ -46,6 +46,10 @@ app: ASP_URL: "https://mutinynet.arkade.sh" PERSISTENCE_BACKEND: "enclave" SUPERVISOR_URL: "http://127.0.0.1:7073" + # Placeholder so the key joins ENCLAVE_APP_ENV_KEYS in the EIF; the + # runtime overlays the real JSON from SSM /env/FCM_SERVICE_ACCOUNT_JSON + # at boot (populated by tofu env_values, never committed). + FCM_SERVICE_ACCOUNT_JSON: "" # Secrets managed by KMS inside the enclave. diff --git a/infrastructure/mutiny/tofu/.gitignore b/infrastructure/mutiny/tofu/.gitignore index a0029b90..e7516fa2 100644 --- a/infrastructure/mutiny/tofu/.gitignore +++ b/infrastructure/mutiny/tofu/.gitignore @@ -6,4 +6,9 @@ terraform.tfstate.backup .terraform/ backend.tf .terraform.lock.hcl -.artifacts/ \ No newline at end of file +.artifacts/ + +# Deploy-time env overrides — values pushed to SSM via env_values map. +# Holds the plaintext FCM service-account JSON and any other operator +# secrets that aren't routed through the PCR0-locked secrets: mechanism. +env_values.auto.tfvars.json \ No newline at end of file From c90d75d1588b74536f1e61b434fcf6e7bafeee37 Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Mon, 25 May 2026 04:20:18 +0100 Subject: [PATCH 2/6] feat(onboarding): preset server picker (Regtest/Mutiny) + drop self-signed TLS bypass Replace the free-form URL text field with two preset cards on the onboarding server-connect screen. Regtest = 10.0.2.2 (gated on kDebugMode; emulator-only); Mutiny = mutiny.vtxos.network. Single-tap selects and advances to DKG. The Mutiny enclave now serves a CA-signed cert, so the production app's trust-everything HttpOverrides (in main.dart and integration test bootApp) is removed. The wallet now uses the system trust store like any other Dart HTTP caller. Integration test helpers updated: ServerConnectPage.useDefault -> pickRegtest / pickMutiny; the restore-flow stage in app_test.dart taps the new key. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/integration_test/app_test.dart | 4 +- app/integration_test/test_helpers/flows.dart | 2 +- .../test_helpers/page_objects.dart | 8 +- .../test_helpers/test_setup.dart | 9 - app/lib/main.dart | 11 -- .../onboarding/server_connect_screen.dart | 157 ++++++++++++------ 6 files changed, 113 insertions(+), 78 deletions(-) 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), + ], + ), + ), + ), + ); + } +} From b3e3e7d247cd976fc8f68f7a405721a4652f4eb3 Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Mon, 25 May 2026 04:21:09 +0100 Subject: [PATCH 3/6] refactor(enclave): move HTTP from Rust to Dart; Rust keeps pure verification The enclave-client crate bundled two unrelated concerns: HTTP transport (reqwest + tokio) and attestation crypto (COSE_Sign1, X.509, P-384, Schnorr). Two TLS configs, two retry policies, awkward error marshalling across FFI. Split the responsibilities along the natural seam: - crates/enclave-client/ becomes a pure verification library. No reqwest, no tokio, no async. Three pure functions: verify_attestation_doc, extract_app_key_hash, verify_schnorr_signature. Deleted: client.rs, manifest.rs, types.rs (the HTTP-bound pieces). Cargo deps drop from 19 to 14 entries. - FFI surface shrinks from 6 handle-based functions + a tokio runtime to 2 stateless verifier functions (plus enclave_string_free for string returns). No more ClientHandle, no block_on. tokio dropped from ffi/Cargo.toml; hex added for PCR hex-encoding. - app-core/lib/attested_wallet_api.dart drives the protocol from Dart: package:http for transport, in-process attestation cache with TTL + in-flight-verify dedup, X-Attestation-Signature header verified per response. The previous async_enclave.dart isolate wrapper is gone - per-request verify is sub-millisecond CPU work; init-time COSE/X.509 verify is ~10-50ms once per TTL, fine on main isolate. The cache uses package:http's persistent Client for connection-pool reuse. Wall-clock TTL is clamped against backward clock jumps so the UI's ttlRemainingSecs stays sane; the cache self-heals across an enclave key rotation via the retry-on-failed-verify loop. Behavioral changes worth noting: - Missing/failed X-Attestation-Signature is now a hard exception instead of returning the body with signature_verified=false. No consumers used the soft-fail path; net security improvement. - The retry-on-transient closure now catches all exceptions, not just reqwest connect/request errors. Matches the original intent ("attestation warmup, connection reset") more broadly. - No /api/health warmup on init. First request pays one TLS handshake. Co-Authored-By: Claude Opus 4.7 (1M context) --- app-core/lib/attested_wallet_api.dart | 400 +++++-- app-core/lib/enclave/async_enclave.dart | 268 ----- app-core/lib/enclave/native_enclave.dart | 298 +++-- app-core/pubspec.lock | 67 +- crates/enclave-client/Cargo.lock | 1268 +--------------------- crates/enclave-client/Cargo.toml | 11 +- crates/enclave-client/src/client.rs | 276 ----- crates/enclave-client/src/error.rs | 12 - crates/enclave-client/src/lib.rs | 52 +- crates/enclave-client/src/manifest.rs | 113 -- crates/enclave-client/src/nitro.rs | 10 +- crates/enclave-client/src/types.rs | 128 --- crates/enclave-client/src/verify.rs | 225 +--- ffi/Cargo.lock | 919 +--------------- ffi/Cargo.toml | 2 +- ffi/src/enclave/mod.rs | 255 ++--- 16 files changed, 733 insertions(+), 3571 deletions(-) delete mode 100644 app-core/lib/enclave/async_enclave.dart delete mode 100644 crates/enclave-client/src/client.rs delete mode 100644 crates/enclave-client/src/manifest.rs delete mode 100644 crates/enclave-client/src/types.rs 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/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() { From 1604288f0d6f166fda0c990bc31e4f01a73fd73b Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Mon, 25 May 2026 04:21:55 +0100 Subject: [PATCH 4/6] chore(release): branded splash + icon, Play upload signing, app label Replaces the placeholder wallet icon with a small hand-drawn SVG mark (mark-a-hat) on the splash screen, swaps the launcher PNGs across all mipmap densities, and changes the visible app label from "ap" to "merlin". Adds flutter_svg + the assets/logo/ entry to pubspec. Wires release signing in app/android/app/build.gradle gated on app/android/key.properties presence. With key.properties absent, falls back to debug signing so flutter run --release still works locally; with it present, produces a Play-uploadable AAB. The keystore files themselves are gitignored (not committed). Drive-by: print -> debugPrint in signing_screen.dart (lint), auto-generated app/devtools_options.yaml from `flutter pub get`. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/android/app/build.gradle | 29 ++++++++++++++++-- app/android/app/src/main/AndroidManifest.xml | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 2529 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 1580 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 3689 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 7522 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 12548 bytes app/assets/logo/mark-a-hat.svg | 5 +++ app/devtools_options.yaml | 3 ++ app/lib/screens/spending/signing_screen.dart | 4 +-- app/lib/screens/splash_screen.dart | 17 +++------- app/pubspec.yaml | 2 ++ 12 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 app/assets/logo/mark-a-hat.svg create mode 100644 app/devtools_options.yaml 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/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. --> >kO`WoXh8qR*tzu!OK`u1<_{j9a0z1H4;?DedN>fz?3Agdt@ zL68E8NFakqM5dAySS2YD8$iGY5y`F)l&}+m;L8vs0$cDG2#PU*AdWu-S>1x5P3KFR zJZ(TiD$vD=03sMG_~sN4q@zi$j?x^-O)6UY0+{<2f@FM1gacmjqhH@A5^qqpcm0R| z;cnrGnadcnsY=)PAPJv+@aF@29LyZqrGMxcs2Y|LzWT@7ZT6JeuS-azQ$au z?dt}Y`Xg?4T%2qr3lfaVC81$8YE3Ep5IXd7%-M{YYx#E*T&oMN!iOB{GHnPx+wFnVG|nyPs<+2rDl;qI?`aGQ?}Gm*0k*Qj8@7CX5rMfM8Z`2vHkThu;W^V^WpL;(Lz*c>YuD*I4_e2{$B0+^y-MZ)~BM! zt1%^Vh^%8bANF|bat{kz>~~`WGEDHd4&;rO0Eg6=`+~z>j?U-S+cW}tqq;nmSQ*02 zQRlGo$E%;OX4*_C?^jby$G3~3ZTY!!`muOk#@`BrJ&_2%pl*df13hW`;5Uy^t1~!xq(zp&M?j;lUn^o&OMhxe^Y%}1+jnCH zU0clDk6<-r#3*nzpv>aVzDcQu;l}`ar19iL0_7KmVZ84*roTTsl_b4$8)RdgaB`t# z`JK?Y^0G6`ss!}0oXDZim0hfShEc%r4p_PP;5m&)RPn`&IQxs7nJ)k|vkL(S)d9xm zhoXTEhun*3X0|e$85Gtc>V4a1(OjO}9P8z?YgBF3)X2|W+1o8NONm#?h>No`C~66t z98%Sw_BVY3kiJSi6r8+Ret*0wj+466^iM#`T4{gzJW+ zd>y$gGp8c$KAc+su*1XiS3tf*(QaFk)n*UpJeBgx;+2ayt=WNIyfNmf{N@OCfU+um zX#^NN(k&OB0;xUAi245Nz6St$VOaDV#e7vDfC;asslBQNdgv1t zP2tgF%ZspG)0PyAwQ!q{>Yo6#&NE#A6|uYJ$+ww~f;)}oQK2($58;?60h*Zd5k;Bx zQumU&iLyKq+r*20x0EUHObi6HO6~6gsB^s&`E5q)-2T0arU3dN*9mwxf$`~|n2#!r zO#dv~^^?lnB0{_z<27;wpuX)!WXUUD>ioDY5}+4QBFOeljdp#`D=P;bbD{!Sg4_J* z0?%d5ckfEl_mwGu{n~3mz1bNA)TD7DyEa2ii5&hBHlCW7iO5Q8K^%?1m%Xb24&^22 z3Eeo`=*dB%>^X}Jq4)02*5F5jZ7+e1l`_>pka$f%y6+BPu5K*^WQY#BcdvUaE^%i; zfy=s+dIPN|>4vtli%$Bo%(5z;r~Mj#VX|8iYiIym5sSgo4JrZ+r2f4-4Sw_Iy-=qS z^yE?pKYc=iEI16LVjgZkX5YYkJ>C0Y=m^KaT#2$r6u97s^D8rEZ$fqUE7)87*!rQ)Vv0@)dpW81RCC-ctxzVJMfkqEu=WS#`Hj*g?XUgMn7}^q z={_}kS2kHcQnf~9b_ZOz1%;NGvo&NsP!g8r#_qFD3V^*P)~8~7cxt8n_v~^-Hi7F_ zKbR;-F9HBPPJbI_${v%QSZFws+>lc%@XWk;RT?0@f>u zy%;6+7aD*{NRRn+(FOVeg-aSz=1K~_w!y-y!vxviJLn9?xXABlx3f1rhXAI+wEzd5Ic)$xB_Eez-AU!`T2xDveUs9vwS!ah1;8dWPK{lU zbFoW*<2eZ6hxW7t#dn>jzh(nu`qCqQ&_tKYQ#L}m(7Omw8D4n{sQ8W=ea(;6GJH-p zJVj}2Q(tX#@Nw-XB5;~Ux%o~K^G6Wytbczy?GlP_ieSy&?Z@BAgq!LB<5KGIv)=+y zeIx{K&=1)Dt#=WSN(#0!-L5{s_iZ~PrlA~ed*}_JdTdV7OfgrxgG~$N|E;EsHq7qk uw)Iua)+I`Eu{(YLzW=8K`LF&ZuMNe|mJh7+P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..e7f83f939ca1f464b9333d70c0d2e627d8ef0e0d 100644 GIT binary patch literal 1580 zcmZwHc{tQt7zgm*jP)6*03Oa^AxNJJL6Vmge>a9Eo`&vuMTUfDx-Gb_8;uZ@`4;HHyr>nd?c|nE|BqCAWz*Xa+u(ah~5lQL}4^+{MaAUgQ{N66YyYYv`g zX?=@`*tNL8oG^;xXpEu--`=>ah`H{)8r8=+HCY)vq&u&CWn?TQ`hC=ro@54O)4J!C zd3)Rq0H$MOUGd*C9pY1+o}+wld63pWfeFQs{1VFf@7Gr06kH6QZ#~&2`$j;?aIEpkN}`g2fH@dmAf$K&C7i@4BNUW1KNb*JN#d~rN}{YeBrIlp76 zpO9gGG=s01KeNQfgI8-d+}|;?IB;2uI}9Hc-*>MjT$j3F(Xv}de}_<4GI>2aR}u^lH=3AHock^ zYG(`5{S>%gIFc*}3u&bNS7QSAGtSYeq^RFHZoz=b&o4q2ctei9yttjnXaEe$R~%;Z zdMqcn2+16yCxF@XDIN+(e}OQ{$}obN%seFKd&O3syD05`J4MS%@~MalSy-u-WPmZFt!1GCVI_t;y< zZ-_AhV4nEY5hR~|wc#M!PlQx>^}tq z<~Ab#M3Lk3_N6w!g8i%HdriWaQkCrx3fBuK!p~meAqC>9s@+72tI7aA8J2E;aIw%u zMrX81M&rMn74RY(_cIKtWu;hOM|G5Eo literal 442 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 VsdvhWjRg2F(9YTkUx_1L`7gny?YjT~ literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!iOI#yLg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+5i=O3-AeX1=1l$e`s#|#^}+&7(N@w0CIr{$Oe+Uk^K-ZP~83C zcc@hG6rikF&NPT(23>y!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..653eb114a5931a83c73f6f5de971ae54eed88862 100644 GIT binary patch literal 7522 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 KJt|eT4gDVgJcP>t literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb28e45604e46eeda8dd24651419bc0..91b7f605c202536403092f1e1b441219829a4efe 100644 GIT binary patch literal 12548 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 IR<;QJKdz;Zg#Z8m literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/app/assets/logo/mark-a-hat.svg b/app/assets/logo/mark-a-hat.svg new file mode 100644 index 00000000..9f580174 --- /dev/null +++ b/app/assets/logo/mark-a-hat.svg @@ -0,0 +1,5 @@ + + + + + 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/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 931ff206..838c102f 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: # Push notifications: wake the app on receive so it can re-delegate. firebase_core: ^3.6.0 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: From 4ddc35ba459d57806994df559c0800f51e94f022 Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Tue, 26 May 2026 19:23:46 +0100 Subject: [PATCH 5/6] feat(enclave): update supervisor URL from 7073 to 8080 in configuration files --- cosigner-runtime/enclave/enclave.yaml | 2 +- cosigner-runtime/src/config.rs | 2 +- .../src/persistence/enclave_store.rs | 2 +- cosigner-runtime/src/telemetry.rs | 19 +-- infrastructure/mutiny/enclave.yaml | 2 +- infrastructure/mutiny/env.md | 110 ++++++++++++++++++ 6 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 infrastructure/mutiny/env.md diff --git a/cosigner-runtime/enclave/enclave.yaml b/cosigner-runtime/enclave/enclave.yaml index 7e5f85e7..778996ce 100644 --- a/cosigner-runtime/enclave/enclave.yaml +++ b/cosigner-runtime/enclave/enclave.yaml @@ -46,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. 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/infrastructure/mutiny/enclave.yaml b/infrastructure/mutiny/enclave.yaml index 9de4743b..963c45b6 100644 --- a/infrastructure/mutiny/enclave.yaml +++ b/infrastructure/mutiny/enclave.yaml @@ -45,7 +45,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" # Placeholder so the key joins ENCLAVE_APP_ENV_KEYS in the EIF; the # runtime overlays the real JSON from SSM /env/FCM_SERVICE_ACCOUNT_JSON # at boot (populated by tofu env_values, never committed). diff --git a/infrastructure/mutiny/env.md b/infrastructure/mutiny/env.md new file mode 100644 index 00000000..491d1e13 --- /dev/null +++ b/infrastructure/mutiny/env.md @@ -0,0 +1,110 @@ +# 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`). The enclave +runtime overlays SSM values on top of `enclave.yaml`'s `app.env` defaults at +boot, for every key listed in `ENCLAVE_APP_ENV_KEYS` (which the EIF baker +populates from `app.env`). + +## One-time setup + +Keep the source key file **outside** the repo. Default convention: + +``` +~/secrets/vtxos-fcm.json +``` + +The corresponding key in [enclave.yaml](enclave.yaml) `app.env:` must already +exist as an empty placeholder (so it's in `ENCLAVE_APP_ENV_KEYS`): + +```yaml +app: + env: + FCM_SERVICE_ACCOUNT_JSON: "" +``` + +[tofu/env_values.auto.tfvars.json](tofu/.gitignore) is gitignored — it never +gets committed. + +## Generate the tfvars + +From the repo root: + +```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. The `jq -n --arg` form handles the double-escaping of inner +quotes and PEM newlines correctly. + +To add a second value, extend the inner object: + +```bash +jq -n \ + --arg fcm "$(cat ~/secrets/vtxos-fcm.json)" \ + --arg foo "$(cat ~/secrets/other.txt)" \ + '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm, OTHER_KEY: $foo}}' \ + > infrastructure/mutiny/tofu/env_values.auto.tfvars.json +``` + +Every key here must also be declared as an empty placeholder in +`enclave.yaml`'s `app.env`, otherwise the SSM param gets written but the +runtime ignores it. + +## 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 **not** +required for value changes — only for adding new keys (because the new key +needs to join `ENCLAVE_APP_ENV_KEYS`, which is baked into the EIF). + +## 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: +jq -n --arg fcm "$(cat ~/secrets/vtxos-fcm.json)" \ + '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm}}' \ + > infrastructure/mutiny/tofu/env_values.auto.tfvars.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. From 5036c2351bf2b39669fd4deb7111c1377b2a402e Mon Sep 17 00:00:00 2001 From: Joshua Aruokhai Date: Wed, 27 May 2026 06:18:13 +0100 Subject: [PATCH 6/6] feat(enclave): update enclave configuration and environment variables for TLS support and KMS key management --- cosigner-runtime/enclave/enclave.yaml | 15 +- infrastructure/mutiny/enclave.yaml | 21 +- infrastructure/mutiny/env.md | 63 ++- infrastructure/mutiny/tofu/.gitignore | 10 +- infrastructure/mutiny/tofu/main.tf | 35 +- .../tofu/modules/backend/.terraform.lock.hcl | 20 + .../mutiny/tofu/modules/enclave/main.tf | 432 ++++++++---------- .../enclave/templates/user_data.sh.tftpl | 5 +- 8 files changed, 283 insertions(+), 318 deletions(-) create mode 100644 infrastructure/mutiny/tofu/modules/backend/.terraform.lock.hcl diff --git a/cosigner-runtime/enclave/enclave.yaml b/cosigner-runtime/enclave/enclave.yaml index 778996ce..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" diff --git a/infrastructure/mutiny/enclave.yaml b/infrastructure/mutiny/enclave.yaml index 963c45b6..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" @@ -46,10 +47,6 @@ app: ASP_URL: "https://mutinynet.arkade.sh" PERSISTENCE_BACKEND: "enclave" SUPERVISOR_URL: "http://127.0.0.1:8080" - # Placeholder so the key joins ENCLAVE_APP_ENV_KEYS in the EIF; the - # runtime overlays the real JSON from SSM /env/FCM_SERVICE_ACCOUNT_JSON - # at boot (populated by tofu env_values, never committed). - FCM_SERVICE_ACCOUNT_JSON: "" # Secrets managed by KMS inside the enclave. @@ -61,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 index 491d1e13..53aea17b 100644 --- a/infrastructure/mutiny/env.md +++ b/infrastructure/mutiny/env.md @@ -4,10 +4,10 @@ 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`). The enclave -runtime overlays SSM values on top of `enclave.yaml`'s `app.env` defaults at -boot, for every key listed in `ENCLAVE_APP_ENV_KEYS` (which the EIF baker -populates from `app.env`). +(`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 @@ -17,45 +17,38 @@ Keep the source key file **outside** the repo. Default convention: ~/secrets/vtxos-fcm.json ``` -The corresponding key in [enclave.yaml](enclave.yaml) `app.env:` must already -exist as an empty placeholder (so it's in `ENCLAVE_APP_ENV_KEYS`): - -```yaml -app: - env: - FCM_SERVICE_ACCOUNT_JSON: "" -``` - [tofu/env_values.auto.tfvars.json](tofu/.gitignore) is gitignored — it never gets committed. -## Generate the tfvars - -From the repo root: +## Set the value (preferred: enclave CLI) ```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 +enclave tofu env \ + --key FCM_SERVICE_ACCOUNT_JSON \ + --value "$(cat ~/secrets/vtxos-fcm.json)" ``` -The `*.auto.tfvars.json` suffix makes OpenTofu auto-load it; no `-var-file` -flag needed. The `jq -n --arg` form handles the double-escaping of inner -quotes and PEM newlines correctly. +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: -To add a second value, extend the inner object: +```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)" \ - --arg foo "$(cat ~/secrets/other.txt)" \ - '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm, OTHER_KEY: $foo}}' \ +jq -n --arg fcm "$(cat ~/secrets/vtxos-fcm.json)" \ + '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm}}' \ > infrastructure/mutiny/tofu/env_values.auto.tfvars.json ``` -Every key here must also be declared as an empty placeholder in -`enclave.yaml`'s `app.env`, otherwise the SSM param gets written but the -runtime ignores it. +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 @@ -69,9 +62,8 @@ Writes `///env/` per entry, e.g. ## Make the enclave pick it up -Restart the enclave (SSM overrides are read at boot). EIF rebuild is **not** -required for value changes — only for adding new keys (because the new key -needs to join `ENCLAVE_APP_ENV_KEYS`, which is baked into the EIF). +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 @@ -90,9 +82,8 @@ Should print the opening of the JSON, confirming the value is there. ```bash # Refresh the source file from GCP, then: -jq -n --arg fcm "$(cat ~/secrets/vtxos-fcm.json)" \ - '{env_values: {FCM_SERVICE_ACCOUNT_JSON: $fcm}}' \ - > infrastructure/mutiny/tofu/env_values.auto.tfvars.json +enclave tofu env --key FCM_SERVICE_ACCOUNT_JSON \ + --value "$(cat ~/secrets/vtxos-fcm.json)" cd infrastructure/mutiny/tofu && tofu apply # Then restart the enclave. ``` diff --git a/infrastructure/mutiny/tofu/.gitignore b/infrastructure/mutiny/tofu/.gitignore index e7516fa2..3e5d2a48 100644 --- a/infrastructure/mutiny/tofu/.gitignore +++ b/infrastructure/mutiny/tofu/.gitignore @@ -5,10 +5,8 @@ terraform.tfstate terraform.tfstate.backup .terraform/ backend.tf -.terraform.lock.hcl -.artifacts/ +modules/enclave/.signing/ -# Deploy-time env overrides — values pushed to SSM via env_values map. -# Holds the plaintext FCM service-account JSON and any other operator -# secrets that aren't routed through the PCR0-locked secrets: mechanism. -env_values.auto.tfvars.json \ No newline at end of file +# 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