Initial implementation

This commit is contained in:
Valentyn Faychuk 2024-08-13 02:22:34 +00:00
commit c19c42ccd7
Signed by: valy
GPG Key ID: F1AB995E20FEADC5
20 changed files with 759 additions and 0 deletions

4
.gitignore vendored Normal file

@ -0,0 +1,4 @@
target
Cargo.lock
client_instance
server_instance

71
Cargo.toml Normal file

@ -0,0 +1,71 @@
[package]
name = "occlum-ratls"
version = "0.4.5"
edition = "2021"
authors = ["Ivan Chirkin <chirkin.ivan@gmail.com>"]
description = "Lib for remote attestation between occlum instances"
license = "MIT OR Apache-2.0"
repository = "https://github.com/aggregion/occlum-ratls"
keywords = ["occlum", "rustls", "ratls"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
x509-parser = "0.15.0"
#rustls = { version = "0.21", features = [
rustls = { version = "0.20.9", features = [
"dangerous_configuration",
"logging"
] }
log = "0.4.17"
rcgen = "0.12.1"
occlum-sgx = "^0.1.12"
ring = "0.17.8"
hex = "0.4"
[dependencies.actix-web]
version = "4.3.1"
features = ["rustls-0_20"]
optional = true
[dependencies.actix-http]
version = "3.3"
features = ["http2", "ws"]
optional = true
[dependencies.actix-service]
version = "2"
optional = true
[dependencies.reqwest]
version = "=0.11.10"
default-features = false
features = ["__rustls"]
optional = true
[dev-dependencies.env_logger]
version = "0.10.0"
[dev-dependencies.tokio]
version = "1"
features = ["full"]
[dev-dependencies.cargo-husky]
version = "1"
default-features = false
features = ["precommit-hook", "run-cargo-test", "run-cargo-clippy"]
[features]
default = []
occlum = []
reqwest = ["dep:reqwest"]
actix-web = ["dep:actix-web", "actix-service", "actix-http"]
[[example]]
name = "server"
required-features = ["actix-web"]
[[example]]
name = "client"
required-features = ["reqwest"]

12
README.md Normal file

@ -0,0 +1,12 @@
# Occlum SGX Remote Attestation integrated in TLS connection
Steps to test the project:
```
occlum-cargo build --example server --features="occlum,actix-web"
strip -s target/x86_64-unknown-linux-musl/debug/examples/server
./build_server.sh
occlum-cargo build --example client --features="occlum,reqwest"
strip -s target/x86_64-unknown-linux-musl/debug/examples/client
./build_client.sh
```

11
build_client.sh Executable file

@ -0,0 +1,11 @@
#!/bin/bash
set -e
# initialize occlum workspace
rm -rf client_instance && mkdir client_instance && cd client_instance
occlum init && rm -rf image
copy_bom -f ../client.yaml --root image --include-dir /opt/occlum/etc/template
occlum build
occlum run /bin/client

11
build_server.sh Executable file

@ -0,0 +1,11 @@
#!/bin/bash
set -e
# initialize occlum workspace
rm -rf server_instance && mkdir server_instance && cd server_instance
occlum init && rm -rf image
copy_bom -f ../server.yaml --root image --include-dir /opt/occlum/etc/template
occlum build
occlum run /bin/server

7
client.yaml Normal file

@ -0,0 +1,7 @@
includes:
- base.yaml
targets:
- target: /bin
copy:
- files:
- ../target/x86_64-unknown-linux-musl/debug/examples/client

27
examples/client.rs Normal file

@ -0,0 +1,27 @@
use occlum_ratls::prelude::*;
use reqwest::ClientBuilder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("trace"));
let mrsigner_hex = "83D719E77DEACA1470F6BAF62A4D774303C899DB69020F9C70EE1DFC08C7CE9E";
let mut mrsigner = [0u8; 32];
hex::decode_to_slice(mrsigner_hex, &mut mrsigner)?;
let client = ClientBuilder::new()
.use_ratls(
RaTlsConfig::new().allow_instance_measurement(
InstanceMeasurement::new()
.with_mrsigners(vec![SGXMeasurement::new(mrsigner)])
.with_product_ids(vec![0]),
),
)
.build()?;
let res = client.get("https://127.0.0.1:8000").send().await?;
let data = res.text().await?;
println!("response: {}", data);
Ok(())
}

37
examples/server.rs Normal file

