Skip to main content

Step 1: Create project components

Step 1.1: Set up a basic rust project

Create and navigate to a fresh directory using

mkdir my-server && cd my-server

Set up a basic rust project using

cargo init

Step 1.2: Set up dependencies and binaries in Cargo.toml

Open cargo.toml and add the following at the bottom

tokio = { version = "1", features = ["full"] }
clap = { version = "4.0.26", features = ["derive"] }
libsodium-sys-stable = "1.20.4"
rand_core = "0.6"
x25519-dalek = { git="https://github.com/dalek-cryptography/x25519-dalek", features = ["static_secrets"] }
chacha20poly1305 = "0.10.1"
aws-nitro-enclaves-cose = "0.5.0"
hyper = { version = "0.14.29", features = ["client", "http1", "http2", "tcp"] }
serde_cbor = "0.11.2"
openssl = { version = "0.10", features = ["vendored"] }
hex = "0.4.3"

[[bin]]
name = "app"
path = "src/app.rs"

[[bin]]
name = "keygen"
path = "src/keygen.rs"

[[bin]]
name = "loader"
path = "src/loader.rs"

[[bin]]
name = "requester"
path = "src/requester.rs"

[[bin]]
name = "verifier"
path = "src/verifier.rs"

Step 1.3: Create the app server

The app server implements privacy preserving addition of two integers. It handles two types of messages, one to set the data set and the other to request computation over the data set and return the result. It performs a static Diffie-Hellman key exchange to generate a shared key and ensures the data set is encrypted as well as allow only the loader to set data sets.

Create a src/app.rs file with the following contents

use chacha20poly1305::{
aead::{Aead, KeyInit, Payload},
ChaCha20Poly1305,
};
use clap::Parser;
use libsodium_sys::crypto_sign_ed25519_sk_to_curve25519;
use std::error::Error;
use std::fs::File;
use std::io::Read;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use x25519_dalek::x25519;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// ip address of the server <ip:port>
#[clap(short, long, value_parser)]
ip_addr: String,

/// path to private key file
#[arg(short, long)]
secret: String,

/// path to loader public key file
#[arg(short, long)]
loader: String,

/// path to requester public key file
#[arg(short, long)]
requester: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();

println!(
"secret: {}, loader: {}, requester: {}",
cli.secret, cli.loader, cli.requester
);

let mut file = File::open(cli.secret)?;
let mut ed25519_secret = [0; 64];
file.read_exact(&mut ed25519_secret)?;

let mut secret = [0; 32];
if unsafe { crypto_sign_ed25519_sk_to_curve25519(secret.as_mut_ptr(), ed25519_secret.as_mut_ptr()) } != 0 {
return Err("failed to convert ed25519 secret to x25519".into());
}

let mut file = File::open(cli.loader)?;
let mut loader = [0; 32];
file.read_exact(&mut loader)?;

let mut file = File::open(cli.requester)?;
let mut requester = [0; 32];
file.read_exact(&mut requester)?;

let loader_shared = x25519(secret, loader);
let loader_cipher = ChaCha20Poly1305::new(&loader_shared.into());

println!("Listening on: {}", cli.ip_addr);

let listener = TcpListener::bind(cli.ip_addr).await?;

let mut data: Vec<u8> = vec![0, 0];
while let Ok((inbound, _)) = listener.accept().await {
let mut buf: Vec<u8> = Vec::with_capacity(1000);
let (mut ri, mut wi) = tokio::io::split(inbound);
let len = ri.read_to_end(&mut buf).await?;

if buf[0] == 0 {
data = loader_cipher
.decrypt(
buf[1..13].into(),
Payload {
msg: &buf[13..len],
aad: &[0],
},
)
.map_err(|e| "Decrypt failed: ".to_owned() + &e.to_string())?;
wi.write_all(b"Data write suceeded!").await?;
} else if buf[0] == 1 {
let sum = data[0] + data[1];
wi.write_all(b"Result: ").await?;
wi.write_all(sum.to_string().as_bytes()).await?;
} else {
wi.write_all(b"Unknown msg").await?;
}
}

Ok(())
}

Step 1.4: Create the loader client

The loader implements privacy preserving data set provisioning. It performs a static Diffie-Hellman key exchange to generate a shared key and ensures the data set is encrypted before upload.

In this tutorial, the numbers 12 and 43 are loaded with an expected result sum of 55.

Create a /src/loader.rs file with the following contents

use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
ChaCha20Poly1305,
};
use clap::Parser;
use libsodium_sys::crypto_sign_ed25519_pk_to_curve25519;
use std::error::Error;
use std::fs::File;
use std::io::Read;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use x25519_dalek::x25519;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// ip address of the server <ip:port>
#[clap(short, long, value_parser)]
ip_addr: String,

/// path to app public key file
#[arg(short, long)]
app: String,

/// path to private key file
#[arg(short, long)]
secret: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();

println!("secret: {}, app: {}", cli.secret, cli.app);

let mut file = File::open(cli.secret)?;
let mut secret = [0u8; 32];
file.read_exact(&mut secret)?;

