# Backup Service Attestation Data This document defines the attestation data the backup service must submit to the contract for the "Hard Launch" TEE-enabled phase. ## Background ### Current State The backup service (soft launch) registers via [`register_backup_service()`][register-backup-service] with only a TLS public key — no attestation, no TEE verification: ```rust // current soft-launch struct pub struct BackupServiceInfo { pub public_key: Ed25519PublicKey, } ``` MPC nodes, by contrast, already submit full TEE attestation data via [`submit_participant_info()`][submit-participant-info]. The contract verifies the TDX quote, checks the Docker image hash against a whitelist, and stores the result in [`TeeState`][tee-state]. The attestation binds the node's identity to its TEE via [`ReportDataV1`][report-data-v1]: ``` [version: 2 bytes, big endian] || [SHA3-384(tls_public_key || account_public_key): 48 bytes] || [zero padding: 14 bytes] ``` ### Problem For the hard launch, the backup service runs inside a TEE. The contract needs to verify that the backup service is running approved software in a genuine TEE, the same way it already does for MPC nodes. This requires defining what attestation data the backup service submits and how it integrates with the existing [`TeeState`][tee-state] struct. The goal is to share as much TEE-related code with the MPC node as possible. ## Proposal ### `BackupServiceInfo` with Attestation The backup service submits a TLS public key ([already exists][backup-service-info]) and a TDX attestation quote (new): ```rust pub struct BackupServiceInfo { /// Ed25519 public key for mutual TLS authentication with MPC nodes pub public_key: Ed25519PublicKey, /// TDX attestation quote (Dstack variant only — Mock is rejected) pub attestation: Attestation, } ``` The method signature stays `register_backup_service(backup_service_info: BackupServiceInfo)`. The backup service signs this transaction using the account key it generated inside the TEE. The operator retrieves this public key from the backup service's HTTP endpoint (same pattern as MPC nodes with [`GET /public_data`][public-data]) and adds it as an access key to their NEAR account. The transaction is then signed under the operator's `AccountId`. The contract rejects `Mock` attestations — only `Dstack` is accepted. A separate method from [`submit_participant_info()`][submit-participant-info] is needed because `submit_participant_info()` writes to `self.tee_state` (the MPC node [`TeeState`][tee-state]), while the backup service writes to `self.node_migrations.backup_service_tee_state` (a separate `TeeState` instance with its own image hash whitelist). The contract derives `account_id` from `env::predecessor_account_id()` and `account_public_key` from `env::signer_account_pk()`, same as for MPC nodes. ### ReportData The backup service uses the same [`ReportDataV1`][report-data-v1] format as MPC nodes. The `ReportData` binds both keys the backup service generates inside the TEE to the attestation: - **`tls_public_key`**: MPC nodes look up the backup service's TLS public key from the contract to verify the mTLS peer before keyshare transfer. Binding it to the attestation proves the key was generated inside the TEE — an attacker cannot register a TLS key generated outside. - **`account_public_key`** (derived on-chain via `env::signer_account_pk()`): The contract verifies the transaction signer matches the key in `ReportData`. This proves the transaction signer is the same entity running inside the TEE — an attacker cannot submit an attestation generated by one TEE under a different account. ### Verification The contract verifies the attestation using existing [`TeeState`][tee-state] verification logic: 1. Quote validity via DCAP attestation provider 2. Docker image hash against the backup service allowed list 3. Launcher compose hash against the backup service allowed list 4. Timestamp within deadline 5. `ReportData` matches `SHA3-384(tls_public_key || account_public_key)` 6. Transaction signer's public key matches `account_public_key` via `env::signer_account_pk()` Once verified, the attestation is stored in a new field on [`NodeMigrations`][node-migrations]: ```rust pub struct NodeMigrations { ongoing_migrations: IterableMap, pub backup_service_tee_state: TeeState, // replaces backup_services_info } ``` `backup_services_info` is removed — `TeeState` already stores the TLS key inside `NodeId`. The MPC node reads the backup service's TLS key from `backup_service_tee_state` instead. The attestation uses the same [`NodeId`][node-id] struct as MPC nodes: ```rust NodeId { account_id: env::predecessor_account_id(), tls_public_key, account_public_key: Some(account_public_key), } ``` The backup service persists its TLS and account keys to dstack's encrypted disk, same as MPC nodes do with [`PersistentSecrets`][persistent-secrets]. The TLS key is stable across restarts, so [`TeeState`][tee-state]'s existing lookup by TLS public key works the same way as for MPC nodes. ### Periodic Resubmission The backup service resubmits its attestation every 7 days with a fresh TDX quote using the same keys. [`add_participant()`][add-participant] overwrites the existing entry for the same TLS key. ## End-to-End Flows The following walkthroughs show how the attestation data defined above is used in the backup and recovery workflows. ### Backup Flow **Precondition**: MPC participants have voted for a backup service Docker image hash via `vote_backup_service_code_hash()`. **On boot (one-time):** 1. Operator starts the backup service CVM. Dstack boots, extends RTMR3 with the launcher compose hash. Launcher verifies the image hash, extends RTMR3, starts the container. On first boot, the launcher uses `DEFAULT_IMAGE_DIGEST` compiled into it (same pattern as MPC node launcher). On subsequent boots, `TeeContext` will have written updated hashes to disk. 2. Backup service generates an Ed25519 TLS keypair and a NEAR account keypair inside the TEE. 3. Operator retrieves the account public key from the backup service's HTTP endpoint and adds it as an access key to their NEAR account (same as for MPC nodes — see [securing-mpc-with-tee §Bootstrapping](./securing-mpc-with-tee-design-doc.md#bootstrapping)). **Attestation registration (periodic — every 7 days, starting on first boot):** 4. Backup service constructs [`ReportDataV1::new(tls_pk_bytes, account_pk_bytes)`][report-data-v1] and generates a fresh TDX quote. 5. Backup service calls `register_backup_service(BackupServiceInfo { public_key, attestation })`. The contract verifies the attestation and stores/overwrites the entry in `backup_service_tee_state`. **Keyshare monitoring (continuous):** 6. Backup service polls the contract for missing keyshares. 7. MPC node looks up the backup service's TLS key and attestation validity from `backup_service_tee_state` (requires a view method that maps `AccountId` to backup service attestation — see [Required Changes](#contract-methods)), then sends encrypted keyshares over mTLS. **On restart:** 8. TLS and account keys survive the restart (persisted to encrypted disk). The backup service resubmits its attestation (step 4-5) with the same keys. Keyshares are kept in memory only and must be re-fetched from MPC nodes (step 6-7). ### Recovery Flow **Precondition**: Backup service has keyshares in memory. The operator wants to migrate to a new MPC node (e.g., hardware failure, or moving to a different TEE). 1. **Operator trigger (manual)**: Operator calls `start_node_migration(destination_node_info)`. 2. **Detection (automated)**: Backup service detects the migration event on-chain. 3. **Transfer**: Backup service connects to the new node via mTLS, sends encrypted keyshares. 4. **Verification**: New node queries the contract to verify the backup service's attestation before saving keyshares. 5. **Finalization**: New node calls `conclude_node_migration(keyset)`. ## Required Changes ### Contract Methods The [migration service `impl` block][migration-service-impl] needs: - **[`register_backup_service()`][register-backup-service]**: Accept extended `BackupServiceInfo`. Write to `backup_service_tee_state` via [`add_participant()`][add-participant]. Remove [`backup_services_info`][backup-services-info-field] (replaced by `backup_service_tee_state`). - **[`migration_info()`][migration-info]**: Replace with a view method that returns the backup service's TLS key and attestation validity from `backup_service_tee_state` given an `AccountId`. The MPC node needs this to look up its operator's backup service (since `TeeState` is keyed by TLS public key, not `AccountId`). - **New methods** (per [migration-service.md §Backup Service TEE methods](./migration-service.md#backup-service-tee-methods)): - `vote_backup_service_code_hash(code_hash)` - `allowed_backup_service_code_hashes()` - `allowed_backup_service_launcher_compose_hashes()` - `reverify_backup_services()` - **Resharing cleanup**: [`clean_non_participants()`][clean-non-participants] should also clean `backup_service_tee_state` when operators leave the participant set. - **`start_node_migration()`** and **`conclude_node_migration()`**: No attestation-related changes. ### Attestation Verification [`Attestation::verify()`][attestation-verify] cannot be reused as-is: - It extracts the Docker image hash using a [hardcoded `"mpc-image-digest"` event name][mpc-image-hash-event-usage], which the backup service launcher would not use. - [`verify_measurements()`][verify-measurements] rejects an empty allowed measurements list — for MPC nodes these were [seeded during a contract migration][seed-measurements], and the backup service `TeeState` should be seeded the same way. ### TeeContext [`TeeContext`](./tee-context-design.md) hardcodes MPC node method names and needs parameterization: | What | Current | Backup service needs | |------|---------|---------------------| | Submit attestation | `submit_participant_info` | `register_backup_service` | | Poll image hashes | `allowed_docker_image_hashes` | `allowed_backup_service_code_hashes` | | Poll compose hashes | `allowed_launcher_compose_hashes` | `allowed_backup_service_launcher_compose_hashes` | [register-backup-service]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/lib.rs#L2027 [submit-participant-info]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/lib.rs#L830 [migration-service-impl]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/lib.rs#L2009-L2011 [migration-info]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/lib.rs#L2012 [tee-state]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/tee/tee_state.rs#L103 [node-id]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/tee/tee_state.rs#L27 [add-participant]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/tee/tee_state.rs#L154 [clean-non-participants]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/tee/tee_state.rs#L407 [node-migrations]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/node_migrations.rs#L11 [backup-service-info]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/node_migrations.rs#L91 [backup-services-info-field]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/node_migrations.rs#L12 [report-data-v1]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/mpc-attestation/src/report_data.rs#L34 [attestation-verify]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/mpc-attestation/src/attestation.rs#L133 [verify-measurements]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/mpc-attestation/src/attestation.rs#L260 [mpc-image-hash-event-usage]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/mpc-attestation/src/attestation.rs#L147 [seed-measurements]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/contract/src/v3_7_0_state.rs#L76-L83 [persistent-secrets]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/node/src/config.rs#L407 [public-data]: https://github.com/near/mpc/blob/249bd1e5a33610299940d802d0c9e7d48a6b8acb/crates/node/src/web.rs#L443