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