Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Platform Abstraction Layer between trusted host and trusted applications. #2

Draft
wants to merge 1 commit into
base: no_std_pr
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions trusted/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Trusted Computations
7 changes: 7 additions & 0 deletions trusted/pal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "trusted-pal"
version = "0.1.0"
authors = ["people"]

[dependencies]
anyhow = { version = "1", default-features = false }
1 change: 1 addition & 0 deletions trusted/pal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Platform Abstraction Layer for the Oak Platform
6 changes: 6 additions & 0 deletions trusted/pal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![no_std]

extern crate alloc;
extern crate anyhow;

pub mod pal;
174 changes: 174 additions & 0 deletions trusted/pal/src/pal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! This module expresses a simple abstraction layer between the trusted host and
//! trusted application it runs. The main goal is to abstract away details of the
//! trusted hosting and support mutiple host implementations. The host implementations
//! differ in their capabilities with some imposing significant restrictions but
//! offering better security and privacy guarantees. Hence the abstraction layer
//! focuses on the most restricted host and assumes restricted kernel.
//!
//! Restricted kernel limits the execution model to processing signals (or rather
//! messages) received from the untrusted launcher over a communication channel. Given
//! this limitation the abstraction layer assumes that the trusted application performs
//! processing in response to the signal from the untrusted launcher which can be
//! either a message or a clock tick. In other words the abstraction layer defines
//! a poll based execution model driven by a signal generating loop in untrusted
//! launcher.
//!
//! [Application] trait must be implemented by a concrete trusted application to
//! receive signals from the trusted host originating from untrusted launcher.
//!
//! [Host] trait must be implemented by a concrete trusted host to expose its capabilities
//! to the trusted application.

use alloc::boxed::Box;
use alloc::vec::Vec;
use anyhow::Result;

type Message = Vec<u8>;

/// Represents a set of claims for the trusted host and application, provides capability
/// sign and verify application data.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the sign and verify methods defined on the Attestation trait? I would expect to see them on the Host trait perhaps

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was that the attestation encapsulates signing key pair (only a public signing key if the attestation have been serialized and sent over the wire) that is used to sign (if private key is present) or verify the data. Specifically the verify functionality is mainly targeting scenarios where one TEE sends its serialized attestation along with signed data to another TEE, another TEE deserializes the attestation and then verifies the signed data using the public signing key encapsulated in the attestation. Does that makes sense?

pub trait Attestation {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could have a method for getting Evidence, but the Attestation trait shouldn't itself represent an evidence, since Attestation will represent an API to the Restricted Kernel.

It also could be renamed to Attester to comply with the RATS standard.

/// TODO: Expose application identity, provenance and configuration information such
/// that applications can assert the properties of the peer.

/// Serializes attestation such that it can be transferred over the wire to a peer
/// and attested by the peer.
///
/// # Returns
///
/// An error if failed to serialize into a byte string, a success otherwise.
///
/// # Note
///
/// The serialized representation must contain enough information to
/// verify the attestation itself (e.g. that it has been produced by a proper root
/// of trust and that it represents a trusted chain of components) and to verify
/// signed application data (e.g. trusted applications sharing data with each other).
///
/// For example, before two trusted application peers can start interacting they must
/// establish trust. The trusted application initiating the interaction will first
/// obtain its own attestation, sign the data it wants to share with the peer,
/// share serialized attestation, the data and its signature with the peer. The peer
/// deserializes and verifies the attestation (see `Host::verify_peer_attestation`),
/// uses the attestation to verify the data signatures, performs similar handshake
/// with the peer.
fn serialize(&self) -> Result<Vec<u8>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be its own trait and included as part of this trait.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will factor it out into a separate trait.


/// Signs a byte string using private signing key.
///
/// # Arguments
///
/// * `data` - The byte string to sign.
///
/// # Returns
///
/// An error if the attestation doesn't possess the signing capability (e.g. this
/// instance has been transferred over the wire and meant to be used for verification)
/// or if the signing failed, a success otherwise.
fn sign(&self, data: &[u8]) -> Result<Vec<u8>>;

/// Verifies signature of a byte string using public signing key.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the semantics of this method. What public key does it expect to use to verify the signature?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attestation encapsulates attestation evidence and application signing keys that are generated at the start of the application and remain unchanged through its lifetime. The signing keys in turn are also signed using the keys from the attestation evidence. When the attestation is obtained from the host it has both public and private keys and enables application data signing. When the attestation is serialized and sent over to a peer only a public signing key is serialized. The peer can then use deserialized attestation and encapsulated public signing key to verify signed application data.

///
/// # Arguments
///
/// * `data` - The byte string to verify signature for.
/// * `signature` - The signature of the byte string.
///
/// # Returns
///
/// An error if the signature cannot be verifed or is invalid, a success otherwise.
fn verify(&self, data: &[u8], signature: &[u8]) -> Result<()>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think SignatureVerifier can be a separate entity, which will take a public key taken from the Evidence and certificate containing the signed data, and verify the certificate.

And AttestationVerifier will take an Evidence and reference values (or a policy) and verify it.


/// Gets serialized public key used for signing by the trusted application.
/// The signing key is generated at the start of the trusted application and
/// remains unchanged through the lifetime.
///
/// # Note
///
/// Public signing key can be used to derive a trusted application identity.
/// For example, a trusted application that represents a node in Raft cluster
/// running inside of a group trusted hosts, it is important that Raft node
/// identity cannot be forged, does not require coordination to pick one and
/// has low collision chance. Hash of the public signing key is an identity
/// mechanism that is compliant with these requirements.
fn public_signing_key(&self) -> Vec<u8>;
}

