WIP quartz sessions (#48)

This commit is contained in:
Shoaib Ahmed 2024-02-29 00:29:37 +05:30 committed by GitHub
commit a4a690b9cb
19 changed files with 2819 additions and 273 deletions

1663
Cargo.lock generated

File diff suppressed because it is too large Load diff

1
enclaves/quartz/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
quartz.sig

View file

@ -0,0 +1,23 @@
[package]
name = "quartz-enclave"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.1.8", features = ["derive"] }
color-eyre = "0.6.2"
cosmwasm-std = "1.4.0"
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc"] }
prost = "0.12"
rand = "0.8.5"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.94"
tendermint = "0.34.0"
tendermint-light-client = "0.34.0"
tonic = "0.11"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-proto = { path = "../../utils/quartz-proto" }
quartz-relayer = { path = "../../utils/quartz-relayer" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }

30
enclaves/quartz/README.md Normal file
View file

@ -0,0 +1,30 @@
## Quartz enclave
### Enclave usage
```bash
gramine-sgx-gen-private-key
CARGO_TARGET_DIR=./target cargo build --release
gramine-manifest \
-Dlog_level="error" \
-Dhome=${HOME} \
-Darch_libdir="/lib/$(gcc -dumpmachine)" \
-Dra_type="epid" \
-Dra_client_spid="51CAF5A48B450D624AEFE3286D314894" \
-Dra_client_linkable=1 \
-Dquartz_dir="$(pwd)" \
quartz.manifest.template quartz.manifest
gramine-sgx-sign --manifest quartz.manifest --output quartz.manifest.sgx
gramine-sgx ./quartz
```
### CLI usage
```bash
cargo run -- --chain-id testing \
--trusted-height 1 \
--trusted-hash "A1D115BA3A5E9FCC12ED68A9D8669159E9085F6F96EC26619F5C7CEB4EE02869"
```

View file

@ -0,0 +1,57 @@
# Quartz manifest file
loader.entrypoint = "file:{{ gramine.libos }}"
libos.entrypoint = "{{ quartz_dir }}/target/release/quartz-enclave"
loader.log_level = "{{ log_level }}"
loader.env.LD_LIBRARY_PATH = "/lib:{{ arch_libdir }}:/usr/{{ arch_libdir }}"
loader.env.HOME = "{{ home }}"
loader.env.INSIDE_SGX = "1"
loader.env.TLS = { passthrough = true }
loader.env.RA_TYPE = { passthrough = true }
loader.env.RA_TLS_ALLOW_DEBUG_ENCLAVE_INSECURE = { passthrough = true }
loader.env.RA_TLS_ALLOW_OUTDATED_TCB_INSECURE = { passthrough = true }
loader.env.RA_TLS_MRENCLAVE = { passthrough = true }
loader.env.RA_TLS_MRSIGNER = { passthrough = true }
loader.env.RA_TLS_ISV_SVN = { passthrough = true }
loader.env.RA_TLS_ISV_PROD_ID = { passthrough = true }
loader.env.RA_TLS_EPID_API_KEY = { passthrough = true }
loader.env.MYAPP_DATA = { passthrough = true }
loader.argv = ["quartz-enclave",
"--chain-id", "testing",
"--trusted-height", "1",
"--trusted-hash", "A1D115BA3A5E9FCC12ED68A9D8669159E9085F6F96EC26619F5C7CEB4EE02869"]
fs.mounts = [
{ uri = "file:{{ gramine.runtimedir() }}", path = "/lib" },
{ uri = "file:{{ arch_libdir }}", path = "{{ arch_libdir }}" },
{ uri = "file:/usr/{{ arch_libdir }}", path = "/usr{{ arch_libdir }}" },
{ uri = "file:{{ quartz_dir }}", path = "{{ quartz_dir }}" },
]
# sgx.debug = true
sgx.enclave_size = "512M"
sgx.max_threads = 4
sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }}
sgx.remote_attestation = "{{ ra_type }}"
sgx.ra_client_spid = "{{ ra_client_spid }}"
sgx.ra_client_linkable = {{ 'true' if ra_client_linkable == '1' else 'false' }}
sgx.trusted_files = [
"file:{{ gramine.libos }}",
"file:{{ quartz_dir }}/target/release/quartz-enclave",
"file:{{ gramine.runtimedir() }}/",
"file:{{ arch_libdir }}/",
"file:/usr/{{ arch_libdir }}/",
]
sgx.allowed_files = [
"file:{{ quartz_dir }}/exchange.sk",
"file:{{ quartz_dir }}/request.json",
]
sys.insecure__allow_eventfd = true
sys.enable_sigterm_injection = true