@ -0,0 +1,37 @@
use actix_web::{get, App, HttpServer};
use occlum_ratls::prelude::*;
use std::net::SocketAddr;
#[get("/")]
async fn index() -> String {
format!("Hello world!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("trace"));
let mrsigner_hex = "83D719E77DEACA1470F6BAF62A4D774303C899DB69020F9C70EE1DFC08C7CE9E";
let mut mrsigner = [0u8; 32];
hex::decode_to_slice(mrsigner_hex, &mut mrsigner).expect("mrsigner decoding failed");
HttpServer::new(|| App::new().service(index))
.bind_ratls(
SocketAddr::from(([127, 0, 0, 1], 8000)),
RaTlsConfig::new()
.allow_instance_measurement(
InstanceMeasurement::new().with_mrsigners(vec![SGXMeasurement::new(mrsigner)]),
)
.allow_instance_measurement(
InstanceMeasurement::new()
.with_mrenclaves(vec![
SGXMeasurement::new([0u8; 32]),
SGXMeasurement::new([1u8; 32]),
])
.with_product_ids(vec![0, 2]),
),
)
.unwrap()
.run()
.await
}

7
server.yaml Normal file

@ -0,0 +1,7 @@
includes:
- base.yaml
targets:
- target: /bin
copy:
- files:
- ../target/x86_64-unknown-linux-musl/debug/examples/server

157
src/cert.rs Normal file

@ -0,0 +1,157 @@
use std::error::Error;
#[cfg(feature = "occlum")]
use crate::utils::hash_sha512;
use crate::{error::RaTlsError, RaTlsConfig};
use log::error;
#[cfg(feature = "occlum")]
use occlum_sgx::SGXQuote;
use rustls::{
sign::{any_supported_type, CertifiedKey},
Certificate, PrivateKey,
};
use rcgen::{
Certificate as GenCertificate, CertificateParams, CustomExtension, DistinguishedName, KeyPair,
};
#[cfg(feature = "occlum")]
use x509_parser::{nom::Parser, oid_registry::Oid, prelude::X509CertificateParser, public_key};
pub trait CertificateBuilder: Send + Sync {
fn build(&self) -> Result<CertifiedKey, RaTlsError>;
}
struct RaTlsCertifiedKey {
cert_der: Vec<u8>,
key_der: Vec<u8>,
}
pub struct RaTlsCertificateBuilder {
common_name: String,
}
impl Default for RaTlsCertificateBuilder {
fn default() -> Self {
Self {
common_name: "RATLS".to_string(),
}
}
}
pub const REPORT_OID: [u64; 5] = [1, 2, 840, 113741, 1];
impl RaTlsCertificateBuilder {
pub fn new() -> Self {
Self {
..Default::default()
}
}
pub fn with_common_name(self, cn: String) -> Self {
Self { common_name: cn }
}
fn build_internal(&self) -> Result<RaTlsCertifiedKey, Box<dyn Error>> {
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, self.common_name.clone());
distinguished_name.push(rcgen::DnType::CountryName, "US");
distinguished_name.push(rcgen::DnType::OrganizationName, "Aggregion");
let mut params = CertificateParams::default();
let key_pair = KeyPair::generate(params.alg)?;
let quote = self.get_quote(&key_pair)?;
params.key_pair = Some(key_pair);
params.distinguished_name = distinguished_name;
params.custom_extensions = vec![CustomExtension::from_oid_content(&REPORT_OID, quote)];
let crt = GenCertificate::from_params(params)?;
Ok(RaTlsCertifiedKey {
cert_der: crt.serialize_der()?,
key_der: crt.serialize_private_key_der(),
})
}
#[cfg(not(feature = "occlum"))]
fn get_quote(&self, _: &KeyPair) -> Result<Vec<u8>, Box<dyn Error>> {
Ok([0u8; 32].to_vec())
}
#[cfg(feature = "occlum")]
fn get_quote(&self, key_pair: &KeyPair) -> Result<Vec<u8>, Box<dyn Error>> {
let public_key = key_pair.public_key_raw().to_vec();
let report_data = hash_sha512(public_key);
let quote = SGXQuote::from_report_data(&report_data)?;
Ok(quote.as_slice().to_vec())
}
}
impl CertificateBuilder for RaTlsCertificateBuilder {
fn build(&self) -> Result<CertifiedKey, RaTlsError> {
self.build_internal()
.map(|k| {
let sign_key = any_supported_type(&PrivateKey(k.key_der)).unwrap();
CertifiedKey::new(vec![Certificate(k.cert_der)], sign_key)
})
.map_err(|e| {
let err = RaTlsError::CertificateBuildError(e.to_string());
error!("{}", err);
err
})
}
}
pub trait RaTlsCertificate {
fn verify_quote(&self, config: &RaTlsConfig) -> Result<(), Box<dyn Error>>;
}
impl RaTlsCertificate for rustls::Certificate {
#[cfg(not(feature = "occlum"))]
fn verify_quote(&self, _: &RaTlsConfig) -> Result<(), Box<dyn Error>> {
Ok(())
}
#[cfg(feature = "occlum")]
fn verify_quote(&self, config: &RaTlsConfig) -> Result<(), Box<dyn Error>> {
let mut parser = X509CertificateParser::new().with_deep_parse_extensions(true);
let (_, x509) = parser.parse(self.as_ref()).unwrap();
let report_oid = Oid::from(&REPORT_OID).unwrap();
println!("{:?}", x509);
if let Ok(Some(report)) = x509.get_extension_unique(&report_oid) {
let quote = SGXQuote::from_slice(report.value)?;
quote.verify()?;
let public_key = x509.public_key().parsed()?;
let public_key = match public_key {
public_key::PublicKey::EC(key) => key.data().to_vec(),
_ => return Err("Unexpected public key type".into()),
};
let report_data = &*quote.report_data();
if hash_sha512(public_key) != report_data {
return Err("Invalid quote report data".into());
}
println!("x509 pk matches report data");
config.is_allowed_quote(&quote)?;
Ok(())
} else {
Err("Not found quote extension".into())
}
}
}

