diff --git a/CHANGELOG.md b/CHANGELOG.md index 15494e041..cdaec0ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ runtime - Test CLI command to retrieve quote and change endpoint / TSS account in one command ([#1198](https://github.com/entropyxyz/entropy-core/pull/1198)) - On-chain unresponsiveness reporting [(#1215)](https://github.com/entropyxyz/entropy-core/pull/1215) - Add cli options for adding validator [(#1242)](https://github.com/entropyxyz/entropy-core/pull/1242) +- Database encryption key backup / recovery feature for entropy-tss [(#1249)](https://github.com/entropyxyz/entropy-core/pull/1249) ### Changed - Use correct key rotation endpoint in OCW ([#1104](https://github.com/entropyxyz/entropy-core/pull/1104)) diff --git a/Cargo.lock b/Cargo.lock index d52c742e6..b75201920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,6 +2753,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "subxt", + "tdx-quote", ] [[package]] diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f93b3cb84..d795db175 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -50,7 +50,7 @@ pub use crate::{ errors::{ClientError, SubstrateError}, }; pub use entropy_protocol::{sign_and_encrypt::EncryptedSignedMessage, KeyParams}; -pub use entropy_shared::{HashingAlgorithm, QuoteContext}; +pub use entropy_shared::{attestation::QuoteContext, HashingAlgorithm}; use parity_scale_codec::Decode; use rand::Rng; use std::str::FromStr; diff --git a/crates/client/src/tests.rs b/crates/client/src/tests.rs index a4633cb39..6560201fd 100644 --- a/crates/client/src/tests.rs +++ b/crates/client/src/tests.rs @@ -19,7 +19,7 @@ use crate::{ update_programs, }; -use entropy_shared::{QuoteContext, QuoteInputData}; +use entropy_shared::attestation::{QuoteContext, QuoteInputData}; use entropy_testing_utils::{ constants::{TEST_PROGRAM_WASM_BYTECODE, TSS_ACCOUNTS, X25519_PUBLIC_KEYS}, helpers::{encode_verifying_key, spawn_tss_nodes_and_start_chain}, @@ -129,7 +129,7 @@ async fn test_change_threshold_accounts() { let encoded_pck = encode_verifying_key(&pck.verifying_key()).unwrap().to_vec(); let quote = { - let input_data = entropy_shared::QuoteInputData::new( + let input_data = QuoteInputData::new( tss_public_key, *x25519_public_key.as_bytes(), nonce, @@ -368,7 +368,7 @@ async fn test_set_session_key_and_declare_validate() { let encoded_pck = encode_verifying_key(&pck.verifying_key()).unwrap().to_vec(); let quote = { - let input_data = entropy_shared::QuoteInputData::new( + let input_data = QuoteInputData::new( tss_public_key, *x25519_public_key.as_bytes(), nonce, diff --git a/crates/kvdb/src/encrypted_sled/constants.rs b/crates/kvdb/src/encrypted_sled/constants.rs index 335f0131e..bf13e969a 100644 --- a/crates/kvdb/src/encrypted_sled/constants.rs +++ b/crates/kvdb/src/encrypted_sled/constants.rs @@ -16,5 +16,3 @@ //! Constants for [encrypted_sled](crate::encrypted_sled) pub(super) const PASSWORD_VERIFICATION_KEY: &str = "verification_key"; pub(super) const PASSWORD_VERIFICATION_VALUE: &str = "verification_value"; -pub(super) const PASSWORD_SALT_KEY: &[u8] = b"password_salt_key"; -pub(super) const UNSAFE_PASSWORD: &str = "entropy_unsafe_password"; diff --git a/crates/kvdb/src/encrypted_sled/kv.rs b/crates/kvdb/src/encrypted_sled/kv.rs index 4672ad430..204223a99 100644 --- a/crates/kvdb/src/encrypted_sled/kv.rs +++ b/crates/kvdb/src/encrypted_sled/kv.rs @@ -19,8 +19,6 @@ //! to be inserted, forming a [EncryptedRecord]:. The nonce is later //! used to decrypt and retrieve the originally inserted value. -use std::convert::TryInto; - use chacha20poly1305::{ self, aead::{AeadInPlace, NewAead}, @@ -32,7 +30,6 @@ use zeroize::Zeroize; use super::{ constants::*, - password::{Password, PasswordSalt}, record::EncryptedRecord, result::{EncryptedDbError::*, EncryptedDbResult}, }; @@ -48,26 +45,13 @@ impl EncryptedDb { /// Creates an XChaCha20 stream cipher from a password-based-key-derivation-function and /// verifies that the password is valid. /// See [super::Password] for more info on pdkdf. - pub fn open

(db_name: P, password: Password) -> EncryptedDbResult + pub fn open

(db_name: P, mut key: [u8; 32]) -> EncryptedDbResult where P: AsRef, { let kv = sled::open(db_name).map_err(CorruptedKv)?; - let password_salt: PasswordSalt = if kv.was_recovered() { - // existing kv: get the existing password salt - kv.get(PASSWORD_SALT_KEY)?.ok_or(MissingPasswordSalt)?.try_into()? - } else { - // new kv: choose a new password salt and store it - let mut password_salt = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut password_salt); - kv.insert(PASSWORD_SALT_KEY, &password_salt)?; - password_salt.into() - }; - - // zeroize key since we are no longer using it after creating cipher - let mut key = Self::chacha20poly1305_kdf(password, password_salt)?; - let cipher = XChaCha20Poly1305::new(&key); + let cipher = XChaCha20Poly1305::new(&key.into()); key.zeroize(); let encrypted_db = EncryptedDb { kv, cipher }; @@ -84,23 +68,6 @@ impl EncryptedDb { Ok(encrypted_db) } - fn chacha20poly1305_kdf( - password: Password, - salt: PasswordSalt, - ) -> EncryptedDbResult { - let mut output = chacha20poly1305::Key::default(); - - // default params: log_n = 15, r = 8, p = 1 - scrypt::scrypt( - password.as_ref(), - salt.as_ref(), - &scrypt::Params::default(), - output.as_mut_slice(), - )?; - - Ok(output) - } - /// get a new random nonce to use for value encryption using [rand::thread_rng] fn generate_nonce() -> chacha20poly1305::XNonce { let mut bytes = chacha20poly1305::XNonce::default(); diff --git a/crates/kvdb/src/encrypted_sled/mod.rs b/crates/kvdb/src/encrypted_sled/mod.rs index 303d47be3..10a8a2315 100644 --- a/crates/kvdb/src/encrypted_sled/mod.rs +++ b/crates/kvdb/src/encrypted_sled/mod.rs @@ -20,17 +20,12 @@ mod constants; mod kv; -mod password; mod record; mod result; // match the API of sled pub use kv::EncryptedDb as Db; -pub use password::{Password, PasswordMethod, PasswordSalt}; pub use result::{EncryptedDbError as Error, EncryptedDbResult as Result}; #[cfg(test)] mod tests; - -#[cfg(test)] -pub use tests::get_test_password; diff --git a/crates/kvdb/src/encrypted_sled/password.rs b/crates/kvdb/src/encrypted_sled/password.rs deleted file mode 100644 index 3b5a907f0..000000000 --- a/crates/kvdb/src/encrypted_sled/password.rs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (C) 2023 Entropy Cryptography Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//! Handles the generation of a key for the stream cipher from the user's password using [scrypt] -//! pbkdf. -use std::convert::{TryFrom, TryInto}; - -use sled::IVec; -use zeroize::Zeroize; - -use super::{constants::UNSAFE_PASSWORD, result::EncryptedDbResult}; - -/// Safely store strings -// TODO use https://docs.rs/secrecy ? -#[derive(Zeroize, Clone)] -#[zeroize(drop)] -pub struct Password(String); - -impl AsRef<[u8]> for Password { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl From for Password { - fn from(string: String) -> Self { - Self(string) - } -} - -pub struct PasswordSalt([u8; 32]); - -impl AsRef<[u8]> for PasswordSalt { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl From<[u8; 32]> for PasswordSalt { - fn from(bytes: [u8; 32]) -> Self { - Self(bytes) - } -} - -impl TryFrom for PasswordSalt { - type Error = std::array::TryFromSliceError; - - fn try_from(value: IVec) -> Result { - Ok(Self(value.as_ref().try_into()?)) - } -} - -use rpassword::read_password; - -/// Specifies how [Password] will be retrieved -#[derive(Clone, Debug)] -pub enum PasswordMethod { - NoPassword, - Prompt, -} -impl PasswordMethod { - /// Execute the password method to retrieve a password - pub fn execute(&self) -> EncryptedDbResult { - Ok(match self { - Self::NoPassword => Password(UNSAFE_PASSWORD.to_string()), - Self::Prompt => { - println!("Please type your password:"); - Password(read_password()?) - }, - }) - } -} - -#[cfg(test)] -impl From<&str> for Password { - fn from(value: &str) -> Self { - Self(value.to_string()) - } -} diff --git a/crates/kvdb/src/encrypted_sled/tests.rs b/crates/kvdb/src/encrypted_sled/tests.rs index d546f7d0c..c6f2c2d2d 100644 --- a/crates/kvdb/src/encrypted_sled/tests.rs +++ b/crates/kvdb/src/encrypted_sled/tests.rs @@ -15,23 +15,17 @@ use serial_test::serial; -use super::{kv::EncryptedDb, Password}; +use super::kv::EncryptedDb; use crate::{clean_tests, encrypted_sled::Db, get_db_path}; -fn setup_db(require_password: bool) -> Db { - let db = if !require_password { - EncryptedDb::open(get_db_path(true), get_test_password()) - } else { - EncryptedDb::open(get_db_path(true), Password::from("super-secret password.")) - }; - assert!(db.is_ok()); - db.unwrap() +fn setup_db(key: [u8; 32]) -> Db { + EncryptedDb::open(get_db_path(true), key).unwrap() } #[test] #[serial] fn test_encrypted_sled() { - let db = setup_db(false); + let db = setup_db([1; 32]); // insert -> returns None let res = db.insert("key", "value").unwrap(); @@ -73,28 +67,25 @@ fn test_encrypted_sled() { #[test] #[serial] -fn test_use_existing_salt() { - let db = setup_db(false); +fn test_use_existing_key() { + let db = setup_db([1; 32]); let db_path = get_db_path(true); drop(db); // open existing db - assert!(EncryptedDb::open(db_path, get_test_password()).is_ok()); + assert!(EncryptedDb::open(db_path, [1; 32]).is_ok()); clean_tests(); } #[test] #[serial] -fn test_password() { - let db = setup_db(true); +fn test_key() { + let db = setup_db([1; 32]); let db_path = get_db_path(true); drop(db); - // try to open the kv store using a different password - let db = EncryptedDb::open( - db_path, - Password::from("super-secret password!"), // replace '.' with '!' - ); + // try to open the kv store using a different key + let db = EncryptedDb::open(db_path, [2; 32]); assert!(matches!(db, Err(super::result::EncryptedDbError::WrongPassword))); clean_tests(); } @@ -102,7 +93,7 @@ fn test_password() { #[test] #[serial] fn test_large_input() { - let db = setup_db(false); + let db = setup_db([1; 32]); let large_value = vec![0; 100000]; let res = db.insert("key", large_value.clone()).unwrap(); @@ -112,7 +103,3 @@ fn test_large_input() { assert_eq!(res, Some(sled::IVec::from(large_value))); clean_tests(); } - -pub fn get_test_password() -> Password { - crate::encrypted_sled::PasswordMethod::NoPassword.execute().unwrap() -} diff --git a/crates/kvdb/src/kv_manager/kv.rs b/crates/kvdb/src/kv_manager/kv.rs index bc3a0aa4a..8e04c75c7 100644 --- a/crates/kvdb/src/kv_manager/kv.rs +++ b/crates/kvdb/src/kv_manager/kv.rs @@ -29,7 +29,7 @@ use super::{ KeyReservation, DEFAULT_KV_NAME, DEFAULT_KV_PATH, }, }; -use crate::encrypted_sled::{self, Password}; +use crate::encrypted_sled; #[derive(Clone)] pub struct Kv { @@ -44,22 +44,22 @@ where { /// Creates a new kv service. Returns [InitErr] on failure. /// the path of the kvstore is `root_path` + "/kvstore/" + `kv_name` - pub fn new(root_path: PathBuf, password: Password) -> KvResult { + pub fn new(root_path: PathBuf, key: [u8; 32]) -> KvResult { let kv_path = root_path.join(DEFAULT_KV_PATH).join(DEFAULT_KV_NAME); // use to_string_lossy() instead of to_str() to avoid handling Option<&str> let kv_path = kv_path.to_string_lossy().to_string(); - Self::with_db_name(kv_path, password) + Self::with_db_name(kv_path, key) } /// Creates a kvstore at `full_db_name` and spawns a new kv_manager. Returns [InitErr] on /// failure. `full_db_name` is the name of the path of the kvstrore + its name /// Example: ~/entropy/kvstore/database_1 - pub fn with_db_name(full_db_name: String, password: Password) -> KvResult { + pub fn with_db_name(full_db_name: String, encryption_key: [u8; 32]) -> KvResult { let (sender, rx) = mpsc::unbounded_channel(); // get kv store from db name before entering the kv_cmd_handler because // it's more convenient to return an error from outside of a tokio::span - let kv = get_kv_store(&full_db_name, password)?; + let kv = get_kv_store(&full_db_name, encryption_key)?; tokio::spawn(kv_cmd_handler(rx, kv)); Ok(Self { sender }) @@ -129,13 +129,10 @@ where /// let my_db = get_kv_store(&"my_current_dir_db")?; /// let my_db = get_kv_store(&"/tmp/my_tmp_bd")?; #[tracing::instrument(skip_all, fields(db_name))] -pub fn get_kv_store( - db_name: &str, - password: Password, -) -> encrypted_sled::Result { +pub fn get_kv_store(db_name: &str, key: [u8; 32]) -> encrypted_sled::Result { // create/open DB tracing::debug!("Decrypting KV store"); - let kv = encrypted_sled::Db::open(db_name, password)?; + let kv = encrypted_sled::Db::open(db_name, key)?; // log whether the DB was newly created or not if kv.was_recovered() { diff --git a/crates/kvdb/src/kv_manager/tests.rs b/crates/kvdb/src/kv_manager/tests.rs index 7251ab711..2d264b9ee 100644 --- a/crates/kvdb/src/kv_manager/tests.rs +++ b/crates/kvdb/src/kv_manager/tests.rs @@ -30,18 +30,19 @@ use super::{ }; use crate::{ clean_tests, - encrypted_sled::{get_test_password, Db, Result}, + encrypted_sled::{Db, Result}, get_db_path, }; -pub fn open_with_test_password() -> Result { - Db::open(get_db_path(true), get_test_password()) +pub fn open_with_test_key() -> Result { + Db::open(get_db_path(true), [1; 32]) } #[test] #[serial] fn reserve_success() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); + let key: String = "key".to_string(); assert_eq!(handle_reserve(&kv, key.clone()).unwrap(), KeyReservation { key: key.clone() }); @@ -57,7 +58,7 @@ fn reserve_success() { #[test] #[serial] fn reserve_failure() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); handle_reserve(&kv, key.clone()).unwrap(); @@ -70,7 +71,7 @@ fn reserve_failure() { #[test] #[serial] fn put_success() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); handle_reserve(&kv, key.clone()).unwrap(); @@ -84,7 +85,7 @@ fn put_success() { #[test] #[serial] fn put_failure_no_reservation() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); @@ -101,7 +102,7 @@ fn put_failure_no_reservation() { #[test] #[serial] fn put_failure_put_twice() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); let value = "value".to_string(); @@ -127,7 +128,7 @@ fn put_failure_put_twice() { #[test] #[serial] fn get_success() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); let value = "value"; @@ -144,7 +145,7 @@ fn get_success() { #[test] #[serial] fn get_failure() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); let err = handle_get::(&kv, key).err().unwrap(); @@ -156,7 +157,7 @@ fn get_failure() { #[test] #[serial] fn test_exists() { - let kv = open_with_test_password().unwrap(); + let kv = open_with_test_key().unwrap(); let key: String = "key".to_string(); let value: String = "value".to_string(); diff --git a/crates/kvdb/src/kv_manager/value.rs b/crates/kvdb/src/kv_manager/value.rs index 7c84edc1f..860a9c86d 100644 --- a/crates/kvdb/src/kv_manager/value.rs +++ b/crates/kvdb/src/kv_manager/value.rs @@ -27,7 +27,6 @@ use super::{ helpers::{deserialize, serialize}, kv::Kv, }; -use crate::encrypted_sled::Password; /// Mnemonic type needs to be known globaly to create/access the mnemonic kv store #[derive(Zeroize, Debug, Clone, Serialize, Deserialize)] @@ -56,16 +55,24 @@ impl fmt::Debug for PartyInfo { #[derive(Clone)] pub struct KvManager { kv: Kv, + storage_path: PathBuf, } impl KvManager { - pub fn new(root: PathBuf, password: Password) -> KvResult { - Ok(KvManager { kv: Kv::::new(root, password)? }) + pub fn new(storage_path: PathBuf, encryption_key: [u8; 32]) -> KvResult { + Ok(KvManager { + kv: Kv::::new(storage_path.clone(), encryption_key)?, + storage_path, + }) } pub fn kv(&self) -> &Kv { &self.kv } + + pub fn storage_path(&self) -> &PathBuf { + &self.storage_path + } } /// Value type stored in the kv-store diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 10cd726f5..40e39bee8 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -23,6 +23,7 @@ lazy_static={ version="1.5.0", features=["spin_no_std"] } hex-literal="0.4.1" sp-core ={ version="29.0.0", default-features=false } subxt ={ version="0.35.3", default-features=false, optional=true } +tdx-quote ="0.0.3" [features] default =["std"] @@ -31,3 +32,5 @@ wasm =["codec/std", "scale-info/std", "serde/std", "sp-std/std"] wasm-no-std=["sp-runtime"] user-native=["dep:subxt", "subxt/native"] user-wasm =["dep:subxt", "subxt/web"] +# Enables non-mock TDX quote verification +production=[] diff --git a/crates/shared/src/attestation.rs b/crates/shared/src/attestation.rs new file mode 100644 index 000000000..43d8870cc --- /dev/null +++ b/crates/shared/src/attestation.rs @@ -0,0 +1,198 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +//! TDX attestion related shared types and functions + +use crate::X25519PublicKey; +use blake2::{Blake2b512, Digest}; +use codec::{Decode, Encode}; + +/// Input data to be included in a TDX attestation +pub struct QuoteInputData(pub [u8; 64]); + +impl QuoteInputData { + pub fn new( + tss_account_id: T, + x25519_public_key: X25519PublicKey, + nonce: [u8; 32], + context: QuoteContext, + ) -> Self { + let mut hasher = Blake2b512::new(); + hasher.update(tss_account_id.encode()); + hasher.update(x25519_public_key); + hasher.update(nonce); + hasher.update(context.encode()); + Self(hasher.finalize().into()) + } +} + +/// An indicator as to the context in which a quote is intended to be used +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum QuoteContext { + /// To be used in the `validate` extrinsic + Validate, + /// To be used in the `change_endpoint` extrinsic + ChangeEndpoint, + /// To be used in the `change_threshold_accounts` extrinsic + ChangeThresholdAccounts, + /// To be used when requesting to recover an encryption key + EncryptionKeyRecoveryRequest, +} + +#[cfg(feature = "std")] +impl std::fmt::Display for QuoteContext { + /// Custom display implementation so that it can be used to build a query string + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + QuoteContext::Validate => write!(f, "validate"), + QuoteContext::ChangeEndpoint => write!(f, "change_endpoint"), + QuoteContext::ChangeThresholdAccounts => write!(f, "change_threshold_accounts"), + QuoteContext::EncryptionKeyRecoveryRequest => { + write!(f, "encryption_key_recovery_request") + }, + } + } +} + +#[cfg(feature = "wasm-no-std")] +use sp_std::vec::Vec; + +/// A trait for types which can handle attestation requests. +#[cfg(not(feature = "wasm"))] +pub trait AttestationHandler { + /// Verify that the given quote is valid and matches the given information about the attestee. + /// The Provisioning Certification Key (PCK) certifcate chain is extracted from the quote and + /// verified. If successful, the PCK public key used to sign the quote is returned. + fn verify_quote( + attestee: &AccountId, + x25519_public_key: X25519PublicKey, + quote: Vec, + context: QuoteContext, + ) -> Result; + + /// Indicate to the attestation handler that a quote is desired. + /// + /// The `nonce` should be a piece of data (e.g a random number) which indicates that the quote + /// is reasonably fresh and has not been reused. + fn request_quote(attestee: &AccountId, nonce: [u8; 32]); +} + +/// A convenience implementation for testing and benchmarking. +#[cfg(not(feature = "wasm"))] +impl AttestationHandler for () { + fn verify_quote( + _attestee: &AccountId, + _x25519_public_key: X25519PublicKey, + _quote: Vec, + _context: QuoteContext, + ) -> Result { + Ok(crate::BoundedVecEncodedVerifyingKey::try_from([0; 33].to_vec()).unwrap()) + } + + fn request_quote(_attestee: &AccountId, _nonce: [u8; 32]) {} +} + +/// An error when verifying a quote +#[cfg(not(feature = "wasm"))] +#[derive(Debug, Eq, PartialEq)] +pub enum VerifyQuoteError { + /// Quote could not be parsed or verified + BadQuote, + /// Attestation extrinsic submitted when not requested + UnexpectedAttestation, + /// Hashed input data does not match what was expected + IncorrectInputData, + /// Unacceptable VM image running + BadMrtdValue, + /// Cannot encode verifying key (PCK) + CannotEncodeVerifyingKey, + /// Cannot decode verifying key (PCK) + CannotDecodeVerifyingKey, + /// PCK certificate chain cannot be parsed + PckCertificateParse, + /// PCK certificate chain cannot be verified + PckCertificateVerify, + /// PCK certificate chain public key is not well formed + PckCertificateBadPublicKey, + /// Pck certificate could not be extracted from quote + PckCertificateNoCertificate, +} + +#[cfg(feature = "std")] +impl std::fmt::Display for VerifyQuoteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VerifyQuoteError::BadQuote => write!(f, "Quote could not be parsed of verified"), + VerifyQuoteError::UnexpectedAttestation => { + write!(f, "Attestation extrinsic submitted when not requested") + }, + VerifyQuoteError::IncorrectInputData => { + write!(f, "Hashed input data does not match what was expected") + }, + VerifyQuoteError::BadMrtdValue => write!(f, "Unacceptable VM image running"), + VerifyQuoteError::CannotEncodeVerifyingKey => { + write!(f, "Cannot encode verifying key (PCK)") + }, + VerifyQuoteError::CannotDecodeVerifyingKey => { + write!(f, "Cannot decode verifying key (PCK)") + }, + VerifyQuoteError::PckCertificateParse => { + write!(f, "PCK certificate chain cannot be parsed") + }, + VerifyQuoteError::PckCertificateVerify => { + write!(f, "PCK certificate chain cannot be verified") + }, + VerifyQuoteError::PckCertificateBadPublicKey => { + write!(f, "PCK certificate chain public key is not well formed") + }, + VerifyQuoteError::PckCertificateNoCertificate => { + write!(f, "PCK certificate could not be extracted from quote") + }, + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifyQuoteError {} + +/// Verify a PCK certificate chain from a quote in production +#[cfg(feature = "production")] +pub fn verify_pck_certificate_chain( + quote: &tdx_quote::Quote, +) -> Result { + quote.verify().map_err(|_| VerifyQuoteError::PckCertificateVerify) +} + +/// A mock version of verifying the PCK certificate chain. +/// When generating mock quotes, we just put the encoded PCK in place of the certificate chain +/// so this function just decodes it, checks it was used to sign the quote, and returns it +#[cfg(not(any(feature = "production", feature = "wasm")))] +pub fn verify_pck_certificate_chain( + quote: &tdx_quote::Quote, +) -> Result { + let provisioning_certification_key = + quote.pck_cert_chain().map_err(|_| VerifyQuoteError::PckCertificateNoCertificate)?; + let provisioning_certification_key = tdx_quote::decode_verifying_key( + &provisioning_certification_key + .try_into() + .map_err(|_| VerifyQuoteError::CannotDecodeVerifyingKey)?, + ) + .map_err(|_| VerifyQuoteError::CannotDecodeVerifyingKey)?; + + quote + .verify_with_pck(&provisioning_certification_key) + .map_err(|_| VerifyQuoteError::PckCertificateVerify)?; + Ok(provisioning_certification_key) +} diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 30fb9f855..613dc8d8e 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -20,6 +20,7 @@ //! This helps ensures those structs are synced among clients and nodes. pub use constants::*; pub use types::*; +pub mod attestation; pub mod constants; pub mod types; diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index 7bada801c..8bb4fbd74 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -12,9 +12,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -#![allow(dead_code)] use super::constants::VERIFICATION_KEY_LENGTH; -use blake2::{Blake2b512, Digest}; #[cfg(not(feature = "wasm"))] use codec::alloc::vec::Vec; use codec::{Decode, Encode}; @@ -114,107 +112,3 @@ pub type EncodedVerifyingKey = [u8; VERIFICATION_KEY_LENGTH as usize]; #[cfg(not(feature = "wasm"))] pub type BoundedVecEncodedVerifyingKey = sp_runtime::BoundedVec>; - -/// Input data to be included in a TDX attestation -pub struct QuoteInputData(pub [u8; 64]); - -impl QuoteInputData { - pub fn new( - tss_account_id: T, - x25519_public_key: X25519PublicKey, - nonce: [u8; 32], - context: QuoteContext, - ) -> Self { - let mut hasher = Blake2b512::new(); - hasher.update(tss_account_id.encode()); - hasher.update(x25519_public_key); - hasher.update(nonce); - hasher.update(context.encode()); - Self(hasher.finalize().into()) - } -} - -/// An indicator as to the context in which a quote is intended to be used -#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum QuoteContext { - /// To be used in the `validate` extrinsic - Validate, - /// To be used in the `change_endpoint` extrinsic - ChangeEndpoint, - /// To be used in the `change_threshold_accounts` extrinsic - ChangeThresholdAccounts, -} - -#[cfg(feature = "std")] -impl std::fmt::Display for QuoteContext { - /// Custom display implementation so that it can be used to build a query string - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - QuoteContext::Validate => write!(f, "validate"), - QuoteContext::ChangeEndpoint => write!(f, "change_endpoint"), - QuoteContext::ChangeThresholdAccounts => write!(f, "change_threshold_accounts"), - } - } -} - -/// A trait for types which can handle attestation requests. -#[cfg(not(feature = "wasm"))] -pub trait AttestationHandler { - /// Verify that the given quote is valid and matches the given information about the attestee. - /// The Provisioning Certification Key (PCK) certifcate chain is extracted from the quote and - /// verified. If successful, the PCK public key used to sign the quote is returned. - fn verify_quote( - attestee: &AccountId, - x25519_public_key: X25519PublicKey, - quote: Vec, - context: QuoteContext, - ) -> Result; - - /// Indicate to the attestation handler that a quote is desired. - /// - /// The `nonce` should be a piece of data (e.g a random number) which indicates that the quote - /// is reasonably fresh and has not been reused. - fn request_quote(attestee: &AccountId, nonce: [u8; 32]); -} - -/// A convenience implementation for testing and benchmarking. -#[cfg(not(feature = "wasm"))] -impl AttestationHandler for () { - fn verify_quote( - _attestee: &AccountId, - _x25519_public_key: X25519PublicKey, - _quote: Vec, - _context: QuoteContext, - ) -> Result { - Ok(BoundedVecEncodedVerifyingKey::try_from([0; 33].to_vec()).unwrap()) - } - - fn request_quote(_attestee: &AccountId, _nonce: [u8; 32]) {} -} - -/// An error when verifying a quote -#[cfg(not(feature = "wasm"))] -#[derive(Debug, Eq, PartialEq)] -pub enum VerifyQuoteError { - /// Quote could not be parsed or verified - BadQuote, - /// Attestation extrinsic submitted when not requested - UnexpectedAttestation, - /// Hashed input data does not match what was expected - IncorrectInputData, - /// Unacceptable VM image running - BadMrtdValue, - /// Cannot encode verifying key (PCK) - CannotEncodeVerifyingKey, - /// Cannot decode verifying key (PCK) - CannotDecodeVerifyingKey, - /// PCK certificate chain cannot be parsed - PckCertificateParse, - /// PCK certificate chain cannot be verified - PckCertificateVerify, - /// PCK certificate chain public key is not well formed - PckCertificateBadPublicKey, - /// Pck certificate could not be extracted from quote - PckCertificateNoCertificate, -} diff --git a/crates/test-cli/src/lib.rs b/crates/test-cli/src/lib.rs index d30afc313..528798578 100644 --- a/crates/test-cli/src/lib.rs +++ b/crates/test-cli/src/lib.rs @@ -34,7 +34,7 @@ use entropy_client::{ VERIFYING_KEY_LENGTH, }, }; -pub use entropy_shared::{QuoteContext, PROGRAM_VERSION_NUMBER}; +pub use entropy_shared::{attestation::QuoteContext, PROGRAM_VERSION_NUMBER}; use parity_scale_codec::Decode; use sp_core::{sr25519, Hasher, Pair}; use sp_runtime::{traits::BlakeTwo256, Serialize}; diff --git a/crates/threshold-signature-server/Cargo.toml b/crates/threshold-signature-server/Cargo.toml index f0471d3fd..276f2ad21 100644 --- a/crates/threshold-signature-server/Cargo.toml +++ b/crates/threshold-signature-server/Cargo.toml @@ -108,7 +108,7 @@ default =["std", "dep:tdx-quote"] std =["sp-core/std"] test_helpers=["dep:project-root"] unsafe =[] -production =["std", "dep:configfs-tsm"] +production =["std", "dep:configfs-tsm", "entropy-shared/production"] alice =[] bob =[] # Enable this feature to run the integration tests for the wasm API of entropy-protocol diff --git a/crates/threshold-signature-server/src/attestation/api.rs b/crates/threshold-signature-server/src/attestation/api.rs index 543da1d3a..d19cdf1eb 100644 --- a/crates/threshold-signature-server/src/attestation/api.rs +++ b/crates/threshold-signature-server/src/attestation/api.rs @@ -14,25 +14,29 @@ // along with this program. If not, see . use crate::{ - attestation::errors::AttestationErr, - chain_api::{entropy, get_api, get_rpc, EntropyConfig}, + attestation::errors::{AttestationErr, QuoteMeasurementErr}, + chain_api::{entropy, get_api, get_rpc}, helpers::{ launch::LATEST_BLOCK_NUMBER_ATTEST, substrate::{query_chain, submit_transaction}, }, - AppState, + AppState, SubxtAccountId32, }; use axum::{ body::Bytes, extract::{Query, State}, http::StatusCode, }; -use entropy_client::user::request_attestation; +use entropy_client::{chain_api::EntropyConfig, user::request_attestation}; use entropy_kvdb::kv_manager::KvManager; -use entropy_shared::{OcwMessageAttestationRequest, QuoteContext}; +use entropy_shared::{ + attestation::{QuoteContext, QuoteInputData, VerifyQuoteError}, + OcwMessageAttestationRequest, +}; use parity_scale_codec::Decode; use serde::Deserialize; -use subxt::tx::PairSigner; +use subxt::{backend::legacy::LegacyRpcMethods, OnlineClient}; +use tdx_quote::Quote; use x25519_dalek::StaticSecret; /// HTTP POST endpoint to initiate a TDX attestation. @@ -73,7 +77,9 @@ pub async fn attest( // TODO (#1181): since this endpoint is currently only used in tests we don't know what the context should be let context = QuoteContext::Validate; - let quote = create_quote(nonce, &app_state.signer(), &app_state.x25519_secret, context).await?; + let quote = + create_quote(nonce, app_state.subxt_account_id(), &app_state.x25519_secret, context) + .await?; // Submit the quote let attest_tx = entropy::tx().attestation().attest(quote.clone()); @@ -99,7 +105,9 @@ pub async fn get_attest( let context = context_querystring.as_quote_context()?; - let quote = create_quote(nonce, &app_state.signer(), &app_state.x25519_secret, context).await?; + let quote = + create_quote(nonce, app_state.subxt_account_id(), &app_state.x25519_secret, context) + .await?; Ok((StatusCode::OK, quote)) } @@ -108,28 +116,23 @@ pub async fn get_attest( #[cfg(not(feature = "production"))] pub async fn create_quote( nonce: [u8; 32], - signer: &PairSigner, + tss_account: SubxtAccountId32, x25519_secret: &StaticSecret, context: QuoteContext, ) -> Result, AttestationErr> { use rand::{rngs::StdRng, SeedableRng}; use rand_core::OsRng; - use sp_core::Pair; // In the real thing this is the key used in the quoting enclave let signing_key = tdx_quote::SigningKey::random(&mut OsRng); let public_key = x25519_dalek::PublicKey::from(x25519_secret); - let input_data = entropy_shared::QuoteInputData::new( - signer.signer().public(), - *public_key.as_bytes(), - nonce, - context, - ); + let input_data = + QuoteInputData::new(tss_account.clone(), *public_key.as_bytes(), nonce, context); // This is generated deterministically from TSS account id - let mut pck_seeder = StdRng::from_seed(signer.signer().public().0); + let mut pck_seeder = StdRng::from_seed(tss_account.0); let pck = tdx_quote::SigningKey::random(&mut pck_seeder); let pck_encoded = tdx_quote::encode_verifying_key(pck.verifying_key())?.to_vec(); @@ -172,18 +175,13 @@ pub async fn validate_new_attestation( #[cfg(feature = "production")] pub async fn create_quote( nonce: [u8; 32], - signer: &PairSigner, + tss_account: SubxtAccountId32, x25519_secret: &StaticSecret, context: QuoteContext, ) -> Result, AttestationErr> { let public_key = x25519_dalek::PublicKey::from(x25519_secret); - let input_data = entropy_shared::QuoteInputData::new( - signer.signer().public(), - *public_key.as_bytes(), - nonce, - context, - ); + let input_data = QuoteInputData::new(tss_account, *public_key.as_bytes(), nonce, context); Ok(configfs_tsm::create_quote(input_data.0) .map_err(|e| AttestationErr::QuoteGeneration(format!("{:?}", e)))?) @@ -209,3 +207,25 @@ impl QuoteContextQuery { } } } + +/// Check build-time measurement matches a current-supported release of entropy-tss +/// This differs slightly from the attestation pallet implementation because here we don't have direct +/// access to the parameters pallet - we need to make a query +pub async fn check_quote_measurement( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + quote: &Quote, +) -> Result<(), QuoteMeasurementErr> { + let mrtd_value = quote.mrtd().to_vec(); + let query = entropy::storage().parameters().accepted_mrtd_values(); + let accepted_mrtd_values: Vec<_> = query_chain(api, rpc, query, None) + .await? + .ok_or(QuoteMeasurementErr::NoMeasurementValues)? + .into_iter() + .map(|v| v.0) + .collect(); + if !accepted_mrtd_values.contains(&mrtd_value) { + return Err(VerifyQuoteError::BadMrtdValue.into()); + }; + Ok(()) +} diff --git a/crates/threshold-signature-server/src/attestation/errors.rs b/crates/threshold-signature-server/src/attestation/errors.rs index 0905ceb27..ee9a6ff76 100644 --- a/crates/threshold-signature-server/src/attestation/errors.rs +++ b/crates/threshold-signature-server/src/attestation/errors.rs @@ -64,3 +64,14 @@ impl IntoResponse for AttestationErr { (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() } } + +/// Error when checking quote measurement value +#[derive(Debug, Error)] +pub enum QuoteMeasurementErr { + #[error("Substrate: {0}")] + SubstrateClient(#[from] entropy_client::substrate::SubstrateError), + #[error("Could not get accepted measurement values from on-chain parameters")] + NoMeasurementValues, + #[error("Quote verification: {0}")] + Kv(#[from] entropy_shared::attestation::VerifyQuoteError), +} diff --git a/crates/threshold-signature-server/src/backup_provider/api.rs b/crates/threshold-signature-server/src/backup_provider/api.rs new file mode 100644 index 000000000..633fe044d --- /dev/null +++ b/crates/threshold-signature-server/src/backup_provider/api.rs @@ -0,0 +1,368 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::{ + attestation::api::{check_quote_measurement, create_quote}, + backup_provider::errors::BackupProviderError, + chain_api::entropy, + validation::EncryptedSignedMessage, + AppState, EntropyConfig, SubxtAccountId32, +}; +use axum::{extract::State, Json}; +use entropy_client::substrate::query_chain; +use entropy_shared::{ + attestation::{verify_pck_certificate_chain, QuoteContext, QuoteInputData}, + user::ValidatorInfo, + X25519PublicKey, +}; +use rand::{seq::SliceRandom, RngCore}; +use rand_core::OsRng; +use serde::{Deserialize, Serialize}; +use sp_core::{sr25519, Pair}; +use std::path::PathBuf; +use subxt::{backend::legacy::LegacyRpcMethods, OnlineClient}; +use tdx_quote::Quote; +use x25519_dalek::{PublicKey, StaticSecret}; + +const BACKUP_PROVIDER_FILENAME: &str = "backup-provider-details.json"; + +/// Client function to make a request to a given TSS node to backup a given encryption key +/// This makes a client request to [backup_encryption_key] +pub async fn request_backup_encryption_key( + key: [u8; 32], + backup_provider_details: BackupProviderDetails, + sr25519_pair: &sr25519::Pair, +) -> Result<(), BackupProviderError> { + // Encrypt the key to the backup provider's public x25519 key + let signed_message = EncryptedSignedMessage::new( + sr25519_pair, + key.to_vec(), + &backup_provider_details.provider.x25519_public_key, + &[], + )?; + + // Make the request + let client = reqwest::Client::new(); + let response = client + .post(format!( + "http://{}/backup_encryption_key", + backup_provider_details.provider.ip_address + )) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&signed_message)?) + .send() + .await?; + + let status = response.status(); + if status != reqwest::StatusCode::OK { + let text = response.text().await?; + return Err(BackupProviderError::BadProviderResponse(status, text)); + } + Ok(()) +} + +/// Make a request to a given TSS node to recover an encryption key +pub async fn request_recover_encryption_key( + backup_provider_details: BackupProviderDetails, +) -> Result<[u8; 32], BackupProviderError> { + // Generate encryption keypair used for receiving the key + let response_secret_key = StaticSecret::random_from_rng(OsRng); + let response_key = PublicKey::from(&response_secret_key).to_bytes(); + + let quote_nonce = request_quote_nonce(&response_secret_key, &backup_provider_details).await?; + + // Quote input contains: key_provider_details.tss_account, and response_key + let quote = create_quote( + quote_nonce, + backup_provider_details.tss_account.clone(), + &response_secret_key, + QuoteContext::EncryptionKeyRecoveryRequest, + ) + .await?; + + let key_request = RecoverEncryptionKeyRequest { + tss_account: backup_provider_details.tss_account, + response_key, + quote, + }; + + let client = reqwest::Client::new(); + let response = client + .post(format!( + "http://{}/recover_encryption_key", + backup_provider_details.provider.ip_address + )) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&key_request)?) + .send() + .await?; + + let status = response.status(); + if status != reqwest::StatusCode::OK { + let text = response.text().await?; + return Err(BackupProviderError::BadProviderResponse(status, text)); + } + + let response_bytes = response.bytes().await?; + + // Decrypt the response + let encrypted_response: EncryptedSignedMessage = serde_json::from_slice(&response_bytes)?; + let signed_message = encrypted_response.decrypt(&response_secret_key, &[])?; + + signed_message.message.0.try_into().map_err(|_| BackupProviderError::BadKeyLength) +} + +/// [ValidatorInfo] of a TSS node chosen to make a key backup, together with the account ID of the +/// TSS node who the backup is for +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupProviderDetails { + pub provider: ValidatorInfo, + pub tss_account: SubxtAccountId32, +} + +/// POST request body for the `/recover_encryption_key` HTTP route +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecoverEncryptionKeyRequest { + /// The account ID of the TSS node requesting to recover their encryption key + tss_account: SubxtAccountId32, + /// An ephemeral encryption public key used to receive an encrypted response + response_key: X25519PublicKey, + /// A TDX quote + quote: Vec, +} + +/// HTTP endpoint to backup an encryption key on initial launch +/// The request body should be an encryption key to backup as a [u8; 32] wrapped in an [EncryptedSignedMessage] +pub async fn backup_encryption_key( + State(app_state): State, + Json(encrypted_backup_request): Json, +) -> Result<(), BackupProviderError> { + // Wait for read access to the chain + let mut n = 0; + while !app_state.can_read_from_chain() { + if n > 9 { + return Err(BackupProviderError::NotConnectedToChain); + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + n += 1; + } + + // Decrypt the request body to get the key to be backed-up + let signed_message = encrypted_backup_request.decrypt(&app_state.x25519_secret, &[])?; + let tss_account = signed_message.account_id(); + let key: [u8; 32] = + signed_message.message.0.try_into().map_err(|_| BackupProviderError::BadKeyLength)?; + + // Check for TSS account on the staking pallet - which proves they have made an on-chain attestation + let threshold_address_query = entropy::storage() + .staking_extension() + .threshold_to_stash(SubxtAccountId32(*tss_account.as_ref())); + let (api, rpc) = app_state.get_api_rpc().await?; + query_chain(&api, &rpc, threshold_address_query, None) + .await? + .ok_or(BackupProviderError::NotRegisteredWithStakingPallet)?; + + let mut backups = app_state + .encryption_key_backup_provider + .write() + .map_err(|_| BackupProviderError::RwLockPoison)?; + backups.insert(tss_account, key); + + Ok(()) +} + +/// HTTP endpoint to recover an encryption key following a process restart. +/// The request body should contain a JSON encoded [RecoverEncryptionKeyRequest]. +/// If successful, the response body will contain the encryption key as a [u8; 32] wrapped in an +/// [EncryptedSignedMessage]. +pub async fn recover_encryption_key( + State(app_state): State, + Json(key_request): Json, +) -> Result, BackupProviderError> { + if !app_state.is_ready() { + return Err(BackupProviderError::NotReady); + } + + let quote = Quote::from_bytes(&key_request.quote)?; + + let nonce = { + let mut nonces = + app_state.attestation_nonces.write().map_err(|_| BackupProviderError::RwLockPoison)?; + nonces.remove(&key_request.response_key).ok_or(BackupProviderError::NoNonceInStore)? + }; + + let expected_input_data = QuoteInputData::new( + key_request.tss_account.clone(), + key_request.response_key, + nonce, + QuoteContext::EncryptionKeyRecoveryRequest, + ); + if quote.report_input_data() != expected_input_data.0 { + return Err(BackupProviderError::BadQuoteInputData); + } + + let (api, rpc) = app_state.get_api_rpc().await?; + check_quote_measurement(&api, &rpc, "e).await?; + + let _pck = verify_pck_certificate_chain("e)?; + + let key = { + let backups = app_state + .encryption_key_backup_provider + .read() + .map_err(|_| BackupProviderError::RwLockPoison)?; + *backups.get(&key_request.tss_account.0.into()).ok_or(BackupProviderError::NoKeyInStore)? + }; + + // Encrypt response + let signed_message = + EncryptedSignedMessage::new(&app_state.pair, key.to_vec(), &key_request.response_key, &[])?; + Ok(Json(signed_message)) +} + +/// Create a backup of our key-value store encryption key by sending it to another TSS node to store +pub async fn make_key_backup( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + key: [u8; 32], + sr25519_pair: &sr25519::Pair, + storage_path: PathBuf, +) -> Result<(), BackupProviderError> { + let tss_account = SubxtAccountId32(sr25519_pair.public().0); + // Select a provider by making chain query and choosing a tss node + let key_provider_details = select_backup_provider(api, rpc, tss_account).await?; + // Get them to backup the key + request_backup_encryption_key(key, key_provider_details.clone(), sr25519_pair).await?; + // Store provider details so we know who to ask when recovering + store_key_provider_details(storage_path, key_provider_details)?; + Ok(()) +} + +/// Store the details of a TSS node who has a backup of our encryption key in a file +fn store_key_provider_details( + mut path: PathBuf, + backup_provider_details: BackupProviderDetails, +) -> Result<(), BackupProviderError> { + path.push(BACKUP_PROVIDER_FILENAME); + Ok(std::fs::write(path, serde_json::to_vec(&backup_provider_details)?)?) +} + +/// Retrieve the details of a TSS node who has a backup of our encryption key from a file +pub fn get_key_provider_details( + mut path: PathBuf, +) -> Result { + path.push(BACKUP_PROVIDER_FILENAME); + let bytes = std::fs::read(path)?; + Ok(serde_json::from_slice(&bytes)?) +} + +/// Choose a TSS node to request to make a backup +async fn select_backup_provider( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + tss_account: SubxtAccountId32, +) -> Result { + // Get all active validators + let validators_query = entropy::storage().session().validators(); + let validators = query_chain(api, rpc, validators_query, None) + .await? + .ok_or(BackupProviderError::NoValidators)?; + if validators.is_empty() { + return Err(BackupProviderError::NoValidators); + } + + // Choose one randomly + let validator = validators.choose(&mut OsRng).unwrap(); + + // Get associated details + let threshold_address_query = + entropy::storage().staking_extension().threshold_servers(validator); + let server_info = query_chain(api, rpc, threshold_address_query, None) + .await? + .ok_or(BackupProviderError::NoServerInfo)?; + + tracing::info!( + "Selected TSS account {} to act as a db encrpytion key backup provider", + server_info.tss_account + ); + + Ok(BackupProviderDetails { + provider: ValidatorInfo { + x25519_public_key: server_info.x25519_public_key, + ip_address: std::str::from_utf8(&server_info.endpoint)?.to_string(), + tss_account: server_info.tss_account, + }, + tss_account, + }) +} + +/// HTTP POST route which provides a quote nonce to be used in the quote when requesting to recover +/// an encryption key. +/// The nonce is returned encrypted with the given ephemeral public key. This key is also used as a +/// lookup key for the nonce. +pub async fn quote_nonce( + State(app_state): State, + Json(response_key): Json, +) -> Result, BackupProviderError> { + if !app_state.is_ready() { + return Err(BackupProviderError::NotReady); + } + + let mut nonce = [0; 32]; + OsRng.fill_bytes(&mut nonce); + + { + let mut nonces = + app_state.attestation_nonces.write().map_err(|_| BackupProviderError::RwLockPoison)?; + nonces.insert(response_key, nonce); + } + + // Encrypt response + let signed_message = + EncryptedSignedMessage::new(&app_state.pair, nonce.to_vec(), &response_key, &[])?; + Ok(Json(signed_message)) +} + +/// Client function used to make a POST request to `backup_provider_quote_nonce` +async fn request_quote_nonce( + response_secret_key: &StaticSecret, + backup_provider_details: &BackupProviderDetails, +) -> Result<[u8; 32], BackupProviderError> { + let response_key = PublicKey::from(response_secret_key).to_bytes(); + + let client = reqwest::Client::new(); + let response = client + .post(format!( + "http://{}/backup_provider_quote_nonce", + backup_provider_details.provider.ip_address + )) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&response_key)?) + .send() + .await?; + + let status = response.status(); + if status != reqwest::StatusCode::OK { + let text = response.text().await?; + return Err(BackupProviderError::BadProviderResponse(status, text)); + } + + let response_bytes = response.bytes().await?; + + let encrypted_response: EncryptedSignedMessage = serde_json::from_slice(&response_bytes)?; + let signed_message = encrypted_response.decrypt(response_secret_key, &[])?; + + signed_message.message.0.try_into().map_err(|_| BackupProviderError::BadKeyLength) +} diff --git a/crates/threshold-signature-server/src/backup_provider/errors.rs b/crates/threshold-signature-server/src/backup_provider/errors.rs new file mode 100644 index 000000000..7dc458598 --- /dev/null +++ b/crates/threshold-signature-server/src/backup_provider/errors.rs @@ -0,0 +1,79 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use entropy_kvdb::kv_manager::error::KvError; +use thiserror::Error; + +/// An error relating to backing-up or recovering a key-value database encryption key +#[derive(Debug, Error)] +pub enum BackupProviderError { + #[error("HTTP request: {0}")] + HttpRequest(#[from] reqwest::Error), + #[error("Key-value store: {0}")] + Kv(#[from] KvError), + #[error("Encryption key is not present in backup store")] + NoKeyInStore, + #[error("Cannot retrieve associated nonce for this backup")] + NoNonceInStore, + #[error("Panic while holding lock on backup store")] + RwLockPoison, + #[error("JSON: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("Encryption: {0}")] + Encryption(#[from] crate::validation::EncryptedSignedMessageErr), + #[error("Attestation: {0}")] + Attestation(#[from] crate::attestation::errors::AttestationErr), + #[error("Generic Substrate error: {0}")] + GenericSubstrate(#[from] subxt::error::Error), + #[error("Bad response from backup provider: {0} {1}")] + BadProviderResponse(reqwest::StatusCode, String), + #[error("Provider responded with a key which is not 32 bytes")] + BadKeyLength, + #[error("Substrate: {0}")] + SubstrateClient(#[from] entropy_client::substrate::SubstrateError), + #[error("The account requesting to recover a key is not registered with the staking pallet")] + NotRegisteredWithStakingPallet, + #[error("Filesystem IO: {0}")] + Io(#[from] std::io::Error), + #[error("Utf8Error: {0:?}")] + Utf8(#[from] std::str::Utf8Error), + #[error("Quote parse: {0}")] + QuoteParse(#[from] tdx_quote::QuoteParseError), + #[error("Bad quote input data: TSS account, response public key, or nonce are incorrect")] + BadQuoteInputData, + #[error("Quote verify: {0}")] + VerifyQuote(#[from] entropy_shared::attestation::VerifyQuoteError), + #[error("Could not find another TSS node to request backup")] + NoValidators, + #[error("Could not get server info for TSS node chosen for backup")] + NoServerInfo, + #[error("Node has started fresh and not yet successfully set up")] + NotReady, + #[error("Quote measurement: {0}")] + QuoteMeasurement(#[from] crate::attestation::errors::QuoteMeasurementErr), + #[error("Timed out waiting to be connected to chain")] + NotConnectedToChain, +} + +impl IntoResponse for BackupProviderError { + fn into_response(self) -> Response { + tracing::error!("{:?}", format!("{self}")); + let body = format!("{self}").into_bytes(); + (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() + } +} diff --git a/crates/threshold-signature-server/src/backup_provider/mod.rs b/crates/threshold-signature-server/src/backup_provider/mod.rs new file mode 100644 index 000000000..fa00b855f --- /dev/null +++ b/crates/threshold-signature-server/src/backup_provider/mod.rs @@ -0,0 +1,21 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Backup database encryption key provider service +pub mod api; +pub mod errors; + +#[cfg(test)] +mod tests; diff --git a/crates/threshold-signature-server/src/backup_provider/tests.rs b/crates/threshold-signature-server/src/backup_provider/tests.rs new file mode 100644 index 000000000..3599601b8 --- /dev/null +++ b/crates/threshold-signature-server/src/backup_provider/tests.rs @@ -0,0 +1,92 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::path::PathBuf; + +use crate::{ + backup_provider::api::{ + get_key_provider_details, make_key_backup, request_backup_encryption_key, + request_recover_encryption_key, BackupProviderDetails, + }, + helpers::{ + tests::initialize_test_logger, validator::get_signer_and_x25519_secret_from_mnemonic, + }, + launch::{development_mnemonic, ValidatorName}, +}; +use entropy_kvdb::clean_tests; +use entropy_shared::user::ValidatorInfo; +use entropy_testing_utils::{ + constants::{TSS_ACCOUNTS, X25519_PUBLIC_KEYS}, + helpers::spawn_tss_nodes_and_start_chain, + ChainSpecType, +}; +use serial_test::serial; + +/// This tests the whole process of selecting and using a backup provider +#[tokio::test] +#[serial] +async fn backup_provider_test() { + clean_tests(); + initialize_test_logger().await; + + let (_ctx, api, rpc, _validator_ips, _validator_ids) = + spawn_tss_nodes_and_start_chain(ChainSpecType::IntegrationJumpStarted).await; + + let storage_path: PathBuf = ".entropy/testing/test_db_validator1".into(); + // For testing we use TSS account ID as the db encryption key + let key = TSS_ACCOUNTS[0].0; + + let mnemonic = development_mnemonic(&Some(ValidatorName::Alice)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); + + make_key_backup(&api, &rpc, key, tss_signer.signer(), storage_path.clone()).await.unwrap(); + + let key_provider_details = get_key_provider_details(storage_path).unwrap(); + let recovered_key = request_recover_encryption_key(key_provider_details).await.unwrap(); + assert_eq!(key, recovered_key); +} + +/// More low-level version of key_backup_provider_test +#[tokio::test] +#[serial] +async fn backup_provider_unit_test() { + clean_tests(); + initialize_test_logger().await; + + let (_ctx, _api, _rpc, _validator_ips, _validator_ids) = + spawn_tss_nodes_and_start_chain(ChainSpecType::IntegrationJumpStarted).await; + + let key_provider_details = BackupProviderDetails { + provider: ValidatorInfo { + tss_account: TSS_ACCOUNTS[0].clone(), + x25519_public_key: X25519_PUBLIC_KEYS[0], + ip_address: "127.0.0.1:3001".to_string(), + }, + tss_account: TSS_ACCOUNTS[1].clone(), + }; + // For testing we use TSS account ID as the db encryption key + let key = TSS_ACCOUNTS[1].0; + + let mnemonic = development_mnemonic(&Some(ValidatorName::Bob)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); + + request_backup_encryption_key(key, key_provider_details.clone(), tss_signer.signer()) + .await + .unwrap(); + let recovered_key = request_recover_encryption_key(key_provider_details).await.unwrap(); + assert_eq!(key, recovered_key); +} diff --git a/crates/threshold-signature-server/src/helpers/launch.rs b/crates/threshold-signature-server/src/helpers/launch.rs index 965d31328..dc5bc3d66 100644 --- a/crates/threshold-signature-server/src/helpers/launch.rs +++ b/crates/threshold-signature-server/src/helpers/launch.rs @@ -15,17 +15,25 @@ //! Utilities for starting and running the server. -use std::{fs, path::PathBuf}; - -use crate::{chain_api::entropy, helpers::substrate::query_chain, AppState}; +use std::path::PathBuf; + +use crate::{ + backup_provider::api::{ + get_key_provider_details, make_key_backup, request_recover_encryption_key, + }, + chain_api::entropy, + helpers::{substrate::query_chain, validator::get_signer_and_x25519_secret}, + AppState, +}; use clap::Parser; use entropy_client::substrate::SubstrateError; -use entropy_kvdb::{ - encrypted_sled::PasswordMethod, - kv_manager::{error::KvError, KvManager}, -}; +use entropy_kvdb::kv_manager::{error::KvError, KvManager}; +use rand::RngCore; +use rand_core::OsRng; use serde::Deserialize; use sp_core::crypto::Ss58Codec; +use sp_core::{sr25519, Pair}; +use x25519_dalek::StaticSecret; pub const DEFAULT_MNEMONIC: &str = "alarm mutual concert decrease hurry invest culture survey diagram crash snap click"; @@ -45,6 +53,9 @@ pub const LATEST_BLOCK_NUMBER_ATTEST: &str = "LATEST_BLOCK_NUMBER_ATTEST"; pub const LATEST_BLOCK_NUMBER_PROACTIVE_REFRESH: &str = "LATEST_BLOCK_NUMBER_PROACTIVE_REFRESH"; +const X25519_SECRET: &str = "X25519_SECRET"; +const SR25519_SEED: &str = "SR25519_SEED"; + #[cfg(any(test, feature = "test_helpers"))] pub const DEFAULT_ENDPOINT: &str = "ws://localhost:9944"; @@ -85,55 +96,94 @@ impl Configuration { } } -pub async fn load_kv_store( +/// Setup the encrypted key-value store, recovering the encryption key if needed +/// Returns the kv store, the TSS keypairs, and the encryption key if it needs to be backed-up +pub async fn setup_kv_store( validator_name: &Option, - password_path: Option, -) -> KvManager { - let mut root: PathBuf = PathBuf::from(entropy_kvdb::get_db_path(false)); - if cfg!(test) { - return KvManager::new( - entropy_kvdb::get_db_path(true).into(), - PasswordMethod::NoPassword.execute().unwrap(), - ) - .unwrap(); + storage_path: Option, +) -> anyhow::Result<(KvManager, sr25519::Pair, StaticSecret, Option<[u8; 32]>)> { + let storage_path = storage_path.unwrap_or_else(|| build_db_path(validator_name)); + + // Check for existing database with backup details + if let Ok(key_provider_details) = get_key_provider_details(storage_path.clone()) { + // Retrieve encryption key from another TSS node + let key = request_recover_encryption_key(key_provider_details).await?; + + // Open existing db with recovered key + let kv_manager = KvManager::new(storage_path, key)?; + + // Get keypairs from existing db + let x25519_secret: [u8; 32] = kv_manager + .kv() + .get(X25519_SECRET) + .await? + .try_into() + .map_err(|_| anyhow::anyhow!("X25519 secret from db is not 32 bytes"))?; + let sr25519_seed: [u8; 32] = kv_manager + .kv() + .get(SR25519_SEED) + .await? + .try_into() + .map_err(|_| anyhow::anyhow!("sr25519 seed from db is not 32 bytes"))?; + let pair = sr25519::Pair::from_seed(&sr25519_seed); + Ok((kv_manager, pair, x25519_secret.into(), None)) + } else { + // Generate TSS account (or use ValidatorName to get a test account) + let (pair, seed, x25519_secret, encryption_key) = if cfg!(test) || validator_name.is_some() + { + let (pair, seed, x25519_secret) = + get_signer_and_x25519_secret(&development_mnemonic(validator_name).to_string())?; + // For testing, the db encryption key is just the TSS account id + let encryption_key = pair.public().0; + (pair, seed, x25519_secret, encryption_key) + } else { + // Generate new keys + let (pair, seed) = sr25519::Pair::generate(); + let x25519_secret = StaticSecret::random_from_rng(OsRng); + let mut encryption_key = [0; 32]; + OsRng.fill_bytes(&mut encryption_key); + (pair, seed, x25519_secret, encryption_key) + }; + + // Open store with generated key + let kv_manager = KvManager::new(storage_path, encryption_key)?; + + // Store TSS secret keys in kv store + let reservation = kv_manager.kv().reserve_key(X25519_SECRET.to_string()).await?; + kv_manager.kv().put(reservation, x25519_secret.to_bytes().to_vec()).await?; + let reservation = kv_manager.kv().reserve_key(SR25519_SEED.to_string()).await?; + kv_manager.kv().put(reservation, seed.to_vec()).await?; + + // Return the encryption key so that it can be backed up as part of the pre-requisite checks + Ok((kv_manager, pair, x25519_secret, Some(encryption_key))) } +} - if validator_name == &Some(ValidatorName::Alice) { - return KvManager::new(root, PasswordMethod::NoPassword.execute().unwrap()).unwrap(); - }; +/// Build the storage path for the key-value store, providing separate subdirectories for the +/// different test accounts when testing +pub fn build_db_path(validator_name: &Option) -> PathBuf { + if cfg!(test) { + return PathBuf::from(entropy_kvdb::get_db_path(true)); + } + let mut root: PathBuf = PathBuf::from(entropy_kvdb::get_db_path(false)); + // Alice has no extra subdirectory if validator_name == &Some(ValidatorName::Bob) { root.push("bob"); - return KvManager::new(root, PasswordMethod::NoPassword.execute().unwrap()).unwrap(); }; if validator_name == &Some(ValidatorName::Charlie) { root.push("charlie"); - return KvManager::new(root, PasswordMethod::NoPassword.execute().unwrap()).unwrap(); }; if validator_name == &Some(ValidatorName::Dave) { root.push("dave"); - return KvManager::new(root, PasswordMethod::NoPassword.execute().unwrap()).unwrap(); }; if validator_name == &Some(ValidatorName::Eve) { root.push("eve"); - return KvManager::new(root, PasswordMethod::NoPassword.execute().unwrap()).unwrap(); - }; - - let password = if let Some(password_path) = password_path { - std::str::from_utf8(&fs::read(password_path).expect("error reading password file")) - .expect("failed to convert password to string") - .trim() - .to_string() - .into() - } else { - PasswordMethod::Prompt.execute().unwrap() }; - - // this step takes a long time due to password-based decryption - KvManager::new(root, password).unwrap() + root } #[derive(Parser, Debug, Clone)] @@ -261,7 +311,10 @@ pub async fn setup_latest_block_number(kv: &KvManager) -> Result<(), KvError> { Ok(()) } -pub async fn check_node_prerequisites(app_state: AppState) -> Result<(), &'static str> { +pub async fn check_node_prerequisites( + app_state: AppState, + key_to_backup: Option<[u8; 32]>, +) -> Result<(), &'static str> { use crate::chain_api::{get_api, get_rpc}; let url = &app_state.configuration.endpoint; let account_id = app_state.account_id(); @@ -288,7 +341,9 @@ pub async fn check_node_prerequisites(app_state: AppState) -> Result<(), &'stati let (api, rpc) = backoff::future::retry(backoff.clone(), connect_to_substrate_node) .await .map_err(|_| "Timed out waiting for connection to chain")?; + tracing::info!("Sucessfully connected to Substrate node!"); + app_state.connected_to_chain_node().map_err(|_| "Poisoned mutex")?; tracing::info!("Checking balance of threshold server AccountId `{}`", &account_id); @@ -338,7 +393,25 @@ pub async fn check_node_prerequisites(app_state: AppState) -> Result<(), &'stati backoff::future::retry(backoff, check_for_tss_account_id) .await .map_err(|_| "Timed out waiting for TSS account to be registered on chain")?; + + if let Some(key_to_backup) = key_to_backup { + tracing::info!("Backing up keyshare..."); + make_key_backup( + &api, + &rpc, + key_to_backup, + &app_state.pair, + app_state.kv_store.storage_path().to_path_buf(), + ) + .await + .map_err(|e| { + tracing::error!("Could not make key backup: {}", e); + "Could not make key backup" + })?; + tracing::info!("Successfully backed up keyshare"); + } + tracing::info!("TSS node passed all prerequisite checks and is ready"); - app_state.make_ready(); + app_state.make_ready().map_err(|_| "Poisoned mutex")?; Ok(()) } diff --git a/crates/threshold-signature-server/src/helpers/tests.rs b/crates/threshold-signature-server/src/helpers/tests.rs index beab864db..24328e3b6 100644 --- a/crates/threshold-signature-server/src/helpers/tests.rs +++ b/crates/threshold-signature-server/src/helpers/tests.rs @@ -31,7 +31,10 @@ use crate::{ EntropyConfig, }, helpers::{ - launch::{setup_latest_block_number, Configuration, ValidatorName, DEFAULT_ENDPOINT}, + launch::{ + setup_kv_store, setup_latest_block_number, Configuration, ValidatorName, + DEFAULT_ENDPOINT, + }, logger::{Instrumentation, Logger}, substrate::submit_transaction, }, @@ -39,13 +42,13 @@ use crate::{ }; use axum::{routing::IntoMakeService, Router}; use entropy_client::substrate::query_chain; -use entropy_kvdb::{encrypted_sled::PasswordMethod, get_db_path, kv_manager::KvManager}; +use entropy_kvdb::{get_db_path, kv_manager::KvManager}; use entropy_protocol::PartyId; #[cfg(test)] use entropy_shared::EncodedVerifyingKey; use entropy_shared::NETWORK_PARENT_KEY; use sp_keyring::AccountKeyring; -use std::{fmt, net::SocketAddr, str, time::Duration}; +use std::{fmt, net::SocketAddr, path::PathBuf, str, time::Duration}; use subxt::{ backend::legacy::LegacyRpcMethods, ext::sp_core::sr25519, tx::PairSigner, utils::AccountId32 as SubxtAccountId32, Config, OnlineClient, @@ -67,15 +70,17 @@ pub async fn initialize_test_logger() { } pub async fn setup_client() -> KvManager { - let kv_store = - KvManager::new(get_db_path(true).into(), PasswordMethod::NoPassword.execute().unwrap()) - .unwrap(); + let configuration = Configuration::new(DEFAULT_ENDPOINT.to_string()); + + let storage_path: PathBuf = get_db_path(true).into(); + let (kv_store, sr25519_pair, x25519_secret, _should_backup) = + setup_kv_store(&Some(ValidatorName::Alice), Some(storage_path.clone())).await.unwrap(); let _ = setup_latest_block_number(&kv_store).await; - let configuration = Configuration::new(DEFAULT_ENDPOINT.to_string()); - let app_state = AppState::new(configuration, kv_store.clone(), &Some(ValidatorName::Alice)); + let app_state = AppState::new(configuration, kv_store.clone(), sr25519_pair, x25519_secret); + // Mock making the pre-requisite checks by setting the application state to ready - app_state.make_ready(); + app_state.make_ready().unwrap(); let app = app(app_state).into_make_service(); @@ -100,8 +105,11 @@ pub async fn create_clients( let path = format!(".entropy/testing/test_db_{key_number}"); let _ = std::fs::remove_dir_all(path.clone()); - let kv_store = - KvManager::new(path.into(), PasswordMethod::NoPassword.execute().unwrap()).unwrap(); + let (kv_store, sr25519_pair, x25519_secret, _should_backup) = + setup_kv_store(validator_name, Some(path.into())).await.unwrap(); + + let _ = setup_latest_block_number(&kv_store).await; + let app_state = AppState::new(configuration, kv_store.clone(), sr25519_pair, x25519_secret); let _ = setup_latest_block_number(&kv_store).await; @@ -110,9 +118,8 @@ pub async fn create_clients( let _ = kv_store.clone().kv().put(reservation, value).await; } - let app_state = AppState::new(configuration, kv_store.clone(), validator_name); // Mock making the pre-requisite checks by setting the application state to ready - app_state.make_ready(); + app_state.make_ready().unwrap(); let account_id = app_state.subxt_account_id(); diff --git a/crates/threshold-signature-server/src/helpers/validator.rs b/crates/threshold-signature-server/src/helpers/validator.rs index 06d3bf809..f77314d76 100644 --- a/crates/threshold-signature-server/src/helpers/validator.rs +++ b/crates/threshold-signature-server/src/helpers/validator.rs @@ -27,15 +27,15 @@ use crate::user::UserErr; const KDF_SR25519: &[u8] = b"sr25519-threshold-account"; const KDF_X25519: &[u8] = b"X25519-keypair"; -/// Get the PairSigner as above, and also the x25519 encryption keypair for +/// Get the PairSigner, seed, and also the x25519 encryption keypair for /// this threshold server pub fn get_signer_and_x25519_secret( mnemonic: &str, -) -> Result<(sr25519::Pair, StaticSecret), UserErr> { +) -> Result<(sr25519::Pair, [u8; 32], StaticSecret), UserErr> { let hkdf = get_hkdf_from_mnemonic(mnemonic)?; - let pair_signer = get_signer_from_hkdf(&hkdf)?; + let (pair_signer, seed) = get_signer_from_hkdf(&hkdf)?; let static_secret = get_x25519_secret_from_hkdf(&hkdf)?; - Ok((pair_signer, static_secret)) + Ok((pair_signer, seed, static_secret)) } /// Given a mnemonic, setup hkdf @@ -45,14 +45,12 @@ fn get_hkdf_from_mnemonic(mnemonic: &str) -> Result, UserErr> { Ok(Hkdf::::new(None, &mnemonic.to_seed(""))) } -/// Derive signing keypair -pub fn get_signer_from_hkdf(hkdf: &Hkdf) -> Result { +/// Derive signing keypair and return it together with the seed +pub fn get_signer_from_hkdf(hkdf: &Hkdf) -> Result<(sr25519::Pair, [u8; 32]), UserErr> { let mut sr25519_seed = [0u8; 32]; hkdf.expand(KDF_SR25519, &mut sr25519_seed)?; let pair = sr25519::Pair::from_seed(&sr25519_seed); - sr25519_seed.zeroize(); - - Ok(pair) + Ok((pair, sr25519_seed)) } /// Derive x25519 secret @@ -70,7 +68,7 @@ pub fn get_signer_and_x25519_secret_from_mnemonic( mnemonic: &str, ) -> Result<(subxt::tx::PairSigner, StaticSecret), UserErr> { let hkdf = get_hkdf_from_mnemonic(mnemonic)?; - let pair = get_signer_from_hkdf(&hkdf)?; + let (pair, _) = get_signer_from_hkdf(&hkdf)?; let pair_signer = subxt::tx::PairSigner::new(pair); let static_secret = get_x25519_secret_from_hkdf(&hkdf)?; Ok((pair_signer, static_secret)) diff --git a/crates/threshold-signature-server/src/lib.rs b/crates/threshold-signature-server/src/lib.rs index 7a180af02..25cad4d79 100644 --- a/crates/threshold-signature-server/src/lib.rs +++ b/crates/threshold-signature-server/src/lib.rs @@ -160,6 +160,7 @@ #![doc(html_logo_url = "https://entropy.xyz/assets/logo_02.png")] pub use entropy_client::chain_api; pub(crate) mod attestation; +pub(crate) mod backup_provider; pub(crate) mod health; pub mod helpers; pub(crate) mod node_info; @@ -176,9 +177,12 @@ use axum::{ Router, }; use entropy_kvdb::kv_manager::KvManager; -use rand_core::OsRng; +use entropy_shared::X25519PublicKey; use sp_core::{crypto::AccountId32, sr25519, Pair}; -use std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + sync::{Arc, PoisonError, RwLock}, +}; use subxt::{ backend::legacy::LegacyRpcMethods, tx::PairSigner, utils::AccountId32 as SubxtAccountId32, OnlineClient, @@ -193,9 +197,10 @@ use x25519_dalek::StaticSecret; pub use crate::helpers::{launch, validator::get_signer_and_x25519_secret}; use crate::{ attestation::api::{attest, get_attest}, + backup_provider::api::{backup_encryption_key, quote_nonce, recover_encryption_key}, chain_api::{get_api, get_rpc, EntropyConfig}, health::api::healthz, - launch::{development_mnemonic, Configuration, ValidatorName}, + launch::Configuration, node_info::api::{hashes, info, version as get_version}, r#unsafe::api::{delete, put, remove_keys, unsafe_get}, signing_client::{api::*, ListenerState}, @@ -203,14 +208,35 @@ use crate::{ validator::api::{new_reshare, rotate_network_key}, }; +/// Represents the state relating to the prerequisite checks +#[derive(Clone, PartialEq, Eq)] +pub enum TssState { + /// Initial state where no connection to chain node has been made + NoChainConnection, + /// Connection is made to the chain node but the account may not be yet funded + ReadOnlyChainConnection, + /// Fully ready and able to participate in the protocols + Ready, +} + +impl TssState { + fn new() -> Self { + TssState::NoChainConnection + } + + fn is_ready(&self) -> bool { + self == &TssState::Ready + } + + fn can_read_from_chain(&self) -> bool { + self != &TssState::NoChainConnection + } +} + #[derive(Clone)] pub struct AppState { - /// Tracks whether prerequisite checks have passed. - /// This means: - /// - Communication has been established with the chain node - /// - The TSS account is funded - /// - The TSS account is registered with the staking extension pallet - ready: Arc>, + /// Tracks the state of prerequisite checks + tss_state: Arc>, /// Tracks incoming protocol connections with other TSS nodes listener_state: ListenerState, /// Keypair for TSS account @@ -221,46 +247,71 @@ pub struct AppState { pub configuration: Configuration, /// Key-value store pub kv_store: KvManager, + /// Storage for encryption key backups for other TSS nodes + /// Maps TSS account id to encryption key + pub encryption_key_backup_provider: Arc>>, + /// Storage for quote nonces for other TSS nodes wanting to make encryption key backups + /// Maps response x25519 public key to quote nonce + pub attestation_nonces: Arc>>, } impl AppState { - /// Setup AppState, generating new keypairs unless a test validator name is passed + /// Setup AppState with given secret keys pub fn new( configuration: Configuration, kv_store: KvManager, - validator_name: &Option, + pair: sr25519::Pair, + x25519_secret: StaticSecret, ) -> Self { - let (pair, x25519_secret) = if cfg!(test) || validator_name.is_some() { - get_signer_and_x25519_secret(&development_mnemonic(validator_name).to_string()).unwrap() - } else { - let (pair, _seed) = sr25519::Pair::generate(); - let x25519_secret = StaticSecret::random_from_rng(OsRng); - (pair, x25519_secret) - }; - Self { - ready: Arc::new(RwLock::new(false)), + tss_state: Arc::new(RwLock::new(TssState::new())), pair, x25519_secret, listener_state: ListenerState::default(), configuration, kv_store, + encryption_key_backup_provider: Default::default(), + attestation_nonces: Default::default(), } } /// Returns true if all prerequisite checks have passed. /// Is is not possible to participate in the protocols before this is true. + /// 'Ready' means: + /// - Communication has been established with the chain node + /// - The TSS account is funded + /// - The TSS account is registered with the staking extension pallet pub fn is_ready(&self) -> bool { - match self.ready.read() { - Ok(r) => *r, + match self.tss_state.read() { + Ok(state) => state.is_ready(), + _ => false, + } + } + + /// Returns true if we are able to make chain queries + pub fn can_read_from_chain(&self) -> bool { + match self.tss_state.read() { + Ok(state) => state.can_read_from_chain(), _ => false, } } + /// Mark the node as able to make chain queries. This is called once during prerequisite checks + pub fn connected_to_chain_node( + &self, + ) -> Result<(), PoisonError>> { + let mut tss_state = self.tss_state.write()?; + if *tss_state == TssState::NoChainConnection { + *tss_state = TssState::ReadOnlyChainConnection; + } + Ok(()) + } + /// Mark the node as ready. This is called once when the prerequisite checks have passed. - pub fn make_ready(&self) { - let mut is_ready = self.ready.write().unwrap(); - *is_ready = true; + pub fn make_ready(&self) -> Result<(), PoisonError>> { + let mut tss_state = self.tss_state.write()?; + *tss_state = TssState::Ready; + Ok(()) } /// Get a [PairSigner] for submitting extrinsics with subxt @@ -304,6 +355,9 @@ pub fn app(app_state: AppState) -> Router { .route("/rotate_network_key", post(rotate_network_key)) .route("/attest", post(attest)) .route("/attest", get(get_attest)) + .route("/backup_encryption_key", post(backup_encryption_key)) + .route("/recover_encryption_key", post(recover_encryption_key)) + .route("/backup_provider_quote_nonce", post(quote_nonce)) .route("/healthz", get(healthz)) .route("/version", get(get_version)) .route("/hashes", get(hashes)) diff --git a/crates/threshold-signature-server/src/main.rs b/crates/threshold-signature-server/src/main.rs index 88d5d561c..a1a97310d 100644 --- a/crates/threshold-signature-server/src/main.rs +++ b/crates/threshold-signature-server/src/main.rs @@ -15,16 +15,19 @@ use std::{net::SocketAddr, process, str::FromStr}; +use anyhow::{anyhow, ensure}; use clap::Parser; use entropy_tss::{ app, - launch::{load_kv_store, setup_latest_block_number, Configuration, StartupArgs, ValidatorName}, + launch::{ + setup_kv_store, setup_latest_block_number, Configuration, StartupArgs, ValidatorName, + }, AppState, }; #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { let args = StartupArgs::parse(); args.logger.setup().await; @@ -55,29 +58,36 @@ async fn main() { validator_name = Some(ValidatorName::Eve); } - let kv_store = load_kv_store(&validator_name, args.password_file).await; + let (kv_store, sr25519_pair, x25519_secret, key_option) = + setup_kv_store(&validator_name, None).await?; - let app_state = AppState::new(configuration.clone(), kv_store.clone(), &validator_name); + let app_state = + AppState::new(configuration.clone(), kv_store.clone(), sr25519_pair, x25519_secret); - setup_latest_block_number(&kv_store).await.expect("Issue setting up Latest Block Number"); + ensure!( + setup_latest_block_number(&kv_store).await.is_ok(), + "Issue setting up Latest Block Number" + ); { let app_state = app_state.clone(); tokio::spawn(async move { // Check for a connection to the chain node parallel to starting the tss_server so that // we already can expose the `/info` http route - if let Err(error) = entropy_tss::launch::check_node_prerequisites(app_state).await { + if let Err(error) = + entropy_tss::launch::check_node_prerequisites(app_state, key_option).await + { tracing::error!("Prerequistite checks failed: {} - terminating.", error); process::exit(1); } }); } - let addr = SocketAddr::from_str(&args.threshold_url).expect("failed to parse threshold url."); + let addr = SocketAddr::from_str(&args.threshold_url) + .map_err(|_| anyhow!("Failed to parse threshold url"))?; let listener = tokio::net::TcpListener::bind(&addr) .await - .expect("Unable to bind to given server address."); - axum::serve(listener, app(app_state).into_make_service()) - .await - .expect("failed to launch axum server."); + .map_err(|_| anyhow!("Unable to bind to given server address"))?; + axum::serve(listener, app(app_state).into_make_service()).await?; + Ok(()) } diff --git a/crates/threshold-signature-server/src/user/tests.rs b/crates/threshold-signature-server/src/user/tests.rs index 381d9e015..c09db5d08 100644 --- a/crates/threshold-signature-server/src/user/tests.rs +++ b/crates/threshold-signature-server/src/user/tests.rs @@ -21,6 +21,7 @@ use entropy_client::{ user::{get_all_signers_from_chain, UserSignatureRequest}, }; use entropy_kvdb::clean_tests; +use entropy_kvdb::kv_manager::KvManager; use entropy_protocol::{ decode_verifying_key, protocol_transport::{noise::noise_handshake_initiator, SubscribeMessage}, @@ -74,7 +75,7 @@ use crate::{ EntropyConfig, }, helpers::{ - launch::{development_mnemonic, load_kv_store, ValidatorName}, + launch::{build_db_path, development_mnemonic, ValidatorName}, signing::Hasher, substrate::{get_oracle_data, get_signers_from_chain, query_chain, submit_transaction}, tests::{ @@ -1514,7 +1515,8 @@ async fn test_increment_or_wipe_request_limit() { let substrate_context = test_context_stationary().await; let api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); - let kv_store = load_kv_store(&None, None).await; + + let kv_store = KvManager::new(build_db_path(&None), [0; 32]).unwrap(); let request_limit_query = entropy::storage().parameters().request_limit(); let request_limit = query_chain(&api, &rpc, request_limit_query, None).await.unwrap().unwrap(); diff --git a/pallets/attestation/Cargo.toml b/pallets/attestation/Cargo.toml index 2fa933c84..dfbf31b7f 100644 --- a/pallets/attestation/Cargo.toml +++ b/pallets/attestation/Cargo.toml @@ -61,4 +61,4 @@ std=[ ] try-runtime=['frame-support/try-runtime'] # When enabled, use real PCK certificate chain verification -production=[] +production=['entropy-shared/production'] diff --git a/pallets/attestation/src/lib.rs b/pallets/attestation/src/lib.rs index 1d4f08c6f..7731ef1e9 100644 --- a/pallets/attestation/src/lib.rs +++ b/pallets/attestation/src/lib.rs @@ -47,7 +47,10 @@ mod tests; #[frame_support::pallet] pub mod pallet { - use entropy_shared::{AttestationHandler, QuoteContext, QuoteInputData, VerifyQuoteError}; + use entropy_shared::attestation::{ + verify_pck_certificate_chain, AttestationHandler, QuoteContext, QuoteInputData, + VerifyQuoteError, + }; use frame_support::pallet_prelude::*; use frame_support::traits::Randomness; use frame_system::pallet_prelude::*; @@ -58,7 +61,7 @@ pub mod pallet { rand_core::{RngCore, SeedableRng}, ChaCha20Rng, ChaChaRng, }; - use tdx_quote::{encode_verifying_key, Quote, VerifyingKey}; + use tdx_quote::{encode_verifying_key, Quote}; pub use crate::weights::WeightInfo; @@ -203,7 +206,7 @@ pub mod pallet { } } - impl entropy_shared::AttestationHandler for Pallet { + impl AttestationHandler for Pallet { fn verify_quote( attestee: &T::AccountId, x25519_public_key: entropy_shared::X25519PublicKey, @@ -250,30 +253,4 @@ pub mod pallet { PendingAttestations::::insert(who, nonce) } } - - #[cfg(feature = "production")] - fn verify_pck_certificate_chain(quote: &Quote) -> Result { - quote.verify().map_err(|_| VerifyQuoteError::PckCertificateVerify) - } - - /// A mock version of verifying the PCK certificate chain. - /// When generating mock quotes, we just put the encoded PCK in place of the certificate chain - /// so this function just decodes it, checks it was used to sign the quote, and returns it - #[cfg(not(feature = "production"))] - fn verify_pck_certificate_chain(quote: &Quote) -> Result { - let provisioning_certification_key = - quote.pck_cert_chain().map_err(|_| VerifyQuoteError::PckCertificateNoCertificate)?; - let provisioning_certification_key = tdx_quote::decode_verifying_key( - &provisioning_certification_key - .try_into() - .map_err(|_| VerifyQuoteError::CannotDecodeVerifyingKey)?, - ) - .map_err(|_| VerifyQuoteError::CannotDecodeVerifyingKey)?; - - ensure!( - quote.verify_with_pck(&provisioning_certification_key).is_ok(), - VerifyQuoteError::PckCertificateVerify - ); - Ok(provisioning_certification_key) - } } diff --git a/pallets/attestation/src/tests.rs b/pallets/attestation/src/tests.rs index 7b57f452f..8e5d985bd 100644 --- a/pallets/attestation/src/tests.rs +++ b/pallets/attestation/src/tests.rs @@ -14,7 +14,9 @@ // along with this program. If not, see . use crate::mock::*; -use entropy_shared::{AttestationHandler, QuoteContext, QuoteInputData, VerifyQuoteError}; +use entropy_shared::attestation::{ + AttestationHandler, QuoteContext, QuoteInputData, VerifyQuoteError, +}; use frame_support::{assert_noop, assert_ok}; use rand_core::OsRng; diff --git a/pallets/staking/src/benchmarking.rs b/pallets/staking/src/benchmarking.rs index 8b59b3e0a..fbb4e29c0 100644 --- a/pallets/staking/src/benchmarking.rs +++ b/pallets/staking/src/benchmarking.rs @@ -18,7 +18,10 @@ use super::*; #[allow(unused_imports)] use crate::Pallet as Staking; -use entropy_shared::{AttestationHandler, QuoteContext, MAX_SIGNERS}; +use entropy_shared::{ + attestation::{AttestationHandler, QuoteContext}, + MAX_SIGNERS, +}; use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller}; use frame_support::{ assert_ok, ensure, @@ -107,7 +110,7 @@ fn prepare_attestation_for_validate( let attestation_key = tdx_quote::SigningKey::from_bytes(&ATTESTATION_KEY.into()).unwrap(); - let input_data = entropy_shared::QuoteInputData::new( + let input_data = entropy_shared::attestation::QuoteInputData::new( &threshold, x25519_public_key, nonce, diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 516e6a3e6..a227fcaa0 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -58,8 +58,9 @@ use sp_staking::SessionIndex; #[frame_support::pallet] pub mod pallet { use entropy_shared::{ - QuoteContext, ValidatorInfo, VerifyQuoteError, X25519PublicKey, MAX_SIGNERS, - PREGENERATED_NETWORK_VERIFYING_KEY, VERIFICATION_KEY_LENGTH, + attestation::{AttestationHandler, QuoteContext, VerifyQuoteError}, + ValidatorInfo, X25519PublicKey, MAX_SIGNERS, PREGENERATED_NETWORK_VERIFYING_KEY, + VERIFICATION_KEY_LENGTH, }; use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, @@ -105,7 +106,7 @@ pub mod pallet { type MaxEndpointLength: Get; /// The handler to use when issuing and verifying attestations. - type AttestationHandler: entropy_shared::AttestationHandler; + type AttestationHandler: AttestationHandler; } /// Endpoint where a threshold server can be reached at @@ -433,7 +434,9 @@ pub mod pallet { if let Some(server_info) = maybe_server_info { // Before we modify the `server_info`, we want to check that the validator is // still running TDX hardware. - >::verify_quote( + >::verify_quote( &server_info.tss_account.clone(), server_info.x25519_public_key, quote, @@ -497,7 +500,7 @@ pub mod pallet { // Before we modify the `server_info`, we want to check that the validator is // still running TDX hardware. let provisioning_certification_key = - >::verify_quote( + >::verify_quote( &tss_account.clone(), x25519_public_key, quote, @@ -632,7 +635,7 @@ pub mod pallet { ); let provisioning_certification_key = - >::verify_quote( + >::verify_quote( &joining_server_info.tss_account.clone(), joining_server_info.x25519_public_key, quote, diff --git a/pallets/staking/src/mock.rs b/pallets/staking/src/mock.rs index 85e217f74..f7f13aab5 100644 --- a/pallets/staking/src/mock.rs +++ b/pallets/staking/src/mock.rs @@ -16,7 +16,7 @@ use core::convert::{TryFrom, TryInto}; use std::cell::RefCell; -use entropy_shared::QuoteContext; +use entropy_shared::attestation::QuoteContext; use frame_election_provider_support::{ bounds::{ElectionBounds, ElectionBoundsBuilder}, onchain, SequentialPhragmen, VoteWeight, @@ -395,18 +395,22 @@ pub(crate) const INVALID_QUOTE: [u8; 32] = [1; 32]; pub struct MockAttestationHandler; -impl entropy_shared::AttestationHandler for MockAttestationHandler { +impl entropy_shared::attestation::AttestationHandler for MockAttestationHandler { fn verify_quote( _attestee: &AccountId, _x25519_public_key: entropy_shared::X25519PublicKey, quote: Vec, _context: QuoteContext, - ) -> Result - { + ) -> Result< + entropy_shared::BoundedVecEncodedVerifyingKey, + entropy_shared::attestation::VerifyQuoteError, + > { let quote: Result<[u8; 32], _> = quote.try_into(); match quote { Ok(q) if q == VALID_QUOTE => Ok([0; 33].to_vec().try_into().unwrap()), - Ok(q) if q == INVALID_QUOTE => Err(entropy_shared::VerifyQuoteError::BadQuote), + Ok(q) if q == INVALID_QUOTE => { + Err(entropy_shared::attestation::VerifyQuoteError::BadQuote) + }, _ => { // We don't really want to verify quotes for tests in this pallet, so if we get // something else we'll just accept it.