let mut file = File::open(cli.app)?;
let mut ed25519_app = [0u8; 32];
file.read_exact(&mut ed25519_app)?;
let mut app = [0; 32];
if unsafe { crypto_sign_ed25519_pk_to_curve25519(app.as_mut_ptr(), ed25519_app.as_mut_ptr()) } != 0 {
return Err("failed to convert ed25519 public key to x25519".into());
}

let app_shared = x25519(secret, app);
let app_cipher = ChaCha20Poly1305::new(&app_shared.into());

let msg = [12, 43];
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let buf = app_cipher
.encrypt(
&nonce,
Payload {
msg: &msg,
aad: &[0],
},
)
.unwrap();

let outbound = TcpStream::connect(cli.ip_addr).await?;
let (mut ro, mut wo) = tokio::io::split(outbound);
wo.write_u8(0).await?;
wo.write_all(nonce.as_slice()).await?;
wo.write_all(buf.as_slice()).await?;
wo.shutdown().await?;

let mut resp = String::with_capacity(1000);
ro.read_to_string(&mut resp).await?;

println!("Repsonse: {}", resp);

Ok(())
}

Step 1.5: Create the requester client

The requester triggers the app server to perform the addition and reports the results.

Create a /src/requester.rs file with the following contents

use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
ChaCha20Poly1305,
};
use clap::Parser;
use std::error::Error;
use std::fs::File;
use std::io::Read;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use x25519_dalek::x25519;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// ip address of the server <ip:port>
#[clap(short, long, value_parser)]
ip_addr: String,

/// path to app public key file
#[arg(short, long)]
app: String,

/// path to private key file
#[arg(short, long)]
secret: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();

println!("secret: {}, app: {}", cli.secret, cli.app);

let mut file = File::open(cli.secret)?;
let mut secret = [0; 32];
file.read_exact(&mut secret)?;

let mut file = File::open(cli.app)?;
let mut app = [0; 32];
file.read_exact(&mut app)?;

let app_shared = x25519(secret, app);
let app_cipher = ChaCha20Poly1305::new(&app_shared.into());

let msg = [12, 43];
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let buf = app_cipher
.encrypt(
&nonce,
Payload {
msg: &msg,
aad: &[0],
},
)
.unwrap();

let outbound = TcpStream::connect(cli.ip_addr).await?;
let (mut ro, mut wo) = tokio::io::split(outbound);
wo.write_u8(1).await?;
wo.write_all(nonce.as_slice()).await?;
wo.write_all(buf.as_slice()).await?;
wo.shutdown().await?;

let mut resp = String::with_capacity(1000);
ro.read_to_string(&mut resp).await?;

println!("Repsonse: {}", resp);

Ok(())
}

Step 1.6: Create the key generator

The key generator generates Curve25519 keys that are used to encrypt/decrypt the data.

Create a src/keygen.rs file with the following contents

use clap::Parser;
use rand_core::OsRng;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use x25519_dalek::{PublicKey, StaticSecret};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// path to private key file
#[arg(short, long)]
secret: String,

/// path to public key file
#[arg(short, long)]
public: String,
}

fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();

println!("private key: {}, public key: {}", cli.secret, cli.public);

let secret = StaticSecret::new(OsRng);
let public = PublicKey::from(&secret);

let mut file = File::create(cli.secret)?;
file.write_all(&secret.to_bytes())?;

let mut file = File::create(cli.public)?;
file.write_all(&public.to_bytes())?;

println!("Generation successful!");

Ok(())
}

Step 1.7: Create the verifier

The verifier queries and verifies attestations from the enclave. It is a critical component that ensures you are communicating with an enclave and not just a server, without which privacy of the data set cannot be maintained.

Create a aws.cert file with the following contents

-----BEGIN CERTIFICATE-----
MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD
VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4
MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL
DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG
BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb
48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE
h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF
R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC
MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW
rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N
IwLz3/Y=
-----END CERTIFICATE-----

Create a src/verifier.rs file with the following contents

use aws_nitro_enclaves_cose::{crypto::Openssl, crypto::SigningPublicKey, CoseSign1};
use clap::Parser;
use hex;
use hyper::{client::Client, Uri};
use openssl::asn1::Asn1Time;
use openssl::error::ErrorStack;
use openssl::x509::{X509VerifyResult, X509};
use serde_cbor::{self, value, value::Value};
use std::collections::BTreeMap;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use tokio;

fn get_all_certs(cert: X509, cabundle: Vec<Value>) -> Result<Vec<X509>, ErrorStack> {
let mut all_certs = Vec::new();
all_certs.push(cert);
for cert in cabundle {
let intermediate_certificate = match cert {
Value::Bytes(b) => b,
_ => unreachable!(),
};
let intermediate_certificate = X509::from_der(&intermediate_certificate)?;
all_certs.push(intermediate_certificate);
}
Ok(all_certs)
}