74
src/client.rs Normal file

@ -0,0 +1,74 @@
use std::sync::Arc;
use crate::{
cert::{CertificateBuilder, RaTlsCertificate, RaTlsCertificateBuilder},
RaTlsConfig, RaTlsConfigBuilder, RaTlsError,
};
use rustls::{
client::{ResolvesClientCert, ServerCertVerified, ServerCertVerifier},
sign::CertifiedKey,
ClientConfig,
};
pub struct RaTlsServerCertVerifier {
config: RaTlsConfig,
}
impl RaTlsServerCertVerifier {
pub fn new(config: RaTlsConfig) -> Self {
Self { config }
}
}
impl ServerCertVerifier for RaTlsServerCertVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> Result<ServerCertVerified, rustls::Error> {
end_entity
.verify_quote(&self.config)
.map_err(|e| rustls::Error::General(e.to_string()))?;
Ok(ServerCertVerified::assertion())
}
}
pub struct RaTlsClientCertResolver {
cert: Arc<CertifiedKey>,
}
impl RaTlsClientCertResolver {
pub fn new() -> Result<Self, RaTlsError> {
let builder = RaTlsCertificateBuilder::new().with_common_name("Client".to_string());
let cert = builder.build().map(Arc::new)?;
Ok(Self { cert })
}
}
impl ResolvesClientCert for RaTlsClientCertResolver {
fn resolve(
&self,
_acceptable_issuers: &[&[u8]],
_sigschemes: &[rustls::SignatureScheme],
) -> Option<Arc<CertifiedKey>> {
Some(self.cert.clone())
}
fn has_certs(&self) -> bool {
true
}
}
impl RaTlsConfigBuilder<ClientConfig> for ClientConfig {
fn from_ratls_config(config: RaTlsConfig) -> Result<Self, RaTlsError> {
Ok(Self::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(RaTlsServerCertVerifier::new(config)))
.with_client_cert_resolver(Arc::new(RaTlsClientCertResolver::new()?)))
}
}

138
src/config.rs Normal file