View file

@ -0,0 +1,62 @@
use std::{
fs::{read, File},
io::{Error as IoError, Write},
};
use quartz_cw::{
msg::execute::attested::HasUserData,
state::{MrEnclave, UserData},
};
pub trait Attestor {
type Error: ToString;
fn quote(&self, user_data: impl HasUserData) -> Result<Vec<u8>, Self::Error>;
fn mr_enclave(&self) -> Result<MrEnclave, Self::Error>;
}
#[derive(Clone, PartialEq, Debug)]
pub struct EpidAttestor;
impl Attestor for EpidAttestor {
type Error = IoError;
fn quote(&self, user_data: impl HasUserData) -> Result<Vec<u8>, Self::Error> {
let user_data = user_data.user_data();
let mut user_report_data = File::create("/dev/attestation/user_report_data")?;
user_report_data.write_all(user_data.as_slice())?;
user_report_data.flush()?;
read("/dev/attestation/quote")
}
fn mr_enclave(&self) -> Result<MrEnclave, Self::Error> {
let quote = self.quote(NullUserData)?;
Ok(quote[112..(112 + 32)]
.try_into()
.expect("hardcoded array size"))
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct MockAttestor;
impl Attestor for MockAttestor {
type Error = String;
fn quote(&self, _user_data: impl HasUserData) -> Result<Vec<u8>, Self::Error> {
Ok(vec![])
}
fn mr_enclave(&self) -> Result<MrEnclave, Self::Error> {
Ok([0u8; 32])
}
}
struct NullUserData;
impl HasUserData for NullUserData {
fn user_data(&self) -> UserData {
[0u8; 64]
}
}

View file

@ -0,0 +1,52 @@
use std::net::SocketAddr;
use clap::Parser;
use color_eyre::eyre::{eyre, Result};
use tendermint::Hash;
use tendermint_light_client::types::{Height, TrustThreshold};
fn parse_trust_threshold(s: &str) -> Result<TrustThreshold> {
if let Some((l, r)) = s.split_once('/') {
TrustThreshold::new(l.parse()?, r.parse()?).map_err(Into::into)
} else {
Err(eyre!(
"invalid trust threshold: {s}, format must be X/Y where X and Y are integers"
))
}
}
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// RPC server address
#[clap(long, default_value = "127.0.0.1:11090")]
pub rpc_addr: SocketAddr,
/// Identifier of the chain
#[clap(long)]
pub chain_id: String,
/// Height of the trusted header (AKA root-of-trust)
#[clap(long)]
pub trusted_height: Height,
/// Hash of the trusted header (AKA root-of-trust)
#[clap(long)]
pub trusted_hash: Hash,
/// Trust threshold
#[clap(long, value_parser = parse_trust_threshold, default_value_t = TrustThreshold::TWO_THIRDS)]
pub trust_threshold: TrustThreshold,
/// Trusting period, in seconds (default: two weeks)
#[clap(long, default_value = "1209600")]
pub trusting_period: u64,
/// Maximum clock drift, in seconds
#[clap(long, default_value = "5")]
pub max_clock_drift: u64,
/// Maximum block lag, in seconds
#[clap(long, default_value = "5")]
pub max_block_lag: u64,
}

View file

@ -0,0 +1,64 @@
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(
clippy::checked_conversions,
clippy::panic,
clippy::panic_in_result_fn,
missing_docs,
trivial_casts,
trivial_numeric_casts,
rust_2018_idioms,
unused_lifetimes,
unused_import_braces,
unused_qualifications
)]
mod attestor;
mod cli;
mod server;
use std::time::Duration;
use clap::Parser;
use quartz_cw::state::{Config, LightClientOpts};
use quartz_proto::quartz::core_server::CoreServer;
use tonic::transport::Server;
use crate::{
attestor::{Attestor, EpidAttestor},
cli::Cli,
server::CoreService,
};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let light_client_opts = LightClientOpts::new(
args.chain_id,
args.trusted_height.into(),
Vec::from(args.trusted_hash)
.try_into()
.expect("invalid trusted hash"),
(
args.trust_threshold.numerator(),
args.trust_threshold.denominator(),
),
args.trusting_period,
args.max_clock_drift,
args.max_block_lag,
)?;
let config = Config::new(
EpidAttestor.mr_enclave()?,
Duration::from_secs(30 * 24 * 60),
light_client_opts,
);
Server::builder()
.add_service(CoreServer::new(CoreService::new(config, EpidAttestor)))
.serve(args.rpc_addr)
.await?;
Ok(())
}