/// Represents a trusted host, abstracting away the details of how trusted
/// application is hosted (e.g. trusted host can be restricted kernel bare
/// metal based encrypted virtual machine or a linux kernel docker based
/// encrypted virtual machine).
pub trait Host {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be called Platform or something like that?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, platform would be a better name here, will rename it.

/// Gets attestation that represens a set of claims about the trusted application
/// and its host. The attestation is generated after the trusted application and
/// host are fully initialiezed, the attestation doesn't change throughout the
/// lifetime.
fn get_self_attestation(&self) -> Box<dyn Attestation>;

/// Gets serialized configuration that stays immutable through the lifetime of
/// the trusted application.
fn get_self_config(&self) -> Vec<u8>;

/// Sends messages through the communication channel that connects the trusted
/// application to the untrusted launcher.
///
/// # Argumnets
///
/// * `messages` - A set of messsages to send through the channel.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to send multiple messages atomically? Would a simpler API that allows sending a single message not be sufficient? The application can always batch multiple actions in a single message if desired, so it seems unnecessary to add another level of grouping in this API.
Also I am hoping we could expose a raw micro RPC transport to talk to the host, and the application will use that in whichever way it wants; would that be enough for your use case?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here send messages has a subtle semantic. Technically it doesn't send messages, it buffers them until the call to the receive_messages finishes. The application generates messages to be sent during the receive_messages call and then calls send_messages to be sent after receive_messages call completes. This is mainly due to the fact that in microRPC the call can only be issued from the Untrusted Launcher side. Hence, the send_messages buffers the messages to be sent and then pack them into a response for the microRPC.

With regards to the sending one or many messages at once, given the buffering semantic, it doesn't matter. However, processing by the application even of a single incoming message may result in generation of multiple messages (for example, a proposal message for the replicated log will result num replicas messages for appending entries).

Given that the purpose of this interface to abstract away the details of the platform (restricted kernel works with microRPC and Docker works with gRPC) the microRPC is the detail of how host (platform) is implemented.

///
/// # Returns
///
/// Error if the communication channel is irrepairably broken, a success otherise.
fn send_messages(&mut self, messages: &[Message]) -> Result<()>;

/// Attempts to deserialize peer attestation and perform initial verification
/// of the attestation. The application specific verification (e.g. ensuring
/// that attestation points to a particular binary and configuration) is the
/// responsibility of the trusted application.
///
/// # Arguments
///
/// * `peer_attestation` - A serialized attestation of a peer trusted application.
///
/// # Returns
///
/// A deserialized and initially verified peer attestation if success, an error
/// otherwise. An error may be caused by malformed serialized representation or
/// by failing verification.
fn verify_peer_attestation(&self, peer_attestation: &[u8]) -> Result<Box<dyn Attestation>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of this is only constrained by the Attestation trait, so I'm not sure how it's expected to be used, could you elaborate?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's imagine there is a single node Raft cluster. We are trying to add another node to the cluster by sending configuration change to the leader. The leader tries to send append entries to the new node. Before doing so, we need to establish trust and secure channel. The leader obtains its attestation from the host (platform), generates its public key for the HPKE and signs it, sends both in an initiate handshake message. The follower receives the initiate handshake message, gets serialized leader attestation and asks the host (platform) to verify (check that the DICE chain is valid) and deserialize it. Now that the follower has the deserialized attestation containing public signing key it can verify signed public key of the leader for the HPKE. Afterwards the follower performs symmetric action (gets its attestation, generates HPKE response and sign it, serializes attestation and sends it and signed HPKE response to the leader). Hope this helps.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per previous comment, AttestationVerifier could be a separate entity, since it doesn't need anything from the Restricted Kernel. You just provide it with a policy (which itself could be generated from self evidence, i.e. policy = is a given evidence equal to self evidence) and it verifies it.

}

/// Represents a trusted application running inside a trusted host. The trusted
/// application is considered passive and performs execution in response to
/// receiving messages through the communication channel that connects the trusted
/// application to the untrusted launcher. In the absence of messages to be processed
/// the trusted application will receive periodically empty set of messages to allow
/// trusted application to make progress based on time change.
pub trait Application {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the application may also just implement the same micro RPC transport trait. It can just always return an empty message back.

Would that work?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is possible to implement the transport trait, however this will lead to two consequences:

  • the access to the host (platform) will have to be exposed separately (say, through a separate init method on the application)
  • the clock reading will have to be either exposed separately through the host (platform) or encapsulated into the messages themselves.
    I feel like these two consequences will make the trait less clear and more complex.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And of course, the microRPC is not necessarily the medium (in Docker as far as I understand the gRPC is the medium) which will break the abstraction.

/// Receives messages to process by the trusted application. Conceptually represents
/// a computation slice. A set of messages may be empty.
///
/// # Arguments
///
/// * `host` - Trusted host that is responsible the trusted application. Provides
/// access to message sending and attestation capabilities.
/// * `instant` - A measurement of a monotonically nondecreasing clock provided by
/// the untrusted launcher to the trusted host. The resolution of the instant is
/// mesured in milliseconds. Instants are opaque that can only be compared to one
/// another. In other words the absolute value must not be interpretted as wall
/// clock time or time since the trusted application start.
/// * `messages` - A potentially empty set of messages received from the untrusted
/// launcher for the trusted application to process.
///
/// # Returns
///
/// Error if the trusted application encountered an unrecoverable error and must
/// be terminated, a success otherwise. The application level recoverable errors
/// are represented and communicated using messages.
fn receive_messages(
&mut self,
host: &mut impl Host,
instant: u64,
messages: &[Message],
) -> Result<()>;
}
Loading