@ -0,0 +1,138 @@
use crate::{RaTlsConfigBuilder, RaTlsError};
#[cfg(feature = "occlum")]
use occlum_sgx::SGXQuote;
pub use occlum_sgx::SGXMeasurement;
use rustls::{ClientConfig, ServerConfig};
#[derive(Default)]
pub struct RaTlsConfig {
#[cfg(feature = "occlum")]
pub(crate) allowed_instances: Vec<InstanceMeasurement>,
}
#[cfg(feature = "occlum")]
#[derive(Default, Clone)]
pub struct InstanceMeasurement {
pub(crate) mrsigners: Option<Vec<SGXMeasurement>>,
pub(crate) mrenclaves: Option<Vec<SGXMeasurement>>,
pub(crate) product_ids: Option<Vec<u16>>,
pub(crate) versions: Option<Vec<u16>>,
}
#[cfg(feature = "occlum")]
impl InstanceMeasurement {
pub fn new() -> Self {
Self::default()
}
pub fn with_mrsigners(self, mrsigners: Vec<SGXMeasurement>) -> Self {
Self {
mrsigners: Some(mrsigners),
..self
}
}
pub fn with_mrenclaves(self, mrenclaves: Vec<SGXMeasurement>) -> Self {
Self {
mrenclaves: Some(mrenclaves),
..self
}
}
pub fn with_product_ids(self, product_ids: Vec<u16>) -> Self {
Self {
product_ids: Some(product_ids),
..self
}
}
pub fn with_versions(self, versions: Vec<u16>) -> Self {
Self {
versions: Some(versions),
..self
}
}
pub(crate) fn check_quote_measurements(&self, quote: &SGXQuote) -> bool {
println!("mrsigner {}", quote.mrsigner());
println!("mrenclave {}", quote.mrenclave());
println!("productid {}", quote.product_id());
println!("version {}", quote.version());
println!("-----------------------------------------------------------------");
println!("mrsigners {:?}", self.mrsigners);
println!("mrenclaves {:?}", self.mrenclaves);
println!("product_ids {:?}", self.product_ids);
println!("versions {:?}", self.versions);
let mut result = false;
if let Some(mrsigners) = &self.mrsigners {
result = true;
let value = quote.mrsigner();
if !mrsigners.contains(&value) {
return false;
}
}
if let Some(mrenclaves) = &self.mrenclaves {
result = true;
let value = quote.mrenclave();
if !mrenclaves.contains(&value) {
return false;
}
}
if let Some(product_ids) = &self.product_ids {
result = true;
let value = quote.product_id();
if !product_ids.contains(&value) {
return false;
}
}
if let Some(versions) = &self.versions {
result = true;
let value = quote.version();
if !versions.contains(&value) {
return false;
}
}
result
}
}
impl RaTlsConfig {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "occlum")]
pub fn allow_instance_measurement(mut self, instance_measurement: InstanceMeasurement) -> Self {
self.allowed_instances.push(instance_measurement);
self
}
#[cfg(feature = "occlum")]
pub(crate) fn is_allowed_quote(&self, quote: &SGXQuote) -> Result<(), RaTlsError> {
match self
.allowed_instances
.iter()
.any(|im| im.check_quote_measurements(quote))
{
true => Ok(()),
false => Err(RaTlsError::QuoteVerifyError(format!(
"{:?} is not allowed",
quote
))),
}
}
pub fn into_server_config(self) -> Result<ServerConfig, RaTlsError> {
ServerConfig::from_ratls_config(self)
}
pub fn into_client_config(self) -> Result<ClientConfig, RaTlsError> {
ClientConfig::from_ratls_config(self)
}
}

20
src/error.rs Normal file

@ -0,0 +1,20 @@
use std::{error::Error, fmt::Display};
#[derive(Debug)]
pub enum RaTlsError {
CertificateBuildError(String),
QuoteVerifyError(String),
}
impl Display for RaTlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
RaTlsError::CertificateBuildError(ref message) => {
write!(f, "CertificateBuildError: {}", message)
}
RaTlsError::QuoteVerifyError(ref message) => write!(f, "QuoteVerifyError: {}", message),
}
}
}
impl Error for RaTlsError {}

48
src/http/actix_web.rs Normal file

@ -0,0 +1,48 @@
use std::net;
use actix_http::{body::MessageBody, Request};
use actix_service::{IntoServiceFactory, ServiceFactory};
use actix_web::dev::{AppConfig, Response};
use actix_web::*;
use rustls::ServerConfig;
use crate::{config::RaTlsConfig, RaTlsConfigBuilder};
pub trait ActixWebWithRatls<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig>,
S::Error: Into<Error>,
S::InitError: std::fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
{
fn bind_ratls<A: net::ToSocketAddrs>(
self,
addr: A,
config: RaTlsConfig,
) -> Result<HttpServer<F, I, S, B>, std::io::Error>;
}
impl<F, I, S, B> ActixWebWithRatls<F, I, S, B> for HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error>,
S::InitError: std::fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody + 'static,
{
fn bind_ratls<A: net::ToSocketAddrs>(
self,
addr: A,
config: RaTlsConfig,
) -> Result<Self, std::io::Error> {
let config = ServerConfig::from_ratls_config(config).map_err(|e|
std::io::Error::new(std::io::ErrorKind::Other, format!("{}", e))
)?;
self.bind_rustls(addr, config)
}
}

4
src/http/mod.rs Normal file

@ -0,0 +1,4 @@
#[cfg(feature = "actix-web")]
pub mod actix_web;
#[cfg(feature = "reqwest")]
pub mod reqwest;