View file

@ -0,0 +1,101 @@
use std::sync::{Arc, Mutex};
use k256::ecdsa::SigningKey;
use quartz_cw::{
msg::{
execute::{session_create::SessionCreate, session_set_pub_key::SessionSetPubKey},
instantiate::CoreInstantiate,
},
state::{Config, Nonce},
};
use quartz_proto::quartz::{
core_server::Core, InstantiateRequest as RawInstantiateRequest,
InstantiateResponse as RawInstantiateResponse, SessionCreateRequest as RawSessionCreateRequest,
SessionCreateResponse as RawSessionCreateResponse,
SessionSetPubKeyRequest as RawSessionSetPubKeyRequest,
SessionSetPubKeyResponse as RawSessionSetPubKeyResponse,
};
use quartz_relayer::types::{InstantiateResponse, SessionCreateResponse, SessionSetPubKeyResponse};
use rand::Rng;
use tonic::{Request, Response, Status};
use crate::attestor::Attestor;
type TonicResult<T> = Result<T, Status>;
#[derive(Clone, Debug)]
pub struct CoreService<A> {
config: Config,
nonce: Arc<Mutex<Nonce>>,
attestor: A,
}
impl<A> CoreService<A>
where
A: Attestor,
{
pub fn new(config: Config, attestor: A) -> Self {
Self {
config,
nonce: Arc::new(Mutex::new([0u8; 32])),
attestor,
}
}
}
#[tonic::async_trait]
impl<A> Core for CoreService<A>
where
A: Attestor + Send + Sync + 'static,
{
async fn instantiate(
&self,
_request: Request<RawInstantiateRequest>,
) -> TonicResult<Response<RawInstantiateResponse>> {
let core_instantiate_msg = CoreInstantiate::new(self.config.clone());
let quote = self
.attestor
.quote(core_instantiate_msg)
.map_err(|e| Status::internal(e.to_string()))?;
let response = InstantiateResponse::new(self.config.clone(), quote);
Ok(Response::new(response.into()))
}
async fn session_create(
&self,
_request: Request<RawSessionCreateRequest>,
) -> TonicResult<Response<RawSessionCreateResponse>> {
let mut nonce = self.nonce.lock().unwrap();
*nonce = rand::thread_rng().gen::<Nonce>();
let session_create_msg = SessionCreate::new(*nonce);
let quote = self
.attestor
.quote(session_create_msg)
.map_err(|e| Status::internal(e.to_string()))?;
let response = SessionCreateResponse::new(*nonce, quote);
Ok(Response::new(response.into()))
}
async fn session_set_pub_key(
&self,
_request: Request<RawSessionSetPubKeyRequest>,
) -> TonicResult<Response<RawSessionSetPubKeyResponse>> {
let nonce = self.nonce.lock().unwrap();
let sk = SigningKey::random(&mut rand::thread_rng());
let pk = sk.verifying_key();
let session_set_pub_key_msg = SessionSetPubKey::new(*nonce, *pk);
let quote = self
.attestor
.quote(session_set_pub_key_msg)
.map_err(|e| Status::internal(e.to_string()))?;
let response = SessionSetPubKeyResponse::new(*nonce, *pk, quote);
Ok(Response::new(response.into()))
}
}

View file

@ -0,0 +1,11 @@
[package]
name = "quartz-proto"
version = "0.1.0"
edition = "2021"
[dependencies]
prost = "0.12"
tonic = "0.11"
[build-dependencies]
tonic-build = "0.11"

View file