fn verify_cert_chain(
cert: X509,
cabundle: Vec<Value>,
root_cert_pem: Vec<u8>,
) -> Result<(), Box<dyn Error>> {
let certs = get_all_certs(cert, cabundle)?;
let mut i = 0;
while i < certs.len() - 1 {
let pubkey = certs[i + 1].public_key()?;
let x = certs[i].verify(&pubkey)?;
if !x {
return Err("signature verification failed".into());
}
let x = certs[i + 1].issued(&certs[i]);
if x != X509VerifyResult::OK {
return Err("certificate issuer and subject verification failed".into());
}
let current_time = Asn1Time::days_from_now(0)?;
if certs[i].not_after() < current_time || certs[i].not_before() > current_time {
return Err("certificate timestamp expired/not valid".into());
}
i += 1;
}
let root_cert = X509::from_pem(&root_cert_pem)?;
if &root_cert != certs.last().unwrap() {
return Err("root certificate mismatch".into());
}
Ok(())
}

fn verify(
attestion_doc_cbor: Vec<u8>,
root_cert_pem: Vec<u8>,
pcrs: Vec<String>,
) -> Result<Vec<u8>, Box<dyn Error>> {
let cosesign1 = CoseSign1::from_bytes(&attestion_doc_cbor)?;
let payload = cosesign1.get_payload::<Openssl>(None as Option<&dyn SigningPublicKey>)?;
let mut attestation_doc: BTreeMap<Value, Value> =
value::from_value(serde_cbor::from_slice::<Value>(&payload)?)?;
let document_pcrs_arr = attestation_doc
.remove(&value::to_value("pcrs").unwrap())
.ok_or(Box::<dyn Error>::from(
"pcrs key not found in attestation doc",
))?;
let mut document_pcrs_arr: BTreeMap<Value, Value> = value::from_value(document_pcrs_arr)?;
for i in 0..3 {
let pcr = document_pcrs_arr
.remove(&value::to_value(i).unwrap())
.ok_or(Box::<dyn Error>::from(format!("pcr{i} not found")))?;
let pcr = match pcr {
Value::Bytes(b) => b,
_ => unreachable!(),
};
if hex::encode(pcr) != pcrs[i] {
return Err(format!("pcr{i} match failed").into());
}
}
let enclave_certificate = attestation_doc
.remove(&value::to_value("certificate").unwrap())
.ok_or(Box::<dyn Error>::from(
"certificate key not found in attestation doc",
))?;
let enclave_certificate = match enclave_certificate {
Value::Bytes(b) => b,
_ => unreachable!(),
};
let enclave_certificate = X509::from_der(&enclave_certificate)?;
let pub_key = enclave_certificate.public_key()?;
let verify_result = cosesign1.verify_signature::<Openssl>(&pub_key)?;

if !verify_result {
return Err("cose signature verfication failed".into());
}

let cabundle = attestation_doc
.remove(&value::to_value("cabundle").unwrap())
.ok_or(Box::<dyn Error>::from(
"cabundle key not found in attestation doc",
))?;

let mut cabundle: Vec<Value> = value::from_value(cabundle)?;
cabundle.reverse();

verify_cert_chain(enclave_certificate, cabundle, root_cert_pem)?;

let public_key = attestation_doc
.remove(&value::to_value("public_key").unwrap())
.ok_or(Box::<dyn Error>::from(
"public key not found in attestation doc",
))?;
let public_key = match public_key {
Value::Bytes(b) => b,
_ => unreachable!(),
};

Ok(public_key)
}

#[tokio::main]
async fn get_attestation_doc(endpoint: String) -> Result<Vec<u8>, Box<dyn Error>> {
let client = Client::new();
let res = client.get(endpoint.parse::<Uri>()?).await?;
let buf = hyper::body::to_bytes(res).await?;
Ok(buf.to_vec())
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// endpoint of the attestation server http://<ip:port>
#[clap(short, long, value_parser)]
endpoint: String,

/// path to app public key file
#[arg(short, long)]
app: String,

/// expected pcr0
#[arg(short, long)]
pcr0: String,

/// expected pcr1
#[arg(short, long)]
pcr1: String,

/// expected pcr2
#[arg(short, long)]
pcr2: String,
}

fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();

let pcrs = vec![cli.pcr0, cli.pcr1, cli.pcr2];
let attestation_doc = get_attestation_doc(cli.endpoint)?;
let cert = include_bytes!("../aws.cert").to_vec();

let pub_key = verify(attestation_doc, cert, pcrs)?;
println!("verification successful with pubkey: {:?}", pub_key);

let mut file = File::create(cli.app)?;
file.write_all(pub_key.as_slice())?;

Ok(())
}

Step 1.8: Build the components

Build all the components using

rustup target add `uname -m`-unknown-linux-musl
cargo build --release --target `uname -m`-unknown-linux-musl
note

uname -m is used to identify the cpu architecture (x86_64 or aarch64) of machine building the application. These commands build the application for target cpu architecture as same as that of builder machine.

Step 1.9: Generate keys for the loader and requester

Generate keys using

./target/`uname -m`-unknown-linux-musl/release/keygen --secret loader.sec --public loader.pub
./target/`uname -m`-unknown-linux-musl/release/keygen --secret requester.sec --public requester.pub