Project structure, utilities and traits for a DPE

This is the start of a DICE Protection Environment implementation in
the form of a Rust library.

Change-Id: I8a0e2085f36354a1b5f3e0b3c5f81e8cc5987caf
Reviewed-on: https://pigweed-review.googlesource.com/c/open-dice/+/194311
Reviewed-by: Andrew Scull <ascull@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Reviewed-by: Marshall Pierce <marshallpierce@google.com>
Commit-Queue: Darren Krahn <dkrahn@google.com>
diff --git a/dpe-rs/Cargo.toml b/dpe-rs/Cargo.toml
new file mode 100644
index 0000000..c491b03
--- /dev/null
+++ b/dpe-rs/Cargo.toml
@@ -0,0 +1,58 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+[package]
+name = "dpe-rs"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+aes-gcm = { version = "0.10.3", default-features = false, features = ["aes", "heapless", "zeroize"] }
+env_logger = "0.10.0"
+hash32 = "0.3.1"
+heapless = { version = "0.7.16", default-features = false }
+libc-print = "0.1.22"
+log = "0.4.20"
+minicbor = "0.19.1"
+noise-protocol = "0.2.0"
+rand_core = "0.6.4"
+zeroize = { version = "1.7.0", features = ["zeroize_derive"], default-features = false }
+
+[dev-dependencies]
+aes-gcm-siv = "0.11.1"
+ed25519-dalek = { version = "2.1.0", default-features = false, features = ["zeroize"] }
+hkdf = "0.12.3"
+hmac = "0.12.1"
+hpke = { version = "0.11.0", default-features = false, features = ["x25519"] }
+noise-rust-crypto = "0.6.2"
+sha2 = { version = "0.10.8", default-features = false }
+x25519-dalek = { version = "2.0.0", default-features = false, features = ["zeroize"] }
+rand_chacha = { version = "0.3.1", default-features = false }
+
+[workspace.lints.rust]
+unsafe_code = "deny"
+missing_docs = "deny"
+trivial_casts = "deny"
+trivial_numeric_casts = "deny"
+unused_extern_crates = "deny"
+unused_import_braces = "deny"
+unused_results = "deny"
+
+[workspace.lints.clippy]
+indexing_slicing = "deny"
+unwrap_used = "deny"
+panic = "deny"
+expect_used = "deny"
diff --git a/dpe-rs/src/args.rs b/dpe-rs/src/args.rs
new file mode 100644
index 0000000..f83d891
--- /dev/null
+++ b/dpe-rs/src/args.rs
@@ -0,0 +1,148 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Types related to command arguments.
+
+use crate::error::{DpeResult, ErrCode};
+use crate::memory::SizedMessage;
+use heapless::FnvIndexMap;
+use log::error;
+
+/// Represents the numeric identifier of a command or response argument.
+pub type ArgId = u32;
+
+/// Represents the type of a command or response argument.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
+pub enum ArgTypeSelector {
+    /// Indicates an argument was not recognized, so its type is unknown.
+    #[default]
+    Unknown,
+    /// Indicates an argument is encoded as a CBOR byte string.
+    Bytes,
+    /// Indicates an argument is encoded as a CBOR unsigned integer.
+    Int,
+    /// Indicates an argument is encoded as a CBOR true or false simple value.
+    Bool,
+    /// Indicates an argument needs additional custom decoding.
+    Other,
+}
+
+/// Represents a command or response argument value.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum ArgValue<'a> {
+    /// This instantiation borrows a slice of a message buffer that was decoded
+    /// as a CBOR byte string. The slice needs to live at least as long as
+    /// this.
+    BytesArg(&'a [u8]),
+    /// This instantiation contains a decoded CBOR unsigned integer.
+    IntArg(u64),
+    /// This instantiation contains a decoded CBOR boolean value.
+    BoolArg(bool),
+}
+
+impl<'a> ArgValue<'a> {
+    /// Creates a new `BytesArg` from a slice, borrowing the slice.
+    pub fn from_slice(value: &'a [u8]) -> Self {
+        ArgValue::BytesArg(value)
+    }
+
+    /// Returns the borrowed slice if this is a BytesArg.
+    ///
+    /// # Errors
+    ///
+    /// Returns an InternalError error if this is not a BytesArg.
+    pub fn try_into_slice(&self) -> DpeResult<&'a [u8]> {
+        match self {
+            ArgValue::IntArg(_) | ArgValue::BoolArg(_) => {
+                error!("ArgValue::try_info_slice called on {:?}", self);
+                Err(ErrCode::InternalError)
+            }
+            ArgValue::BytesArg(value) => Ok(value),
+        }
+    }
+
+    /// Returns the value held by an IntArg as a u32.
+    ///
+    /// # Errors
+    ///
+    /// Returns an InternalError error if this is not an IntArg.
+    pub fn try_into_u32(&self) -> DpeResult<u32> {
+        match self {
+            ArgValue::IntArg(i) => Ok((*i).try_into()?),
+            _ => {
+                error!("ArgValue::try_into_u32 called on {:?}", self);
+                Err(ErrCode::InternalError)
+            }
+        }
+    }
+
+    /// Creates a new `IntArg` holding the given u32 `value`.
+    pub fn from_u32(value: u32) -> Self {
+        ArgValue::IntArg(value as u64)
+    }
+
+    /// Returns the value held by an IntArg as a u64.
+    ///
+    /// # Errors
+    ///
+    /// Returns an InternalError error if this is not an IntArg.
+    pub fn try_into_u64(&self) -> DpeResult<u64> {
+        match self {
+            ArgValue::IntArg(i) => Ok(*i),
+            _ => {
+                error!("ArgValue::try_into_u64 called on {:?}", self);
+                Err(ErrCode::InternalError)
+            }
+        }
+    }
+
+    /// Creates a new `IntArg` holding the given u64 `value`.
+    pub fn from_u64(value: u64) -> Self {
+        ArgValue::IntArg(value)
+    }
+
+    /// Returns the value held by a BoolArg.
+    ///
+    /// # Errors
+    ///
+    /// Returns an InternalError error if this is not a BoolArg.
+    pub fn try_into_bool(&self) -> DpeResult<bool> {
+        match self {
+            ArgValue::BoolArg(b) => Ok(*b),
+            _ => {
+                error!("ArgValue::try_into_bool called on {:?}", self);
+                Err(ErrCode::InternalError)
+            }
+        }
+    }
+
+    /// Creates a new `BoolArg` holding the given `value`.
+    pub fn from_bool(value: bool) -> Self {
+        ArgValue::BoolArg(value)
+    }
+}
+
+impl<'a, const S: usize> From<&'a SizedMessage<S>> for ArgValue<'a> {
+    fn from(message: &'a SizedMessage<S>) -> Self {
+        Self::BytesArg(message.as_slice())
+    }
+}
+
+/// Contains a set of command or response arguments in the form of a map from
+/// [`ArgId`] to [`ArgValue`].
+pub type ArgMap<'a> = FnvIndexMap<ArgId, ArgValue<'a>, 16>;
+
+/// Contains a set of argument types in the form of a map from ArgId to
+/// [`ArgTypeSelector`].
+pub type ArgTypeMap = FnvIndexMap<ArgId, ArgTypeSelector, 16>;
diff --git a/dpe-rs/src/cbor.rs b/dpe-rs/src/cbor.rs
new file mode 100644
index 0000000..373f4a1
--- /dev/null
+++ b/dpe-rs/src/cbor.rs
@@ -0,0 +1,154 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Utilities related to CBOR encode/decode.
+
+use crate::error::{DpeResult, ErrCode};
+use crate::memory::SizedMessage;
+use log::error;
+use minicbor::{Decoder, Encoder};
+
+// Required in order for minicbor to write into a SizedMessage.
+impl<const S: usize> minicbor::encode::Write for SizedMessage<S> {
+    type Error = ();
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        self.vec.extend_from_slice(buf)
+    }
+}
+
+/// Creates a CBOR [Encoder] which encodes into `output`.
+pub fn cbor_encoder_from_message<const S: usize>(
+    output: &mut SizedMessage<S>,
+) -> Encoder<&mut SizedMessage<S>> {
+    Encoder::new(output)
+}
+
+/// Creates a CBOR [Decoder] which decodes from `input`.
+pub fn cbor_decoder_from_message<const S: usize>(
+    input: &SizedMessage<S>,
+) -> Decoder {
+    Decoder::new(input.as_slice())
+}
+
+/// Extends minicbor::Decoder.
+pub trait DecoderExt {
+    /// Decodes a byte slice and returns only its position. This is useful when
+    /// the byte slice is the last CBOR item, might be large, and will be
+    /// processed in-place using up to the entire available message buffer.
+    /// This is intended to be used in conjunction with [`remove_prefix`].
+    ///
+    /// # Errors
+    ///
+    /// Returns an InvalidArgument error if a CBOR Bytes item cannot be decoded.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use dpe_rs::cbor::{
+    ///     cbor_decoder_from_message,
+    ///     cbor_encoder_from_message,
+    ///     DecoderExt,
+    /// };
+    /// use dpe_rs::memory::Message;
+    ///
+    /// let mut message = Message::new();
+    /// cbor_encoder_from_message(&mut message).bytes(&[0; 1000]);
+    /// let mut decoder = cbor_decoder_from_message(&message);
+    /// let position = decoder.decode_bytes_prefix().unwrap();
+    /// assert_eq!(&message.as_slice()[position..], &[0; 1000]);
+    /// ```
+    /// [`remove_prefix`]: SizedMessage::remove_prefix
+    fn decode_bytes_prefix(&mut self) -> DpeResult<usize>;
+}
+impl DecoderExt for Decoder<'_> {
+    fn decode_bytes_prefix(&mut self) -> DpeResult<usize> {
+        let bytes_len = self.bytes()?.len();
+        Ok(self.position() - bytes_len)
+    }
+}
+
+/// Encodes a CBOR Bytes prefix for a given `bytes_len` and appends it to
+/// `buffer`. This must be appended by `bytes_len` bytes to form a valid CBOR
+/// encoding.
+///
+/// # Errors
+///
+/// Returns an InternalError error if `bytes_len` is too large for the remaining
+/// capacity of the `buffer`.
+///
+/// # Example
+///
+/// ```rust
+/// use dpe_rs::cbor::{
+///     cbor_decoder_from_message,
+///     cbor_encoder_from_message,
+///     encode_bytes_prefix,
+/// };
+/// use dpe_rs::memory::{
+///     Message,
+///     SizedMessage,
+/// };
+/// type Prefix = SizedMessage<10>;
+///
+/// let mut message = Message::from_slice(&[0; 100]).unwrap();
+/// let mut prefix = Prefix::new();
+/// encode_bytes_prefix(&mut prefix, message.len()).unwrap();
+/// message.insert_prefix(prefix.as_slice()).unwrap();
+/// let mut decoder = cbor_decoder_from_message(&message);
+/// assert_eq!(decoder.bytes().unwrap(), &[0; 100]);
+/// ```
+pub fn encode_bytes_prefix<const S: usize>(
+    buffer: &mut SizedMessage<S>,
+    bytes_len: usize,
+) -> DpeResult<()> {
+    // See RFC 8949 sections 3 and 3.1 for how this is encoded.
+    // `CBOR_BYTES_MAJOR_TYPE` is major type 2 in the high-order 3 bits.
+    const CBOR_BYTES_MAJOR_TYPE: u8 = 2 << 5;
+    const CBOR_VALUE_IN_ONE_BYTE: u8 = 24;
+    const CBOR_VALUE_IN_TWO_BYTES: u8 = 25;
+    let initial_byte_value;
+    let mut following_bytes: &[u8] = &[];
+    let mut big_endian_value: [u8; 2] = Default::default();
+    match bytes_len {
+        0..=23 => {
+            // Encode the length in the lower 5 bits of the initial byte.
+            initial_byte_value = bytes_len as u8;
+        }
+        24..=255 => {
+            // Encode the length in a single additional byte.
+            initial_byte_value = CBOR_VALUE_IN_ONE_BYTE;
+            big_endian_value[0] = bytes_len as u8;
+            following_bytes = &big_endian_value[..1];
+        }
+        256..=65535 => {
+            // Encode the length in two additional bytes, big endian.
+            initial_byte_value = CBOR_VALUE_IN_TWO_BYTES;
+            big_endian_value = (bytes_len as u16).to_be_bytes();
+            following_bytes = &big_endian_value;
+        }
+        _ => {
+            error!("Unsupported CBOR length");
+            return Err(ErrCode::InternalError);
+        }
+    }
+    buffer
+        .vec
+        .push(CBOR_BYTES_MAJOR_TYPE + initial_byte_value)
+        .map_err(|_| ErrCode::InternalError)?;
+    buffer
+        .vec
+        .extend_from_slice(following_bytes)
+        .map_err(|_| ErrCode::InternalError)?;
+    Ok(())
+}
diff --git a/dpe-rs/src/constants.rs b/dpe-rs/src/constants.rs
new file mode 100644
index 0000000..2310e67
--- /dev/null
+++ b/dpe-rs/src/constants.rs
@@ -0,0 +1,52 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Global constants
+
+/// The maximum size in bytes of a message buffer.
+pub const MAX_MESSAGE_SIZE: usize = 8192;
+
+/// The size in bytes of a cryptographic hash.
+pub const HASH_SIZE: usize = 64;
+
+/// The size in bytes of a private session key agreement key.
+pub const DH_PRIVATE_KEY_SIZE: usize = 32;
+
+/// The size in bytes of a public session key agreement key.
+pub const DH_PUBLIC_KEY_SIZE: usize = 32;
+
+/// The size in bytes of an encryption key, currently this is the same for
+/// session and sealing encryption.
+pub const ENCRYPTION_KEY_SIZE: usize = 32;
+
+/// The size in bytes of a serialized public key for signing.
+pub const SIGNING_PUBLIC_KEY_SIZE: usize = 32;
+
+/// The size in bytes of a serialized private key for signing.
+pub const SIGNING_PRIVATE_KEY_SIZE: usize = 32;
+
+/// The size in bytes of a serialized public key for sealing.
+pub const SEALING_PUBLIC_KEY_SIZE: usize = 32;
+
+/// The size in bytes of a serialized private key for sealing.
+pub const SEALING_PRIVATE_KEY_SIZE: usize = 32;
+
+/// The maximum size in bytes of a signature produced by the Sign command.
+pub const MAX_SIGNATURE_SIZE: usize = 64;
+
+/// The maximum size in bytes of a session handshake message.
+pub const MAX_HANDSHAKE_MESSAGE_SIZE: usize = 64;
+
+/// The maximum size in bytes of a session handshake payload.
+pub const MAX_HANDSHAKE_PAYLOAD_SIZE: usize = 8;
diff --git a/dpe-rs/src/crypto.rs b/dpe-rs/src/crypto.rs
new file mode 100644
index 0000000..ef16210
--- /dev/null
+++ b/dpe-rs/src/crypto.rs
@@ -0,0 +1,321 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Defines the Crypto trait and related types.
+
+use crate::byte_array_wrapper;
+use crate::constants::*;
+use crate::error::DpeResult;
+use crate::memory::{Message, SizedMessage};
+use zeroize::ZeroizeOnDrop;
+
+byte_array_wrapper!(MacKey, HASH_SIZE, "MAC key");
+byte_array_wrapper!(EncryptionKey, ENCRYPTION_KEY_SIZE, "encryption key");
+byte_array_wrapper!(DhPublicKey, DH_PUBLIC_KEY_SIZE, "DH public key");
+byte_array_wrapper!(DhPrivateKey, DH_PRIVATE_KEY_SIZE, "DH private key");
+byte_array_wrapper!(Hash, HASH_SIZE, "hash");
+byte_array_wrapper!(
+    SigningPublicKey,
+    SIGNING_PUBLIC_KEY_SIZE,
+    "signing public key"
+);
+byte_array_wrapper!(
+    SigningPrivateKey,
+    SIGNING_PRIVATE_KEY_SIZE,
+    "signing private key"
+);
+byte_array_wrapper!(
+    SealingPublicKey,
+    SEALING_PUBLIC_KEY_SIZE,
+    "sealing public key"
+);
+byte_array_wrapper!(
+    SealingPrivateKey,
+    SEALING_PRIVATE_KEY_SIZE,
+    "sealing private key"
+);
+
+/// A session handshake message.
+pub type HandshakeMessage = SizedMessage<MAX_HANDSHAKE_MESSAGE_SIZE>;
+/// A session handshake payload.
+pub type HandshakePayload = SizedMessage<MAX_HANDSHAKE_PAYLOAD_SIZE>;
+/// A signature.
+pub type Signature = SizedMessage<MAX_SIGNATURE_SIZE>;
+
+/// A trait for committing previously staged changes.
+pub trait Commit {
+    /// Commits a previously staged changes. When used with session cipher
+    /// state, the staged changes are typically counter increments that result
+    /// from encrypt or decrypt operations.
+    fn commit(&mut self);
+}
+
+/// A trait for maintaining a counter.
+pub trait Counter {
+    /// Returns the current counter value.
+    fn n(&self) -> u64;
+    /// Sets the counter value to `n`.
+    fn set_n(&mut self, n: u64);
+}
+
+/// Provides cryptographic operations for encrypted sessions.
+pub trait SessionCrypto {
+    /// A type to represent session cipher states. These are owned by and opaque
+    /// to the caller in `new_session_handshake` and `derive_session_handshake`.
+    type SessionCipherState: Commit + Counter;
+
+    /// Performs a session responder handshake for a new session.
+    ///
+    /// # Parameters
+    ///
+    /// * `static_dh_key`: The DPE session identity, which the client is
+    /// expected to already know.
+    /// * `initiator_handshake`: The handshake message received from the client.
+    /// * `payload`: The payload to include in the `responder_handshake`.
+    /// * `responder_handshake`: Receives the handshake message to be sent back
+    /// to the client.
+    /// * `decrypt_cipher_state`: Receives cipher state for decrypting incoming
+    /// session messages. This is intended to be passed to
+    /// [`SessionCrypto::session_decrypt`].
+    /// * `encrypt_cipher_state`: Receives cipher state for encrypting outgoing
+    /// session messages. This is intended to be passed to
+    /// [`SessionCrypto::session_encrypt`].
+    /// * `psk_seed`: Receives a PSK seed that can be used to construct a PSK to
+    /// be used when deriving a session (see
+    /// [`SessionCrypto::derive_session_handshake`]).
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    #[allow(clippy::too_many_arguments)]
+    fn new_session_handshake(
+        static_dh_key: &DhPrivateKey,
+        initiator_handshake: &HandshakeMessage,
+        payload: &HandshakePayload,
+        responder_handshake: &mut HandshakeMessage,
+        decrypt_cipher_state: &mut Self::SessionCipherState,
+        encrypt_cipher_state: &mut Self::SessionCipherState,
+        psk_seed: &mut Hash,
+    ) -> DpeResult<()>;
+
+    /// Performs a session responder handshake for a derived session. In
+    /// contrast to a new session handshake, a derived session does not use a
+    /// static key, but a pre-shared key (PSK) derived from an existing session.
+    ///
+    /// # Parameters
+    ///
+    /// * `psk`: A PSK derived from an existing session.
+    /// * `initiator_handshake`: The handshake message received from the client.
+    /// * `payload`: The payload to include in the `responder_handshake`.
+    /// * `responder_handshake`: Receives the handshake message to be sent back
+    /// to the client.
+    /// * `decrypt_cipher_state`: Receives cipher state for decrypting incoming
+    /// session messages. This is intended to be passed to
+    /// [`SessionCrypto::session_decrypt`].
+    /// * `encrypt_cipher_state`: Receives cipher state for encrypting outgoing
+    /// session messages. This is intended to be passed to
+    /// [`SessionCrypto::session_encrypt`].
+    /// * `psk_seed`: Receives a PSK seed that can be used to construct a PSK to
+    /// be used when deriving another session.
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    #[allow(clippy::too_many_arguments)]
+    fn derive_session_handshake(
+        psk: &Hash,
+        initiator_handshake: &HandshakeMessage,
+        payload: &HandshakePayload,
+        responder_handshake: &mut HandshakeMessage,
+        decrypt_cipher_state: &mut Self::SessionCipherState,
+        encrypt_cipher_state: &mut Self::SessionCipherState,
+        psk_seed: &mut Hash,
+    ) -> DpeResult<()>;
+
+    /// Derives a PSK from session state: `psk_seed`, `decrypt_cipher_state`,
+    /// and `encrypt_cipher_state`. The returned PSK is appropriate as an
+    /// argument to [`derive_session_handshake`].
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    ///
+    /// [`derive_session_handshake`]: #method.derive_session_handshake
+    fn derive_psk_from_session(
+        psk_seed: &Hash,
+        decrypt_cipher_state: &Self::SessionCipherState,
+        encrypt_cipher_state: &Self::SessionCipherState,
+    ) -> DpeResult<Hash>;
+
+    /// Encrypts an outgoing session message with the given `cipher_state`. The
+    /// `in_place_buffer` both provides the plaintext message and receives the
+    /// corresponding ciphertext.
+    ///
+    /// # Errors
+    ///
+    /// This method fails with an OutOfMemory error if the encryption overhead
+    /// does not fit in the buffer.
+    fn session_encrypt(
+        cipher_state: &mut Self::SessionCipherState,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+
+    /// Decrypts an incoming session message with the given `cipher_state`. The
+    /// `in_place_buffer` both provides the ciphertext message and receives the
+    /// corresponding plaintext.
+    ///
+    /// # Errors
+    ///
+    /// This method fails with an InvalidArgument error if the ciphertext cannot
+    /// be decrypted (e.g. if tag authentication fails).
+    fn session_decrypt(
+        cipher_state: &mut Self::SessionCipherState,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+}
+
+/// Provides cryptographic operations. These operations are specifically for DPE
+/// concepts, defined by a DPE profile, and to be invoked by a DPE instance.
+pub trait Crypto {
+    /// An associated [`SessionCrypto`] type.
+    type S: SessionCrypto;
+
+    /// Returns a hash of `input`.
+    ///
+    /// # Errors
+    ///
+    /// This method is infallible.
+    fn hash(input: &[u8]) -> Hash;
+
+    /// Returns a hash over all items in `iter`, in order.
+    ///
+    /// # Errors
+    ///
+    /// This method is infallible.
+    fn hash_iter<'a>(iter: impl Iterator<Item = &'a [u8]>) -> Hash;
+
+    /// Runs a key derivation function (KDF) to derive a key the length of the
+    /// `derived_key` buffer. The inputs are interpreted as documented by the
+    /// [HKDF](<https://datatracker.ietf.org/doc/html/rfc5869>) scheme. The
+    /// implementation doesn't need to be HKDF specifically but needs to work
+    /// with HKDF-style inputs.
+    ///
+    /// # Parameters
+    ///
+    /// * `kdf_ikm`: input keying material
+    /// * `kdf_info`: HKDF-style info (optional)
+    /// * `kdf_salt`: HKDF-style salt (optional)
+    /// * `derived_key`: Receives the derived key
+    ///
+    /// # Errors
+    ///
+    /// Fails with an `InternalError` if `derived_key` is too large.
+    fn kdf(
+        kdf_ikm: &[u8],
+        kdf_info: &[u8],
+        kdf_salt: &[u8],
+        derived_key: &mut [u8],
+    ) -> DpeResult<()>;
+
+    /// Derives an asymmetric key pair for signing from a given `seed`.
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    fn signing_keypair_from_seed(
+        seed: &Hash,
+    ) -> DpeResult<(SigningPublicKey, SigningPrivateKey)>;
+
+    /// Derives an asymmetric key pair for sealing from a given `seed`.
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    fn sealing_keypair_from_seed(
+        seed: &Hash,
+    ) -> DpeResult<(SealingPublicKey, SealingPrivateKey)>;
+
+    /// Computes a MAC over `data` using the given `key`.
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    fn mac(key: &MacKey, data: &[u8]) -> DpeResult<Hash>;
+
+    /// Generates a signature over `tbs` using the given `key`.
+    ///
+    /// # Errors
+    ///
+    /// This method allows implementers to return an error but it is expected to
+    /// be infallible.
+    fn sign(key: &SigningPrivateKey, tbs: &[u8]) -> DpeResult<Signature>;
+
+    /// Encrypts data using the given `key` in a way that it can be decrypted by
+    /// the `unseal` method with the same `key`. The `in_place_buffer` both
+    /// provides the plaintext input and receives the ciphertext output.
+    ///
+    /// # Errors
+    ///
+    /// Fails with OutOfMemory if the ciphertext, including overhead, does not
+    /// fit in the buffer.
+    fn seal(
+        key: &EncryptionKey,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+
+    /// Decrypts and authenticates data previously generated by the `seal`
+    /// method using the given 'key'. The `in_place_buffer` both provides the
+    /// ciphertext input and receives the plaintext output.
+    ///
+    /// # Errors
+    ///
+    /// Fails with InvalidArgument if authenticated decryption fails.
+    fn unseal(
+        key: &EncryptionKey,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+
+    /// Encrypts data using an asymmetric scheme and the given `public_key` in
+    /// a way that it can be decrypted by the `unseal_asymmetric` method given
+    /// the corresponding private key. While this method is useful for testing,
+    /// a DPE does not use this during normal operation. The `in_place_buffer`
+    /// both provides the plaintext input and receives the ciphertext output.
+    ///
+    /// # Errors
+    ///
+    /// Fails with OutOfMemory if the ciphertext, including overhead, does not
+    /// fit in the buffer.
+    fn seal_asymmetric(
+        public_key: &SealingPublicKey,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+
+    /// Decrypts data using an asymmetric scheme and the give `key`. The
+    /// `in_place_buffer` both provides the ciphertext input and receives the
+    /// plaintext output.
+    ///
+    /// # Errors
+    ///
+    /// Fails with InvalidArgument if the ciphertext cannot be decrypted.
+    fn unseal_asymmetric(
+        key: &SealingPrivateKey,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()>;
+}
diff --git a/dpe-rs/src/error.rs b/dpe-rs/src/error.rs
new file mode 100644
index 0000000..50293d7
--- /dev/null
+++ b/dpe-rs/src/error.rs
@@ -0,0 +1,83 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Defines the [ErrCode] and [DpeResult] types.
+
+use log::error;
+
+/// An enum of error codes as defined in the DPE specification. The
+/// discriminant values match the CBOR encoding values per the specification.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+pub enum ErrCode {
+    /// An unexpected error has occurred which is not actionable by the client.
+    InternalError = 1,
+    /// The command could not be decrypted, parsed, or is not supported.
+    InvalidCommand = 2,
+    /// A command argument is malformed, invalid with respect to the current
+    /// DPE state, in conflict with other arguments, not allowed, not
+    /// recognized, or otherwise not supported.
+    InvalidArgument = 3,
+    /// Keys for an encrypted session have been exhausted.
+    SessionExhausted = 4,
+    /// The command cannot be fulfilled because an internal seed component is
+    /// no longer available.
+    InitializationSeedLocked = 5,
+    /// A lack of internal resources prevented the DPE from fulfilling the
+    /// command.
+    OutOfMemory = 6,
+    /// The command was canceled.
+    Canceled = 7,
+}
+
+impl<E> From<minicbor::encode::Error<E>> for ErrCode {
+    fn from(_error: minicbor::encode::Error<E>) -> Self {
+        error!("Failed to encode CBOR message");
+        ErrCode::InternalError
+    }
+}
+
+impl From<minicbor::decode::Error> for ErrCode {
+    fn from(_error: minicbor::decode::Error) -> Self {
+        error!("Failed to decode CBOR message");
+        ErrCode::InvalidArgument
+    }
+}
+
+impl From<core::num::TryFromIntError> for ErrCode {
+    fn from(_: core::num::TryFromIntError) -> Self {
+        error!("Unexpected failure: core::num::TryFromIntError");
+        ErrCode::InternalError
+    }
+}
+
+impl From<u32> for ErrCode {
+    fn from(value: u32) -> Self {
+        match value {
+            1 => Self::InternalError,
+            2 => Self::InvalidCommand,
+            3 => Self::InvalidArgument,
+            4 => Self::SessionExhausted,
+            5 => Self::InitializationSeedLocked,
+            6 => Self::OutOfMemory,
+            7 => Self::Canceled,
+            _ => {
+                error!("Unknown error code");
+                Self::InternalError
+            }
+        }
+    }
+}
+
+/// A Result type using a DPE [`ErrCode`] error type.
+pub type DpeResult<T> = Result<T, ErrCode>;
diff --git a/dpe-rs/src/lib.rs b/dpe-rs/src/lib.rs
new file mode 100644
index 0000000..b995caf
--- /dev/null
+++ b/dpe-rs/src/lib.rs
@@ -0,0 +1,57 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#![no_std]
+#![deny(unsafe_code)]
+#![deny(missing_docs)]
+#![deny(trivial_casts)]
+#![deny(trivial_numeric_casts)]
+#![deny(unused_extern_crates)]
+#![deny(unused_import_braces)]
+#![deny(unused_results)]
+#![deny(clippy::indexing_slicing)]
+#![deny(clippy::unwrap_used)]
+#![deny(clippy::panic)]
+#![deny(clippy::expect_used)]
+
+//! # DICE Protection Environment
+//!
+//! `dpe_rs` implements a DICE Protection Environment (DPE) for a family of DPE
+//! profiles which align with the
+//! [Open Profile for DICE](<https://pigweed.googlesource.com/open-dice/+/HEAD/docs/specification.md>)
+//! specification.
+//!
+//! # no_std
+//!
+//! This crate uses `#![no_std]` for portability to embedded environments.
+//!
+//! # Panics
+//!
+//! Functions and methods in this crate, aside from tests, do not panic. A panic
+//! means there is a bug that should be fixed.
+//!
+//! # Safety
+//!
+//! This crate does not use unsafe code.
+//!
+//! # Notes
+//!
+//! This crate is in development and not ready for production use.
+pub mod args;
+pub mod cbor;
+pub mod constants;
+pub mod crypto;
+pub mod error;
+pub mod memory;
+pub mod noise;
diff --git a/dpe-rs/src/memory.rs b/dpe-rs/src/memory.rs
new file mode 100644
index 0000000..905b136
--- /dev/null
+++ b/dpe-rs/src/memory.rs
@@ -0,0 +1,291 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! Types and functions to help with memory buffer management.
+
+use crate::constants::*;
+use crate::error::{DpeResult, ErrCode};
+use heapless::Vec;
+use zeroize::ZeroizeOnDrop;
+
+/// Creates a byte array wrapper type for the sake of precise typing.
+#[macro_export]
+macro_rules! byte_array_wrapper {
+    ($type_name:ident, $len:ident, $desc:expr) => {
+        #[doc = "A byte array wrapper to represent a "]
+        #[doc = $desc]
+        #[doc = "."]
+        #[derive(Clone, Debug, Eq, PartialEq, Hash, ZeroizeOnDrop)]
+        pub struct $type_name([u8; $len]);
+        impl $type_name {
+            #[doc = "Returns the length of the array."]
+            pub fn len(&self) -> usize {
+                self.0.len()
+            }
+
+            #[doc = "Whether the array is empty."]
+            pub fn is_empty(&self) -> bool {
+                self.0.is_empty()
+            }
+
+            #[doc = "Borrows the array as a slice."]
+            pub fn as_slice(&self) -> &[u8] {
+                self.0.as_slice()
+            }
+
+            #[doc = "Mutably borrows the array as a slice."]
+            pub fn as_mut_slice(&mut self) -> &mut [u8] {
+                &mut self.0
+            }
+
+            #[doc = "Borrows the array."]
+            pub fn as_array(&self) -> &[u8; $len] {
+                &self.0
+            }
+
+            #[doc = "Creates a "]
+            #[doc = stringify!($type_name)]
+            #[doc = " from a slice. Fails if the slice length is not "]
+            #[doc = stringify!($len)]
+            #[doc = "."]
+            pub fn from_slice(s: &[u8]) -> DpeResult<Self> {
+                Self::try_from(s)
+            }
+
+            #[doc = "Creates a "]
+            #[doc = stringify!($type_name)]
+            #[doc = " from a slice infallibly. If the length of the slice is less than "]
+            #[doc = stringify!($len)]
+            #[doc = ", the remainder of the array is the default value. If the length "]
+            #[doc = "of the slice is more than "]
+            #[doc = stringify!($len)]
+            #[doc = ", only the first "]
+            #[doc = stringify!($len)]
+            #[doc = " bytes are used. This method is infallible."]
+            pub fn from_slice_infallible(value: &[u8]) -> Self {
+                #![allow(clippy::indexing_slicing)]
+                let mut tmp: Self = Default::default();
+                if value.len() < $len {
+                    tmp.0[..value.len()].copy_from_slice(value);
+                } else {
+                    tmp.0.copy_from_slice(&value[..$len]);
+                }
+                tmp
+            }
+
+            #[doc = "Creates a "]
+            #[doc = stringify!($type_name)]
+            #[doc = " from an array."]
+            pub fn from_array(value: &[u8; $len]) -> Self {
+                Self(*value)
+            }
+        }
+
+        impl Default for $type_name {
+            fn default() -> Self {
+                Self([0; $len])
+            }
+        }
+
+        impl TryFrom<&[u8]> for $type_name {
+            type Error = $crate::error::ErrCode;
+
+            fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+                value.try_into().map(Self).map_err(|_| {
+                    log::error!("Invalid length for fixed length value: {}", $desc);
+                    $crate::error::ErrCode::InvalidArgument
+                })
+            }
+        }
+
+        impl From<[u8; $len]> for $type_name {
+            fn from(value: [u8; $len]) -> Self {
+                Self(value)
+            }
+        }
+    };
+}
+
+/// Wraps a [heapless::Vec] of bytes and provides various convenience methods
+/// that are useful when processing DPE messages. The inner `vec` is also
+/// accessible directly.
+#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, ZeroizeOnDrop)]
+pub struct SizedMessage<const S: usize> {
+    /// The wrapped Vec.
+    pub vec: Vec<u8, S>,
+}
+
+impl<const S: usize> SizedMessage<S> {
+    /// Creates a new, empty instance.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// assert_eq!(MyMessage::new().len(), 0);
+    /// ```
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Creates a new instance from a slice.
+    ///
+    /// # Errors
+    ///
+    /// If `value` exceeds the available capacity, returns an OutOfMemory error.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// assert_eq!(MyMessage::from_slice(&[0; 12]).unwrap().as_slice(), &[0; 12]);
+    /// ```
+    pub fn from_slice(value: &[u8]) -> DpeResult<Self> {
+        Ok(Self {
+            vec: Vec::from_slice(value).map_err(|_| ErrCode::OutOfMemory)?,
+        })
+    }
+
+    /// Clones `slice`, replacing any existing content.
+    ///
+    /// # Errors
+    ///
+    /// If `slice` exceeds the available capacity, returns an OutOfMemory error.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// let mut m = MyMessage::from_slice(&[0; 12]).unwrap();
+    /// m.clone_from_slice(&[1; 3]).unwrap();
+    /// assert_eq!(m.as_slice(), &[1; 3]);
+    /// ```
+    pub fn clone_from_slice(&mut self, slice: &[u8]) -> DpeResult<()> {
+        self.clear();
+        self.vec.extend_from_slice(slice).map_err(|_| ErrCode::OutOfMemory)
+    }
+
+    /// Borrows the inner byte array.
+    pub fn as_slice(&self) -> &[u8] {
+        self.vec.as_slice()
+    }
+
+    /// Mutably borrows the inner byte array after resizing. This is useful when
+    /// using the type as an output buffer.
+    ///
+    /// # Errors
+    ///
+    /// If `size` exceeds the available capacity, returns an OutOfMemory error.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use rand_core::{RngCore, SeedableRng};
+    ///
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// let mut buffer = MyMessage::new();
+    /// <rand_chacha::ChaCha12Rng as SeedableRng>::seed_from_u64(0)
+    ///     .fill_bytes(buffer.as_mut_sized(100).unwrap());
+    /// assert_eq!(buffer.len(), 100);
+    /// ```
+    pub fn as_mut_sized(&mut self, size: usize) -> DpeResult<&mut [u8]> {
+        self.vec.resize_default(size).map_err(|_| ErrCode::OutOfMemory)?;
+        Ok(self.vec.as_mut())
+    }
+
+    /// Returns the length of the inner vec.
+    pub fn len(&self) -> usize {
+        self.vec.len()
+    }
+
+    /// Whether the inner vec is empty.
+    pub fn is_empty(&self) -> bool {
+        self.vec.is_empty()
+    }
+
+    /// Clears the inner vec.
+    pub fn clear(&mut self) {
+        self.vec.clear()
+    }
+
+    /// Removes the first `prefix_size` bytes from the message. This carries the
+    /// cost of moving the remaining bytes to the front of the buffer.
+    ///
+    /// # Errors
+    ///
+    /// If `prefix_size` is larger than the current length, returns an
+    /// InternalError error.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// let mut m = MyMessage::from_slice("prefixdata".as_bytes()).unwrap();
+    /// m.remove_prefix(6).unwrap();
+    /// assert_eq!(m.as_slice(), "data".as_bytes());
+    /// ```
+    pub fn remove_prefix(&mut self, prefix_size: usize) -> DpeResult<()> {
+        if prefix_size > self.len() {
+            return Err(ErrCode::InternalError);
+        }
+        if prefix_size == self.len() {
+            self.clear();
+        } else if prefix_size > 0 {
+            let slice: &mut [u8] = self.vec.as_mut();
+            slice.copy_within(prefix_size.., 0);
+            self.vec.truncate(self.len() - prefix_size);
+        }
+        Ok(())
+    }
+
+    /// Inserts `prefix` at the start of the message. This carries the cost of
+    /// moving the existing bytes to make room for the prefix.
+    ///
+    /// # Errors
+    ///
+    /// If inserting `prefix` overflows the available capacity, returns an
+    /// OutOfMemory error.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// type MyMessage = dpe_rs::memory::SizedMessage<200>;
+    ///
+    /// let mut m = MyMessage::from_slice("data".as_bytes()).unwrap();
+    /// m.insert_prefix("prefix".as_bytes()).unwrap();
+    /// assert_eq!(m.as_slice(), "prefixdata".as_bytes());
+    /// ```
+    pub fn insert_prefix(&mut self, prefix: &[u8]) -> DpeResult<()> {
+        let old_len = self.len();
+        self.vec
+            .resize_default(self.len() + prefix.len())
+            .map_err(|_| ErrCode::OutOfMemory)?;
+        let slice: &mut [u8] = self.vec.as_mut();
+        slice.copy_within(0..old_len, prefix.len());
+        slice
+            .get_mut(..prefix.len())
+            .ok_or(ErrCode::InternalError)?
+            .copy_from_slice(prefix);
+        Ok(())
+    }
+}
+
+/// Represents a DPE command/response message. This type is large and should not
+/// be instantiated unnecessarily.
+pub type Message = SizedMessage<MAX_MESSAGE_SIZE>;
diff --git a/dpe-rs/src/noise.rs b/dpe-rs/src/noise.rs
new file mode 100644
index 0000000..49fa517
--- /dev/null
+++ b/dpe-rs/src/noise.rs
@@ -0,0 +1,698 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! An encrypted session implementation which uses
+//! Noise_NK_X25519_AESGCM_SHA512 and Noise_NNpsk0_X25519_AESGCM_SHA512.
+
+use crate::crypto::{
+    Commit, Counter, DhPrivateKey, DhPublicKey, HandshakeMessage,
+    HandshakePayload, Hash, SessionCrypto,
+};
+use crate::error::{DpeResult, ErrCode};
+use crate::memory::Message;
+use core::marker::PhantomData;
+use log::{debug, error};
+use noise_protocol::{HandshakeStateBuilder, Hash as NoiseHash, U8Array};
+
+impl From<noise_protocol::Error> for ErrCode {
+    fn from(_err: noise_protocol::Error) -> Self {
+        ErrCode::InvalidArgument
+    }
+}
+
+impl<NoiseHash> From<&NoiseHash> for Hash
+where
+    NoiseHash: U8Array,
+{
+    fn from(value: &NoiseHash) -> Self {
+        // The Noise hash size may not match HASH_SIZE.
+        Hash::from_slice_infallible(value.as_slice())
+    }
+}
+
+/// A cipher state type that can be used as a
+/// [`SessionCipherState`](crate::crypto::SessionCrypto::SessionCipherState).
+pub struct NoiseCipherState<C: noise_protocol::Cipher> {
+    k: C::Key,
+    n: u64,
+    n_staged: u64,
+}
+
+impl<C: noise_protocol::Cipher> Clone for NoiseCipherState<C> {
+    fn clone(&self) -> Self {
+        Self { k: self.k.clone(), n: self.n, n_staged: self.n_staged }
+    }
+}
+
+impl<C: noise_protocol::Cipher> Default for NoiseCipherState<C> {
+    fn default() -> Self {
+        Self { k: C::Key::new(), n: 0, n_staged: 0 }
+    }
+}
+
+impl<C: noise_protocol::Cipher> core::fmt::Debug for NoiseCipherState<C> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(f, "k: redacted, n: {}", self.n)?;
+        Ok(())
+    }
+}
+
+impl<C: noise_protocol::Cipher> core::hash::Hash for NoiseCipherState<C> {
+    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
+        self.k.as_slice().hash(state);
+        self.n.hash(state);
+        self.n_staged.hash(state);
+    }
+}
+
+#[cfg(test)]
+impl<C: noise_protocol::Cipher> PartialEq for NoiseCipherState<C> {
+    fn eq(&self, other: &Self) -> bool {
+        self.k.as_slice() == other.k.as_slice()
+            && self.n == other.n
+            && self.n_staged == other.n_staged
+    }
+}
+
+#[cfg(test)]
+impl<C: noise_protocol::Cipher> Eq for NoiseCipherState<C> {}
+
+impl<C: noise_protocol::Cipher> Counter for NoiseCipherState<C> {
+    fn n(&self) -> u64 {
+        self.n
+    }
+    fn set_n(&mut self, n: u64) {
+        self.n = n;
+    }
+}
+
+impl<C: noise_protocol::Cipher> Commit for NoiseCipherState<C> {
+    // Called when an encrypted message is finalized to commit the new cipher
+    // state.
+    fn commit(&mut self) {
+        self.n = self.n_staged;
+    }
+}
+
+impl<C: noise_protocol::Cipher> From<&noise_protocol::CipherState<C>>
+    for NoiseCipherState<C>
+{
+    fn from(cs: &noise_protocol::CipherState<C>) -> Self {
+        let (key, counter) = cs.clone().extract();
+        NoiseCipherState { k: key, n: counter, n_staged: counter }
+    }
+}
+
+/// Returns the public key corresponding to a given `dh_private_key`.
+pub fn get_dh_public_key<D: noise_protocol::DH>(
+    dh_private_key: &DhPrivateKey,
+) -> DpeResult<DhPublicKey> {
+    DhPublicKey::from_slice(
+        D::pubkey(&D::Key::from_slice(dh_private_key.as_slice())).as_slice(),
+    )
+}
+
+/// A trait representing [`NoiseSessionCrypto`] dependencies.
+pub trait NoiseCryptoDeps {
+    /// Cipher type
+    type Cipher: noise_protocol::Cipher;
+    /// DH type
+    type DH: noise_protocol::DH;
+    /// Hash type
+    type Hash: noise_protocol::Hash;
+}
+
+/// A Noise implementation of the [`SessionCrypto`] trait.
+pub struct NoiseSessionCrypto<D: NoiseCryptoDeps> {
+    #[allow(dead_code)]
+    phantom: PhantomData<D>,
+}
+
+impl<D> Clone for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn clone(&self) -> Self {
+        Self { phantom: Default::default() }
+    }
+}
+
+impl<D> Default for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn default() -> Self {
+        Self { phantom: Default::default() }
+    }
+}
+
+impl<D> core::fmt::Debug for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        Ok(())
+    }
+}
+
+impl<D> core::hash::Hash for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn hash<Hr: core::hash::Hasher>(&self, _: &mut Hr) {}
+}
+
+impl<D> PartialEq for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn eq(&self, _: &Self) -> bool {
+        true
+    }
+}
+
+impl<D> Eq for NoiseSessionCrypto<D> where D: NoiseCryptoDeps {}
+
+impl<D> SessionCrypto for NoiseSessionCrypto<D>
+where
+    D: NoiseCryptoDeps,
+{
+    type SessionCipherState = NoiseCipherState<D::Cipher>;
+
+    /// Implements the responder role of a Noise_NK handshake.
+    fn new_session_handshake(
+        static_dh_key: &DhPrivateKey,
+        initiator_handshake: &HandshakeMessage,
+        payload: &HandshakePayload,
+        responder_handshake: &mut HandshakeMessage,
+        decrypt_cipher_state: &mut NoiseCipherState<D::Cipher>,
+        encrypt_cipher_state: &mut NoiseCipherState<D::Cipher>,
+        psk_seed: &mut Hash,
+    ) -> DpeResult<()> {
+        #[allow(unused_results)]
+        let mut handshake: noise_protocol::HandshakeState<
+            D::DH,
+            D::Cipher,
+            D::Hash,
+        > = {
+            let mut builder = HandshakeStateBuilder::new();
+            builder.set_pattern(noise_protocol::patterns::noise_nk());
+            builder.set_is_initiator(false);
+            builder.set_prologue(&[]);
+            builder.set_s(<D::DH as noise_protocol::DH>::Key::from_slice(
+                static_dh_key.as_slice(),
+            ));
+            builder.build_handshake_state()
+        };
+        handshake.read_message(initiator_handshake.as_slice(), &mut [])?;
+        handshake.write_message(
+            payload.as_slice(),
+            responder_handshake.as_mut_sized(
+                handshake.get_next_message_overhead() + payload.len(),
+            )?,
+        )?;
+        assert!(handshake.completed());
+        let ciphers = handshake.get_ciphers();
+        *decrypt_cipher_state = (&ciphers.0).into();
+        *encrypt_cipher_state = (&ciphers.1).into();
+        debug!("get_hash");
+        *psk_seed = Hash::from_slice(handshake.get_hash())?;
+        Ok(())
+    }
+
+    /// Implements the responder role of a Noise_NNpsk0 handshake.
+    fn derive_session_handshake(
+        psk: &Hash,
+        initiator_handshake: &HandshakeMessage,
+        payload: &HandshakePayload,
+        responder_handshake: &mut HandshakeMessage,
+        decrypt_cipher_state: &mut NoiseCipherState<D::Cipher>,
+        encrypt_cipher_state: &mut NoiseCipherState<D::Cipher>,
+        psk_seed: &mut Hash,
+    ) -> DpeResult<()> {
+        #[allow(unused_results)]
+        let mut handshake: noise_protocol::HandshakeState<
+            D::DH,
+            D::Cipher,
+            D::Hash,
+        > = {
+            let mut builder = HandshakeStateBuilder::new();
+            builder.set_pattern(noise_protocol::patterns::noise_nn_psk0());
+            builder.set_is_initiator(false);
+            builder.set_prologue(&[]);
+            builder.build_handshake_state()
+        };
+        handshake
+            .push_psk(psk.as_slice().get(..32).ok_or(ErrCode::InternalError)?);
+        handshake.read_message(initiator_handshake.as_slice(), &mut [])?;
+        handshake.write_message(
+            payload.as_slice(),
+            responder_handshake.as_mut_sized(
+                handshake.get_next_message_overhead() + payload.len(),
+            )?,
+        )?;
+        let ciphers = handshake.get_ciphers();
+        *decrypt_cipher_state = (&ciphers.0).into();
+        *encrypt_cipher_state = (&ciphers.1).into();
+        *psk_seed = Hash::from_slice(handshake.get_hash())?;
+        Ok(())
+    }
+
+    /// Encrypts a Noise transport message in place.
+    fn session_encrypt(
+        cipher_state: &mut NoiseCipherState<D::Cipher>,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()> {
+        let mut cs = noise_protocol::CipherState::<D::Cipher>::new(
+            cipher_state.k.as_slice(),
+            cipher_state.n,
+        );
+        let plaintext_len = in_place_buffer.len();
+        let _ = cs.encrypt_in_place(
+            in_place_buffer.as_mut_sized(
+                plaintext_len
+                    + <D::Cipher as noise_protocol::Cipher>::tag_len(),
+            )?,
+            plaintext_len,
+        );
+        // Encrypting a message is usually not the final step in preparing
+        // the message for transport. If a subsequent step fails, it is
+        // better for 'n' to remain unchanged so we don't get out of sync.
+        (_, cipher_state.n_staged) = cs.extract();
+        Ok(())
+    }
+
+    /// Decrypts a Noise transport message in place.
+    fn session_decrypt(
+        cipher_state: &mut NoiseCipherState<D::Cipher>,
+        in_place_buffer: &mut Message,
+    ) -> DpeResult<()> {
+        let mut cs = noise_protocol::CipherState::<D::Cipher>::new(
+            cipher_state.k.as_slice(),
+            cipher_state.n,
+        );
+        let ciphertext_len = in_place_buffer.len();
+        let plaintext_len = match cs
+            .decrypt_in_place(in_place_buffer.vec.as_mut(), ciphertext_len)
+        {
+            Ok(length) => length,
+            _ => {
+                error!("Session decrypt failed");
+                return Err(ErrCode::InvalidCommand);
+            }
+        };
+        in_place_buffer.vec.truncate(plaintext_len);
+        (_, cipher_state.n) = cs.extract();
+        Ok(())
+    }
+
+    /// Derives a responder-side PSK.
+    fn derive_psk_from_session(
+        psk_seed: &Hash,
+        decrypt_cipher_state: &NoiseCipherState<D::Cipher>,
+        encrypt_cipher_state: &NoiseCipherState<D::Cipher>,
+    ) -> DpeResult<Hash> {
+        let mut hasher: D::Hash = Default::default();
+        hasher.input(psk_seed.as_slice());
+        // Use the decrypt state as it was before we decrypted the current
+        // command message. This allows clients to compute the PSK using
+        // the cipher states as they are before the client sends the
+        // command.
+        hasher.input(&(decrypt_cipher_state.n() - 1).to_le_bytes());
+        hasher.input(&encrypt_cipher_state.n().to_le_bytes());
+        Ok((&hasher.result()).into())
+    }
+}
+
+/// A SessionClient implements the initiator side of an encrypted session. A
+/// DPE does not use this itself, it is useful for clients and testing.
+pub struct SessionClient<D>
+where
+    D: NoiseCryptoDeps,
+{
+    handshake_state:
+        Option<noise_protocol::HandshakeState<D::DH, D::Cipher, D::Hash>>,
+    /// Cipher state for encrypting messages to a DPE.
+    pub encrypt_cipher_state: NoiseCipherState<D::Cipher>,
+    /// Cipher state for decrypting messages from a DPE.
+    pub decrypt_cipher_state: NoiseCipherState<D::Cipher>,
+    /// PSK seed for deriving sessions. See [`derive_psk`].
+    ///
+    /// [`derive_psk`]: #method.derive_psk
+    pub psk_seed: Hash,
+}
+
+impl<D> Clone for SessionClient<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn clone(&self) -> Self {
+        Self {
+            handshake_state: self.handshake_state.clone(),
+            encrypt_cipher_state: self.encrypt_cipher_state.clone(),
+            decrypt_cipher_state: self.decrypt_cipher_state.clone(),
+            psk_seed: self.psk_seed.clone(),
+        }
+    }
+}
+
+impl<D> Default for SessionClient<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<D> core::fmt::Debug for SessionClient<D>
+where
+    D: NoiseCryptoDeps,
+{
+    fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        Ok(())
+    }
+}
+
+impl<D> SessionClient<D>
+where
+    D: NoiseCryptoDeps,
+{
+    /// Creates a new SessionClient instance. Set up by starting and finishing a
+    /// handshake.
+    pub fn new() -> Self {
+        Self {
+            handshake_state: Default::default(),
+            encrypt_cipher_state: Default::default(),
+            decrypt_cipher_state: Default::default(),
+            psk_seed: Default::default(),
+        }
+    }
+
+    /// Starts a handshake using a known `public_key` and returns a message that
+    /// works with the DPE OpenSession command.
+    pub fn start_handshake_with_known_public_key(
+        &mut self,
+        public_key: &DhPublicKey,
+    ) -> DpeResult<HandshakeMessage> {
+        #[allow(unused_results)]
+        let mut handshake_state = {
+            let mut builder = HandshakeStateBuilder::new();
+            builder.set_pattern(noise_protocol::patterns::noise_nk());
+            builder.set_is_initiator(true);
+            builder.set_prologue(&[]);
+            builder.set_rs(<D::DH as noise_protocol::DH>::Pubkey::from_slice(
+                public_key.as_slice(),
+            ));
+            builder.build_handshake_state()
+        };
+        let mut message = HandshakeMessage::new();
+        handshake_state.write_message(
+            &[],
+            message
+                .as_mut_sized(handshake_state.get_next_message_overhead())?,
+        )?;
+        self.handshake_state = Some(handshake_state);
+        Ok(message)
+    }
+
+    /// Starts a handshake using a `psk` and returns a message that works with
+    /// the DPE DeriveContext command. Use [`derive_psk`] to obtain this value
+    /// from an existing session.
+    ///
+    /// [`derive_psk`]: #method.derive_psk
+    pub fn start_handshake_with_psk(
+        &mut self,
+        psk: &Hash,
+    ) -> DpeResult<HandshakeMessage> {
+        #[allow(unused_results)]
+        let mut handshake_state = {
+            let mut builder = HandshakeStateBuilder::new();
+            builder.set_pattern(noise_protocol::patterns::noise_nn_psk0());
+            builder.set_is_initiator(true);
+            builder.set_prologue(&[]);
+            builder.build_handshake_state()
+        };
+        handshake_state
+            .push_psk(psk.as_slice().get(..32).ok_or(ErrCode::InternalError)?);
+        let mut message = HandshakeMessage::new();
+        handshake_state.write_message(
+            &[],
+            message
+                .as_mut_sized(handshake_state.get_next_message_overhead())?,
+        )?;
+        self.handshake_state = Some(handshake_state);
+        Ok(message)
+    }
+
+    /// Finishes a handshake started using one of the start_handshake_* methods.
+    /// On success, returns the handshake payload from the responder and sets up
+    /// internal state for subsequent calls to encrypt and decrypt.
+    pub fn finish_handshake(
+        &mut self,
+        responder_handshake: &HandshakeMessage,
+    ) -> DpeResult<HandshakePayload> {
+        match self.handshake_state {
+            None => Err(ErrCode::InvalidArgument),
+            Some(ref mut handshake) => {
+                let mut payload = HandshakePayload::new();
+                handshake.read_message(
+                    responder_handshake.as_slice(),
+                    payload.as_mut_sized(
+                        responder_handshake.len()
+                            - handshake.get_next_message_overhead(),
+                    )?,
+                )?;
+                let ciphers = handshake.get_ciphers();
+                self.encrypt_cipher_state = (&ciphers.0).into();
+                self.decrypt_cipher_state = (&ciphers.1).into();
+                self.psk_seed = Hash::from_slice(handshake.get_hash())?;
+                Ok(payload)
+            }
+        }
+    }
+
+    /// Derives a PSK from the current session.
+    pub fn derive_psk(&self) -> Hash {
+        // Note this is from a client perspective so the counters are hashed
+        // encrypt first and unmodified from their current state. A DPE will
+        // reverse the order and decrement the first counter in order to derive
+        // the same value (see derive_psk_from_session).
+        let mut hasher: D::Hash = Default::default();
+        hasher.input(self.psk_seed.as_slice());
+        hasher.input(&self.encrypt_cipher_state.n().to_le_bytes());
+        hasher.input(&self.decrypt_cipher_state.n().to_le_bytes());
+        (&hasher.result()).into()
+    }
+
+    /// Encrypts a message to send to a DPE and commits cipher state changes.
+    pub fn encrypt(&mut self, in_place_buffer: &mut Message) -> DpeResult<()> {
+        NoiseSessionCrypto::<D>::session_encrypt(
+            &mut self.encrypt_cipher_state,
+            in_place_buffer,
+        )?;
+        self.encrypt_cipher_state.commit();
+        Ok(())
+    }
+
+    /// Decrypts a message from a DPE.
+    pub fn decrypt(&mut self, in_place_buffer: &mut Message) -> DpeResult<()> {
+        NoiseSessionCrypto::<D>::session_decrypt(
+            &mut self.decrypt_cipher_state,
+            in_place_buffer,
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    struct DepsForTesting {}
+    impl NoiseCryptoDeps for DepsForTesting {
+        type Cipher = noise_rust_crypto::Aes256Gcm;
+        type DH = noise_rust_crypto::X25519;
+        type Hash = noise_rust_crypto::Sha512;
+    }
+
+    type SessionCryptoForTesting = NoiseSessionCrypto<DepsForTesting>;
+
+    type SessionClientForTesting = SessionClient<DepsForTesting>;
+
+    type CipherStateForTesting = NoiseCipherState<noise_rust_crypto::Aes256Gcm>;
+
+    #[test]
+    fn end_to_end_session() {
+        let mut client = SessionClientForTesting::new();
+        let dh_key: DhPrivateKey = Default::default();
+        let dh_public_key = get_dh_public_key::<
+            <DepsForTesting as NoiseCryptoDeps>::DH,
+        >(&dh_key)
+        .unwrap();
+        let handshake1 = client
+            .start_handshake_with_known_public_key(&dh_public_key)
+            .unwrap();
+        let mut dpe_decrypt_cs: CipherStateForTesting = Default::default();
+        let mut dpe_encrypt_cs: CipherStateForTesting = Default::default();
+        let mut psk_seed = Default::default();
+        let mut handshake2 = Default::default();
+        let payload = HandshakePayload::from_slice("pay".as_bytes()).unwrap();
+        SessionCryptoForTesting::new_session_handshake(
+            &dh_key,
+            &handshake1,
+            &payload,
+            &mut handshake2,
+            &mut dpe_decrypt_cs,
+            &mut dpe_encrypt_cs,
+            &mut psk_seed,
+        )
+        .unwrap();
+        assert_eq!(payload, client.finish_handshake(&handshake2).unwrap());
+
+        // Check that the session works.
+        let mut buffer = Message::from_slice("message".as_bytes()).unwrap();
+        client.encrypt(&mut buffer).unwrap();
+        SessionCryptoForTesting::session_decrypt(
+            &mut dpe_decrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+        SessionCryptoForTesting::session_encrypt(
+            &mut dpe_encrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        dpe_encrypt_cs.commit();
+        client.decrypt(&mut buffer).unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+
+        // Do it again to check session state still works.
+        client.encrypt(&mut buffer).unwrap();
+        SessionCryptoForTesting::session_decrypt(
+            &mut dpe_decrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+        SessionCryptoForTesting::session_encrypt(
+            &mut dpe_encrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        dpe_encrypt_cs.commit();
+        client.decrypt(&mut buffer).unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+    }
+
+    #[test]
+    fn derived_session() {
+        // Set up a session from which to derive.
+        let mut client = SessionClientForTesting::new();
+        let dh_key: DhPrivateKey = Default::default();
+        let dh_public_key = get_dh_public_key::<
+            <DepsForTesting as NoiseCryptoDeps>::DH,
+        >(&dh_key)
+        .unwrap();
+        let handshake1 = client
+            .start_handshake_with_known_public_key(&dh_public_key)
+            .unwrap();
+        let mut dpe_decrypt_cs = Default::default();
+        let mut dpe_encrypt_cs = Default::default();
+        let mut psk_seed = Default::default();
+        let mut handshake2 = Default::default();
+        let payload = HandshakePayload::from_slice("pay".as_bytes()).unwrap();
+        SessionCryptoForTesting::new_session_handshake(
+            &dh_key,
+            &handshake1,
+            &payload,
+            &mut handshake2,
+            &mut dpe_decrypt_cs,
+            &mut dpe_encrypt_cs,
+            &mut psk_seed,
+        )
+        .unwrap();
+        assert_eq!(payload, client.finish_handshake(&handshake2).unwrap());
+
+        // Derive a second session.
+        let mut client2 = SessionClientForTesting::new();
+        let client_psk = client.derive_psk();
+        // Simulate the session state after command decryption on the DPE side
+        // as expected by the DPE PSK logic.
+        let mut buffer = Message::from_slice("message".as_bytes()).unwrap();
+        client.encrypt(&mut buffer).unwrap();
+        SessionCryptoForTesting::session_decrypt(
+            &mut dpe_decrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        let dpe_psk = SessionCryptoForTesting::derive_psk_from_session(
+            &psk_seed,
+            &dpe_decrypt_cs,
+            &dpe_encrypt_cs,
+        )
+        .unwrap();
+        let handshake1 = client2.start_handshake_with_psk(&client_psk).unwrap();
+        let mut dpe_decrypt_cs2 = Default::default();
+        let mut dpe_encrypt_cs2 = Default::default();
+        let mut psk_seed2 = Default::default();
+        SessionCryptoForTesting::derive_session_handshake(
+            &dpe_psk,
+            &handshake1,
+            &payload,
+            &mut handshake2,
+            &mut dpe_decrypt_cs2,
+            &mut dpe_encrypt_cs2,
+            &mut psk_seed2,
+        )
+        .unwrap();
+        assert_eq!(payload, client2.finish_handshake(&handshake2).unwrap());
+
+        // Check that the second session works.
+        let mut buffer = Message::from_slice("message".as_bytes()).unwrap();
+        client2.encrypt(&mut buffer).unwrap();
+        SessionCryptoForTesting::session_decrypt(
+            &mut dpe_decrypt_cs2,
+            &mut buffer,
+        )
+        .unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+        SessionCryptoForTesting::session_encrypt(
+            &mut dpe_encrypt_cs2,
+            &mut buffer,
+        )
+        .unwrap();
+        dpe_encrypt_cs2.commit();
+        client2.decrypt(&mut buffer).unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+
+        // Check that the first session also still works.
+        let mut buffer = Message::from_slice("message".as_bytes()).unwrap();
+        client.encrypt(&mut buffer).unwrap();
+        SessionCryptoForTesting::session_decrypt(
+            &mut dpe_decrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+        SessionCryptoForTesting::session_encrypt(
+            &mut dpe_encrypt_cs,
+            &mut buffer,
+        )
+        .unwrap();
+        dpe_encrypt_cs.commit();
+        client.decrypt(&mut buffer).unwrap();
+        assert_eq!("message".as_bytes(), buffer.as_slice());
+    }
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..89f36c7
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,18 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+max_width = 80
+use_small_heuristics = "Max"
+newline_style = "Unix"
+wrap_comments = true