@ -0,0 +1,6 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.out_dir("src/prost")
.compile(&["proto/quartz.proto"], &["proto"])?;
Ok(())
}

View file

@ -0,0 +1,27 @@
syntax = "proto3";
package quartz;
service Core {
rpc Instantiate (InstantiateRequest) returns (InstantiateResponse) {}
rpc SessionCreate (SessionCreateRequest) returns (SessionCreateResponse) {}
rpc SessionSetPubKey (SessionSetPubKeyRequest) returns (SessionSetPubKeyResponse) {}
}
message InstantiateRequest {}
message InstantiateResponse {
string message = 1;
}
message SessionCreateRequest {}
message SessionCreateResponse {
string message = 1;
}
message SessionSetPubKeyRequest {}
message SessionSetPubKeyResponse {
string message = 1;
}

View file

@ -0,0 +1,5 @@
#![allow(clippy::unwrap_used, unused_qualifications)]
pub mod quartz {
include!(concat!("prost/", "quartz.rs"));
}

View file

@ -0,0 +1,470 @@
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InstantiateRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InstantiateResponse {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SessionCreateRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SessionCreateResponse {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SessionSetPubKeyRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SessionSetPubKeyResponse {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod core_client {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct CoreClient<T> {
inner: tonic::client::Grpc<T>,
}
impl CoreClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> CoreClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> CoreClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + Send + Sync,
{
CoreClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn instantiate(
&mut self,
request: impl tonic::IntoRequest<super::InstantiateRequest>,
) -> std::result::Result<
tonic::Response<super::InstantiateResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/quartz.Core/Instantiate");
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("quartz.Core", "Instantiate"));
self.inner.unary(req, path, codec).await
}
pub async fn session_create(
&mut self,
request: impl tonic::IntoRequest<super::SessionCreateRequest>,
) -> std::result::Result<
tonic::Response<super::SessionCreateResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/quartz.Core/SessionCreate",
);
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("quartz.Core", "SessionCreate"));
self.inner.unary(req, path, codec).await
}
pub async fn session_set_pub_key(
&mut self,
request: impl tonic::IntoRequest<super::SessionSetPubKeyRequest>,
) -> std::result::Result<
tonic::Response<super::SessionSetPubKeyResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/quartz.Core/SessionSetPubKey",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("quartz.Core", "SessionSetPubKey"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod core_server {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with CoreServer.
#[async_trait]
pub trait Core: Send + Sync + 'static {
async fn instantiate(
&self,
request: tonic::Request<super::InstantiateRequest>,
) -> std::result::Result<
tonic::Response<super::InstantiateResponse>,
tonic::Status,
>;
async fn session_create(
&self,
request: tonic::Request<super::SessionCreateRequest>,
) -> std::result::Result<
tonic::Response<super::SessionCreateResponse>,
tonic::Status,
>;
async fn session_set_pub_key(
&self,
request: tonic::Request<super::SessionSetPubKeyRequest>,
) -> std::result::Result<
tonic::Response<super::SessionSetPubKeyResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct CoreServer<T: Core> {
inner: _Inner<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
struct _Inner<T>(Arc<T>);
impl<T: Core> CoreServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
let inner = _Inner(inner);
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for CoreServer<T>
where
T: Core,
B: Body + Send + 'static,
B::Error: Into<StdError> + Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
let inner = self.inner.clone();
match req.uri().path() {
"/quartz.Core/Instantiate" => {
#[allow(non_camel_case_types)]
struct InstantiateSvc<T: Core>(pub Arc<T>);
impl<T: Core> tonic::server::UnaryService<super::InstantiateRequest>
for InstantiateSvc<T> {
type Response = super::InstantiateResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::InstantiateRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Core>::instantiate(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = InstantiateSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/quartz.Core/SessionCreate" => {
#[allow(non_camel_case_types)]
struct SessionCreateSvc<T: Core>(pub Arc<T>);
impl<
T: Core,
> tonic::server::UnaryService<super::SessionCreateRequest>
for SessionCreateSvc<T> {
type Response = super::SessionCreateResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SessionCreateRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Core>::session_create(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = SessionCreateSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/quartz.Core/SessionSetPubKey" => {
#[allow(non_camel_case_types)]
struct SessionSetPubKeySvc<T: Core>(pub Arc<T>);
impl<
T: Core,
> tonic::server::UnaryService<super::SessionSetPubKeyRequest>
for SessionSetPubKeySvc<T> {
type Response = super::SessionSetPubKeyResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SessionSetPubKeyRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Core>::session_set_pub_key(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = SessionSetPubKeySvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
Ok(
http::Response::builder()
.status(200)
.header("grpc-status", "12")
.header("content-type", "application/grpc")
.body(empty_body())
.unwrap(),
)
})
}
}
}
}
impl<T: Core> Clone for CoreServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
impl<T: Core> Clone for _Inner<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for _Inner<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl<T: Core> tonic::server::NamedService for CoreServer<T> {
const NAME: &'static str = "quartz.Core";
}
}

View file

@ -0,0 +1,25 @@
[package]
name = "quartz-relayer"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.1.8", features = ["derive"] }
cosmos-sdk-proto = "0.16.0"
cosmrs = { version = "=0.11.0", features = ["cosmwasm"] }
cosmwasm-std = "1.4.0"
displaydoc = { version = "0.2.3", default-features = false }
ecies = { version = "0.2.6", default-features = false, features = ["pure"] }
k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.94"
subtle-encoding = { version = "0.5.1", features = ["bech32-preview"] }
tempfile = "3"
tendermint = { version = "0.29.0", features = ["secp256k1"] }
thiserror = "1.0.38"
tonic = "=0.8.3"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
quartz-cw = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-tee-ra = { git = "ssh://git@github.com/informalsystems/bisenzone-cw-mvp.git", branch = "hu55a1n1/11-use-quartz" }
quartz-proto = { path = "../../utils/quartz-proto" }

View file

@ -0,0 +1,53 @@
use std::path::PathBuf;
use clap::Parser;
use cosmrs::{tendermint::chain::Id, AccountId};
use displaydoc::Display;
use subtle_encoding::{bech32::decode as bech32_decode, Error as Bech32DecodeError};
use thiserror::Error;
use tonic::transport::Endpoint;
#[derive(Display, Error, Debug)]
pub enum AddressError {
/// Address is not bech32 encoded
NotBech32Encoded(#[source] Bech32DecodeError),
/// Human readable part mismatch (expected `wasm`, found {0})
HumanReadableMismatch(String),
}
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// RPC server address
#[clap(long, default_value = "http://localhost:11090")]
pub enclave_addr: Endpoint,
/// Blockchain node gRPC URL
#[arg(short, long, default_value = "tcp://127.0.0.1:9090")]
pub node_addr: Endpoint,
/// Chain-id of MTCS chain
#[arg(long, default_value = "testing")]
pub chain_id: Id,
/// Smart contract address
#[arg(short, long, value_parser = wasm_address)]
pub contract: AccountId,
/// Path to TSP secret key file
#[arg(short, long)]
pub secret: PathBuf,
/// Gas limit for the set-offs submission transaction
#[arg(long, default_value = "900000000")]
pub gas_limit: u64,
}
fn wasm_address(address_str: &str) -> Result<AccountId, AddressError> {
let (hr, _) = bech32_decode(address_str).map_err(AddressError::NotBech32Encoded)?;
if hr != "wasm" {
return Err(AddressError::HumanReadableMismatch(hr));
}
Ok(address_str.parse().unwrap())
}

View file

@ -0,0 +1 @@
pub mod types;

View file

@ -0,0 +1,178 @@
mod cli;
use std::{
error::Error,
fs::{read_to_string, File},
io::{Read, Write},
process::Command,
};
use clap::Parser;
use cosmos_sdk_proto::{
cosmos::{
auth::v1beta1::{
query_client::QueryClient as AuthQueryClient, BaseAccount as RawBaseAccount,
QueryAccountRequest,
},
tx::v1beta1::{service_client::ServiceClient, BroadcastMode, BroadcastTxRequest},
},
traits::Message,
Any,
};
use cosmrs::{
auth::BaseAccount,
cosmwasm::MsgExecuteContract,
crypto::secp256k1::{SigningKey, VerifyingKey},
tendermint::{account::Id as TmAccountId, chain::Id as TmChainId},
tx,
tx::{Fee, Msg, SignDoc, SignerInfo},
AccountId, Coin,
};
use ecies::{PublicKey, SecretKey};
use quartz_cw::msg::{
execute::attested::{Attested, EpidAttestation},
instantiate::{CoreInstantiate, RawInstantiate},
InstantiateMsg,
};
use quartz_proto::quartz::{core_client::CoreClient, InstantiateRequest};
use quartz_relayer::types::InstantiateResponse;
use quartz_tee_ra::IASReport;
use serde_json::{json, Value};
use subtle_encoding::base64;
use tendermint::public_key::Secp256k1 as TmPublicKey;
use crate::cli::Cli;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let mut client = CoreClient::connect(args.enclave_addr.uri().to_string()).await?;
let response = client.instantiate(InstantiateRequest {}).await?;
let response: InstantiateResponse = response.into_inner().try_into()?;
let (config, quote) = response.into_message().into_tuple();
let ias_report = gramine_sgx_ias_report(&quote)?;
println!(
"{}",
serde_json::to_string(&ias_report).expect("infallible serializer")
);
let ias_report: IASReport = serde_json::from_str(&ias_report.to_string())?;
let attestation = EpidAttestation::new(ias_report);
let cw_instantiate_msg = Attested::new(CoreInstantiate::new(config), attestation);
// Read the TSP secret
let secret = {
let mut secret = Vec::new();
let mut tsp_sk_file = File::open(args.secret)?;
tsp_sk_file.read_to_end(secret.as_mut())?;
let secret = base64::decode(secret).unwrap();
SecretKey::parse_slice(&secret).unwrap()
};
let tm_pubkey = {
let pubkey = PublicKey::from_secret_key(&secret);
TmPublicKey::from_sec1_bytes(&pubkey.serialize()).unwrap()
};
let sender = {
let tm_key = TmAccountId::from(tm_pubkey);
AccountId::new("wasm", tm_key.as_bytes()).unwrap()
};
let msgs = vec![MsgExecuteContract {
sender: sender.clone(),
contract: args.contract.clone(),
msg: serde_json::to_string::<RawInstantiate>(&InstantiateMsg(cw_instantiate_msg).into())?
.into_bytes(),
funds: vec![],
}
.to_any()
.unwrap()];
let account = account_info(args.node_addr.uri().clone(), sender.clone()).await?;
let amount = Coin {
amount: 0u128,
denom: "cosm".parse()?,
};
let tx_bytes = tx_bytes(
&secret,
amount,
args.gas_limit,
tm_pubkey,
msgs,
account.sequence,
account.account_number,
&args.chain_id,
)?;
send_tx(args.node_addr.uri().clone(), tx_bytes).await?;
Ok(())
}
pub async fn account_info(
node: impl ToString,
address: impl ToString,
) -> Result<BaseAccount, Box<dyn Error>> {
let mut client = AuthQueryClient::connect(node.to_string()).await?;
let request = tonic::Request::new(QueryAccountRequest {
address: address.to_string(),
});
let response = client.account(request).await?;
let response = RawBaseAccount::decode(response.into_inner().account.unwrap().value.as_slice())?;
let account = BaseAccount::try_from(response)?;
Ok(account)
}
#[allow(clippy::too_many_arguments)]
pub fn tx_bytes(
secret: &SecretKey,
amount: Coin,
gas: u64,
tm_pubkey: VerifyingKey,
msgs: Vec<Any>,
sequence_number: u64,
account_number: u64,
chain_id: &TmChainId,
) -> Result<Vec<u8>, Box<dyn Error>> {
let tx_body = tx::Body::new(msgs, "", 0u16);
let signer_info = SignerInfo::single_direct(Some(tm_pubkey.into()), sequence_number);
let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(amount, gas));
let sign_doc = SignDoc::new(&tx_body, &auth_info, chain_id, account_number)?;
let tx_signed = sign_doc.sign(&SigningKey::from_bytes(&secret.serialize()).unwrap())?;
Ok(tx_signed.to_bytes()?)
}
pub async fn send_tx(node: impl ToString, tx_bytes: Vec<u8>) -> Result<(), Box<dyn Error>> {
let mut client = ServiceClient::connect(node.to_string()).await?;
let request = tonic::Request::new(BroadcastTxRequest {
tx_bytes,
mode: BroadcastMode::Block.into(),
});
let _response = client.broadcast_tx(request).await?;
Ok(())
}
fn gramine_sgx_ias_report(quote: &[u8]) -> Result<Value, Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let quote_file_path = dir.path().join("test.quote");
let datareport_file_path = dir.path().join("datareport");
let datareportsig_file_path = dir.path().join("datareportsig");
let mut quote_file = File::create(quote_file_path.clone())?;
quote_file.write_all(quote)?;
let gramine_sgx_ias_request_output = Command::new("gramine-sgx-ias-request")
.arg("report")
.args(["-g", "51CAF5A48B450D624AEFE3286D314894"])
.args(["-k", "669244b3e6364b5888289a11d2a1726d"])
.args(["-q", &quote_file_path.display().to_string()])
.args(["-r", &datareport_file_path.display().to_string()])
.args(["-s", &datareportsig_file_path.display().to_string()])
.output()?;
println!("{gramine_sgx_ias_request_output:?}");
let report = read_to_string(datareport_file_path)?;
let report_sig = read_to_string(datareportsig_file_path)?;
let ias_report = json!({"report": report, "reportsig": report_sig});
Ok(ias_report)
}