14
src/http/reqwest.rs Normal file

@ -0,0 +1,14 @@
use reqwest::ClientBuilder;
use rustls::ClientConfig;
use crate::{RaTlsConfig, RaTlsConfigBuilder};
pub trait ReqwestUseRatls {
fn use_ratls(self, config: RaTlsConfig) -> ClientBuilder;
}
impl ReqwestUseRatls for ClientBuilder {
fn use_ratls(self, config: RaTlsConfig) -> ClientBuilder {
self.use_preconfigured_tls(ClientConfig::from_ratls_config(config).unwrap())
}
}

28
src/lib.rs Normal file

@ -0,0 +1,28 @@
mod cert;
mod client;
mod config;
mod error;
mod http;
mod server;
#[cfg(feature = "occlum")]
mod utils;
pub mod prelude;
pub use crate::config::RaTlsConfig;
#[cfg(feature = "occlum")]
pub use crate::config::InstanceMeasurement;
pub use crate::error::RaTlsError;
pub use occlum_sgx::SGXMeasurement;
#[cfg(feature = "actix-web")]
pub use crate::http::actix_web;
#[cfg(feature = "reqwest")]
pub use crate::http::reqwest;
pub trait RaTlsConfigBuilder<T> {
fn from_ratls_config(config: RaTlsConfig) -> Result<T, RaTlsError>;
}

11
src/prelude.rs Normal file

@ -0,0 +1,11 @@
pub use crate::RaTlsConfig;
pub use crate::SGXMeasurement;
#[cfg(feature = "occlum")]
pub use crate::InstanceMeasurement;
#[cfg(feature = "reqwest")]
pub use crate::reqwest::ReqwestUseRatls;
#[cfg(feature = "actix-web")]
pub use crate::actix_web::ActixWebWithRatls;

67
src/server.rs Normal file

@ -0,0 +1,67 @@
use rustls::{server::{ClientCertVerified, ClientCertVerifier, ResolvesServerCert}, sign::CertifiedKey, Certificate, Error, ServerConfig, DistinguishedNames};
use std::{sync::Arc, time::SystemTime};
use crate::{
cert::{CertificateBuilder, RaTlsCertificate, RaTlsCertificateBuilder},
RaTlsConfig, RaTlsConfigBuilder, RaTlsError,
};
pub struct RaTlsClientCertVerifier {
config: RaTlsConfig,
}
impl RaTlsClientCertVerifier {
pub fn new(config: RaTlsConfig) -> Self {
Self { config }
}
}
impl ClientCertVerifier for RaTlsClientCertVerifier {
fn verify_client_cert(
&self,
end_entity: &Certificate,
_intermediates: &[Certificate],
_now: SystemTime,
) -> Result<ClientCertVerified, Error> {
end_entity.verify_quote(&self.config).map_err(|e| {
println!("{:?}", e);
rustls::Error::General(e.to_string())
})?;
Ok(ClientCertVerified::assertion())
}
fn client_auth_root_subjects(&self) -> Option<DistinguishedNames> {
Some(DistinguishedNames::new())
}
}
pub struct RaTlsServerCertResolver {
cert: Arc<CertifiedKey>,
}
impl RaTlsServerCertResolver {
pub fn new() -> Result<Self, RaTlsError> {
let builder = RaTlsCertificateBuilder::new().with_common_name("Client".to_string());
let cert = builder.build().map(Arc::new)?;
Ok(Self { cert })
}
}
impl ResolvesServerCert for RaTlsServerCertResolver {
fn resolve(
&self,
_client_hello: rustls::server::ClientHello,
) -> Option<std::sync::Arc<CertifiedKey>> {
Some(self.cert.clone())
}
}
impl RaTlsConfigBuilder<ServerConfig> for ServerConfig {
fn from_ratls_config(config: RaTlsConfig) -> Result<Self, RaTlsError> {
Ok(Self::builder()
.with_safe_defaults()
.with_client_cert_verifier(Arc::new(RaTlsClientCertVerifier::new(config)))
.with_cert_resolver(Arc::new(RaTlsServerCertResolver::new()?)))
}
}

11
src/utils.rs Normal file

@ -0,0 +1,11 @@
use ring::digest::{Context, SHA512};
pub fn hash_sha512(input: Vec<u8>) -> [u8; 64] {
let mut context = Context::new(&SHA512);
context.update(input.as_ref());
let result = context.finish();
let digest = result.as_ref();
let mut output = [0u8; 64];
output.copy_from_slice(digest);
output
}