From fccd8a3fbae983c0c28863142b7cad4bcd651646 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 13 Aug 2024 11:46:26 +0200 Subject: [PATCH] TSS attestation endpoint (#1001) * Add attest endpoint * Mv QuoteInputData to entropy-shared * Add tdx_quote to deny.toml * Improve test for mock attestation * Bump tdx-quote * Rename variable * Changelog * Changelog * Comment * Use mock attest fn if cfg(test) * Small edits following review * Doccomments * Improve test for mock attestation * Mv attestation stuff to attestation module --- CHANGELOG.md | 1 + Cargo.lock | 13 ++++ crates/shared/Cargo.toml | 1 + crates/shared/src/types.rs | 20 ++++++ crates/threshold-signature-server/Cargo.toml | 4 +- .../src/attestation/api.rs | 69 +++++++++++++++++++ .../src/attestation/errors.rs | 46 +++++++++++++ .../src/attestation/mod.rs | 22 ++++++ .../src/attestation/tests.rs | 51 ++++++++++++++ crates/threshold-signature-server/src/lib.rs | 3 + .../src/validator/errors.rs | 6 +- deny.toml | 1 + 12 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 crates/threshold-signature-server/src/attestation/api.rs create mode 100644 crates/threshold-signature-server/src/attestation/errors.rs create mode 100644 crates/threshold-signature-server/src/attestation/mod.rs create mode 100644 crates/threshold-signature-server/src/attestation/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a74c71764..6997357af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ At the moment this project **does not** adhere to - Reshare confirmation ([#965](https://github.com/entropyxyz/entropy-core/pull/965)) - Set inital signers ([#971](https://github.com/entropyxyz/entropy-core/pull/971)) - Add parent key threshold dynamically ([#974](https://github.com/entropyxyz/entropy-core/pull/974)) +- TSS attestation endpoint ([#1001](https://github.com/entropyxyz/entropy-core/pull/1001) ### Changed - Fix TSS `AccountId` keys in chainspec ([#993](https://github.com/entropyxyz/entropy-core/pull/993)) diff --git a/Cargo.lock b/Cargo.lock index cac1e2d07..60ff956e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2688,6 +2688,7 @@ dependencies = [ name = "entropy-shared" version = "0.2.0" dependencies = [ + "blake2 0.10.6", "hex-literal", "lazy_static", "parity-scale-codec", @@ -2794,6 +2795,7 @@ dependencies = [ "subxt", "subxt-signer", "synedrion", + "tdx-quote", "thiserror", "tokio", "tokio-tungstenite 0.23.1", @@ -6648,8 +6650,10 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ + "ecdsa", "elliptic-curve", "primeorder", + "sha2 0.10.8", ] [[package]] @@ -14047,6 +14051,15 @@ version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +[[package]] +name = "tdx-quote" +version = "0.1.0" +source = "git+https://github.com/entropyxyz/tdx-quote#785cd6680e857cc527037befba185fb5f84ab806" +dependencies = [ + "nom", + "p256", +] + [[package]] name = "tempfile" version = "3.12.0" diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 0bd278fed..9583d256d 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -15,6 +15,7 @@ serde ={ version="1.0", default-features=false, features=["derive"] } serde_derive="1.0.147" strum ="0.26.3" strum_macros="0.26.4" +blake2 ={ version="0.10.4", default-features=false } sp-runtime ={ version="32.0.0", default-features=false, optional=true, features=["serde"] } sp-std ={ version="12.0.0", default-features=false } diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index 9f860d031..8e42d8960 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -14,6 +14,7 @@ #![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}; @@ -99,3 +100,22 @@ pub enum HashingAlgorithm { /// A compressed, serialized [synedrion::ecdsa::VerifyingKey] pub type EncodedVerifyingKey = [u8; VERIFICATION_KEY_LENGTH as usize]; + +/// Input data to be included in a TDX attestation +pub struct QuoteInputData(pub [u8; 64]); + +impl QuoteInputData { + pub fn new( + tss_account_id: [u8; 32], + x25519_public_key: X25519PublicKey, + nonce: [u8; 32], + block_number: u32, + ) -> Self { + let mut hasher = Blake2b512::new(); + hasher.update(tss_account_id); + hasher.update(x25519_public_key); + hasher.update(nonce); + hasher.update(block_number.to_be_bytes()); + Self(hasher.finalize().into()) + } +} diff --git a/crates/threshold-signature-server/Cargo.toml b/crates/threshold-signature-server/Cargo.toml index 73772b4a8..104d844f5 100644 --- a/crates/threshold-signature-server/Cargo.toml +++ b/crates/threshold-signature-server/Cargo.toml @@ -70,6 +70,7 @@ sha1 ="0.10.6" sha2 ="0.10.8" hkdf ="0.12.4" project-root ={ version="0.2.2", optional=true } +tdx-quote ={ git="https://github.com/entropyxyz/tdx-quote", optional=true, features=["mock"] } [dev-dependencies] serial_test ="3.1.1" @@ -83,6 +84,7 @@ ethers-core ="2.0.14" schnorrkel ={ version="0.11.4", default-features=false, features=["std"] } schemars ={ version="0.8.21" } subxt-signer="0.35.3" +tdx-quote ={ git="https://github.com/entropyxyz/tdx-quote", features=["mock"] } # Note: We don't specify versions here because otherwise we run into a cyclical dependency between # `entropy-tss` and `entropy-testing-utils` when we try and publish the `entropy-tss` crate. @@ -102,7 +104,7 @@ vergen={ version="8.3.2", features=["build", "git", "gitcl"] } default =['std'] std =["sp-core/std"] test_helpers=["dep:project-root"] -unsafe =[] +unsafe =["dep:tdx-quote"] 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 new file mode 100644 index 000000000..70971d50d --- /dev/null +++ b/crates/threshold-signature-server/src/attestation/api.rs @@ -0,0 +1,69 @@ +// 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::errors::AttestationErr, AppState}; +use axum::{body::Bytes, extract::State, http::StatusCode}; + +/// HTTP POST endpoint to initiate a TDX attestation. +/// Not yet implemented. +#[cfg(not(any(test, feature = "unsafe")))] +pub async fn attest( + State(_app_state): State, + _input: Bytes, +) -> Result { + // Non-mock attestation (the real thing) will go here + Err(AttestationErr::NotImplemented) +} + +/// HTTP POST endpoint to initiate a mock TDX attestation for testing on non-TDX hardware. +/// The body of the request should be a 32 byte random nonce used to show 'freshness' of the +/// quote. +/// The response body contains a mock TDX v4 quote serialized as described in the +/// [Index TDX DCAP Quoting Library API](https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf). +#[cfg(any(test, feature = "unsafe"))] +pub async fn attest( + State(app_state): State, + input: Bytes, +) -> Result<(StatusCode, Bytes), AttestationErr> { + use crate::{chain_api::get_rpc, get_signer_and_x25519_secret}; + use rand_core::OsRng; + use sp_core::Pair; + + // TODO (#982) confirm with the chain that an attestation should be happenning + let nonce = input.as_ref().try_into()?; + + let rpc = get_rpc(&app_state.configuration.endpoint).await?; + + let block_number = + rpc.chain_get_header(None).await?.ok_or_else(|| AttestationErr::BlockNumber)?.number; + + // In the real thing this is the hardware key used in the quoting enclave + let signing_key = tdx_quote::SigningKey::random(&mut OsRng); + + let (signer, x25519_secret) = get_signer_and_x25519_secret(&app_state.kv_store).await?; + let public_key = x25519_dalek::PublicKey::from(&x25519_secret); + + let input_data = entropy_shared::QuoteInputData::new( + signer.signer().public().into(), + *public_key.as_bytes(), + nonce, + block_number, + ); + + let quote = tdx_quote::Quote::mock(signing_key.clone(), input_data.0); + // Here we would submit an attest extrinsic to the chain - but for now we just include it in the + // response + Ok((StatusCode::OK, Bytes::from(quote.as_bytes().to_vec()))) +} diff --git a/crates/threshold-signature-server/src/attestation/errors.rs b/crates/threshold-signature-server/src/attestation/errors.rs new file mode 100644 index 000000000..21c351cb2 --- /dev/null +++ b/crates/threshold-signature-server/src/attestation/errors.rs @@ -0,0 +1,46 @@ +// 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::array::TryFromSliceError; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AttestationErr { + #[error("Generic Substrate error: {0}")] + GenericSubstrate(#[from] subxt::error::Error), + #[error("User Error: {0}")] + UserErr(#[from] crate::user::UserErr), + #[cfg(not(any(test, feature = "unsafe")))] + #[error("Not yet implemented")] + NotImplemented, + #[error("Input must be 32 bytes: {0}")] + TryFromSlice(#[from] TryFromSliceError), + #[cfg(any(test, feature = "unsafe"))] + #[error("Could not get block number")] + BlockNumber, +} + +impl IntoResponse for AttestationErr { + 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/attestation/mod.rs b/crates/threshold-signature-server/src/attestation/mod.rs new file mode 100644 index 000000000..eb794135f --- /dev/null +++ b/crates/threshold-signature-server/src/attestation/mod.rs @@ -0,0 +1,22 @@ +// 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 . + +//! Makes attestations that this program is running on TDX hardware + +pub mod api; +pub mod errors; + +#[cfg(test)] +mod tests; diff --git a/crates/threshold-signature-server/src/attestation/tests.rs b/crates/threshold-signature-server/src/attestation/tests.rs new file mode 100644 index 000000000..4966c3ad5 --- /dev/null +++ b/crates/threshold-signature-server/src/attestation/tests.rs @@ -0,0 +1,51 @@ +// 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::helpers::tests::{initialize_test_logger, spawn_testing_validators}; +use entropy_kvdb::clean_tests; +use entropy_shared::QuoteInputData; +use entropy_testing_utils::{ + constants::{TSS_ACCOUNTS, X25519_PUBLIC_KEYS}, + substrate_context::test_node_process_testing_state, +}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_attest() { + initialize_test_logger().await; + clean_tests(); + + let _cxt = test_node_process_testing_state(false).await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; + + let nonce = [0; 32]; + let client = reqwest::Client::new(); + let res = client + .post(format!("http://127.0.0.1:3001/attest")) + .body(nonce.to_vec()) + .send() + .await + .unwrap(); + assert_eq!(res.status(), 200); + let quote = res.bytes().await.unwrap(); + + // This internally verifies the signature in the quote + let quote = tdx_quote::Quote::from_bytes("e).unwrap(); + + // Check the input data of the quote + let expected_input_data = + QuoteInputData::new(TSS_ACCOUNTS[0].0, X25519_PUBLIC_KEYS[0], nonce, 0); + assert_eq!(quote.report_input_data(), expected_input_data.0); +} diff --git a/crates/threshold-signature-server/src/lib.rs b/crates/threshold-signature-server/src/lib.rs index 67dfb086c..ff145a3b3 100644 --- a/crates/threshold-signature-server/src/lib.rs +++ b/crates/threshold-signature-server/src/lib.rs @@ -122,6 +122,7 @@ //! [sled](https://docs.rs/sled) #![doc(html_logo_url = "https://entropy.xyz/assets/logo_02.png")] pub use entropy_client::chain_api; +pub(crate) mod attestation; pub(crate) mod health; pub mod helpers; pub(crate) mod node_info; @@ -149,6 +150,7 @@ pub use crate::helpers::{ validator::{get_signer, get_signer_and_x25519_secret}, }; use crate::{ + attestation::api::attest, health::api::healthz, launch::Configuration, node_info::api::{hashes, version as get_version}, @@ -178,6 +180,7 @@ pub fn app(app_state: AppState) -> Router { .route("/user/sign_tx", post(sign_tx)) .route("/signer/proactive_refresh", post(proactive_refresh)) .route("/validator/reshare", post(new_reshare)) + .route("/attest", post(attest)) .route("/healthz", get(healthz)) .route("/version", get(get_version)) .route("/hashes", get(hashes)) diff --git a/crates/threshold-signature-server/src/validator/errors.rs b/crates/threshold-signature-server/src/validator/errors.rs index d7c8a8256..7d64816d4 100644 --- a/crates/threshold-signature-server/src/validator/errors.rs +++ b/crates/threshold-signature-server/src/validator/errors.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::string::FromUtf8Error; +use std::{array::TryFromSliceError, string::FromUtf8Error}; use crate::signing_client::ProtocolErr; use axum::{ @@ -93,6 +93,10 @@ pub enum ValidatorErr { InvalidData, #[error("Data is repeated")] RepeatedData, + #[error("Not yet implemented")] + NotImplemented, + #[error("Input must be 32 bytes: {0}")] + TryFromSlice(#[from] TryFromSliceError), } impl IntoResponse for ValidatorErr { diff --git a/deny.toml b/deny.toml index fbec41912..aa6412393 100644 --- a/deny.toml +++ b/deny.toml @@ -39,6 +39,7 @@ exceptions=[ { allow=["AGPL-3.0"], name="entropy-programs-core" }, { allow=["AGPL-3.0"], name="entropy-programs-runtime" }, { allow=["AGPL-3.0"], name="synedrion" }, + { allow=["AGPL-3.0"], name="tdx-quote" }, # These are the only crates using these licenses, put them here instead of globally allowing # them