View file

@ -0,0 +1,263 @@
use cosmwasm_std::{HexBinary, StdError};
use k256::ecdsa::VerifyingKey;
use quartz_cw::{
error::Error as QuartzCwError,
state::{Config, Nonce, RawConfig},
};
use quartz_proto::quartz::{
InstantiateResponse as RawInstantiateResponse,
SessionCreateResponse as RawSessionCreateResponse,
SessionSetPubKeyResponse as RawSessionSetPubKeyResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
pub struct InstantiateResponse {
message: InstantiateResponseMsg,
}
impl InstantiateResponse {
pub fn new(config: Config, quote: Vec<u8>) -> Self {
Self {
message: InstantiateResponseMsg { config, quote },
}
}
pub fn quote(&self) -> &[u8] {
&self.message.quote
}
pub fn into_message(self) -> InstantiateResponseMsg {
self.message
}
}
impl TryFrom<RawInstantiateResponse> for InstantiateResponse {
type Error = StdError;
fn try_from(value: RawInstantiateResponse) -> Result<Self, Self::Error> {
let raw_message: RawInstantiateResponseMsg = serde_json::from_str(&value.message)
.map_err(|e| StdError::parse_err("RawInstantiateResponseMsg", e))?;
Ok(Self {
message: raw_message.try_into()?,
})
}
}
impl From<InstantiateResponse> for RawInstantiateResponse {
fn from(value: InstantiateResponse) -> Self {
let raw_message: RawInstantiateResponseMsg = value.message.into();
Self {
message: serde_json::to_string(&raw_message).expect("infallible serializer"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct InstantiateResponseMsg {
config: Config,
quote: Vec<u8>,
}
impl InstantiateResponseMsg {
pub fn into_tuple(self) -> (Config, Vec<u8>) {
(self.config, self.quote)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RawInstantiateResponseMsg {
config: RawConfig,
quote: HexBinary,
}
impl TryFrom<RawInstantiateResponseMsg> for InstantiateResponseMsg {
type Error = StdError;
fn try_from(value: RawInstantiateResponseMsg) -> Result<Self, Self::Error> {
Ok(Self {
config: value.config.try_into()?,
quote: value.quote.into(),
})
}
}
impl From<InstantiateResponseMsg> for RawInstantiateResponseMsg {
fn from(value: InstantiateResponseMsg) -> Self {
Self {
config: value.config.into(),
quote: value.quote.into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SessionCreateResponse {
message: SessionCreateResponseMsg,
}
impl SessionCreateResponse {
pub fn new(nonce: Nonce, quote: Vec<u8>) -> Self {
Self {
message: SessionCreateResponseMsg { nonce, quote },
}
}
pub fn quote(&self) -> &[u8] {
&self.message.quote
}
pub fn into_message(self) -> SessionCreateResponseMsg {
self.message
}
}
impl TryFrom<RawSessionCreateResponse> for SessionCreateResponse {
type Error = StdError;
fn try_from(value: RawSessionCreateResponse) -> Result<Self, Self::Error> {
let raw_message: RawSessionCreateResponseMsg = serde_json::from_str(&value.message)
.map_err(|e| StdError::parse_err("RawSessionCreateResponseMsg", e))?;
Ok(Self {
message: raw_message.try_into()?,
})
}
}
impl From<SessionCreateResponse> for RawSessionCreateResponse {
fn from(value: SessionCreateResponse) -> Self {
let raw_message: RawSessionCreateResponseMsg = value.message.into();
Self {
message: serde_json::to_string(&raw_message).expect("infallible serializer"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SessionCreateResponseMsg {
nonce: Nonce,
quote: Vec<u8>,
}
impl SessionCreateResponseMsg {
pub fn into_tuple(self) -> (Nonce, Vec<u8>) {
(self.nonce, self.quote)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RawSessionCreateResponseMsg {
nonce: HexBinary,
quote: HexBinary,
}
impl TryFrom<RawSessionCreateResponseMsg> for SessionCreateResponseMsg {
type Error = StdError;
fn try_from(value: RawSessionCreateResponseMsg) -> Result<Self, Self::Error> {
Ok(Self {
nonce: value.nonce.to_array()?,
quote: value.quote.into(),
})
}
}
impl From<SessionCreateResponseMsg> for RawSessionCreateResponseMsg {
fn from(value: SessionCreateResponseMsg) -> Self {
Self {
nonce: value.nonce.into(),
quote: value.quote.into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SessionSetPubKeyResponse {
message: SessionSetPubKeyResponseMsg,
}
impl SessionSetPubKeyResponse {
pub fn new(nonce: Nonce, pub_key: VerifyingKey, quote: Vec<u8>) -> Self {
Self {
message: SessionSetPubKeyResponseMsg {
nonce,
pub_key,
quote,
},
}
}
pub fn quote(&self) -> &[u8] {
&self.message.quote
}
pub fn into_message(self) -> SessionSetPubKeyResponseMsg {
self.message
}
}
impl TryFrom<RawSessionSetPubKeyResponse> for SessionSetPubKeyResponse {
type Error = StdError;
fn try_from(value: RawSessionSetPubKeyResponse) -> Result<Self, Self::Error> {
let raw_message: RawSessionSetPubKeyResponseMsg = serde_json::from_str(&value.message)
.map_err(|e| StdError::parse_err("RawSessionSetPubKeyResponseMsg", e))?;
Ok(Self {
message: raw_message.try_into()?,
})
}
}
impl From<SessionSetPubKeyResponse> for RawSessionSetPubKeyResponse {
fn from(value: SessionSetPubKeyResponse) -> Self {
let raw_message: RawSessionSetPubKeyResponseMsg = value.message.into();
Self {
message: serde_json::to_string(&raw_message).expect("infallible serializer"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SessionSetPubKeyResponseMsg {
nonce: Nonce,
pub_key: VerifyingKey,
quote: Vec<u8>,
}
impl SessionSetPubKeyResponseMsg {
pub fn into_tuple(self) -> (VerifyingKey, Vec<u8>) {
(self.pub_key, self.quote)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RawSessionSetPubKeyResponseMsg {
nonce: HexBinary,
pub_key: HexBinary,
quote: HexBinary,
}
impl TryFrom<RawSessionSetPubKeyResponseMsg> for SessionSetPubKeyResponseMsg {
type Error = StdError;
fn try_from(value: RawSessionSetPubKeyResponseMsg) -> Result<Self, Self::Error> {
let pub_key = VerifyingKey::from_sec1_bytes(&value.pub_key)
.map_err(QuartzCwError::from)
.map_err(|e| StdError::generic_err(e.to_string()))?;
Ok(Self {
nonce: value.nonce.to_array()?,
pub_key,
quote: value.quote.into(),
})
}
}
impl From<SessionSetPubKeyResponseMsg> for RawSessionSetPubKeyResponseMsg {
fn from(value: SessionSetPubKeyResponseMsg) -> Self {
Self {
nonce: value.nonce.into(),
pub_key: value.pub_key.to_sec1_bytes().into_vec().into(),
quote: value.quote.into(),
}
}
}