From db28d2e354f8700c9e4cfe0b9b4aedbdbd655b0f Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 19 Mar 2024 17:20:38 -0700 Subject: [PATCH] Initial port of Chromium's noise_nk handshake. (#4910) * Initial port of Chromium's noise_nk handshake. This is a quick and dirty port of https://source.chromium.org/chromium/chromium/src/+/main:third_party/cloud_authenticator/?q=third_party%2Fcloud_authenticator&ss=chromium. Multiple upgrades are needed in follow-on PRs: * Add client-side code, basically just copying from tests. * Clean up rustcrypto.rs interface to the crypto used. * Add support for noise_kk. This is server-side code, but the client side code can be gleaned from tests. The rustcrypto.py file is derived from code influenced by the Ring crypto API, and ideally we should simplify and clean up the interface. It was meant to be used for linking to different crypto backends, but as Ring was first, rustcrypto.rs is basically a hack to conform to Ring's API. This PR adds initial noise-nk support, but for enclaves talking to other enclaves, we probably need noiew-kk, which should be added. * Fix clippy warnings * Updated header comments. * Ran cargo fmt. * Removed refs that clippy says are redundant. * Ran prettier on oak_crypito/Cargo.toml. * Synced to upstream, which required rerunning cargo fmt. * Fixing some nits from reviews * Changed rustcrypto module name to crypto_wrapper. * More changes based on review comments. * Ran cargo update on enclave_apps, micro_rpc_workspace_test, and oak_restricted_kernel_bin. * replaced [u8; 32] with [u8; ] everywhere. * More changes responding to review feedback. * More changes responding to review feedback. * Added missing tests.rs file. * Added missing tests.rs file. * Commented some bit manipulation * Reran cargo fmt. * Filed issue for TODO. * Build oak_attestation_verification and dependencies with Bazel (#4911) * Bump walkdir from 2.4.0 to 2.5.0 Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.4.0 to 2.5.0. - [Commits](https://github.com/BurntSushi/walkdir/compare/2.4.0...2.5.0) --- updated-dependencies: - dependency-name: walkdir dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Force-enable frame pointers and use them for profiling. * Use slices instead of `Bytes` in hashtable lookup data update code * Replace `spinning_top` with `parking_lot` when `std` is available * Revert Linux kernel to version 6.1.33 (#4915) Version 6.7.6 is not compatible with stage 0 on SEV, SEV-ES and SEV-SNP. Version 6.8 is also not compatible. It looks like 6.9 should be compatible once it is released, so we can upgrade then. Reverting for now to an older version we know was compatible untill we can upgrade to a newer compatible version. See b/327367706 * Backed out changes to Cargo.lock files. * Also back out Cargo.log change in oak_restricted_kernel_bin/Cargo.lock * Updated Cargo.log in oak_crypto. * Ran prettier on README.md * Fixed next issue found by xtask format checks. * Fixed clippy error. * Updated Cargo.lock files that depende on oak_crypto. --------- Signed-off-by: dependabot[bot] Co-authored-by: Ernesto Ocampo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andri Saar Co-authored-by: conradgrobler <58467069+conradgrobler@users.noreply.github.com> --- Cargo.lock | 5 + enclave_apps/Cargo.lock | 16 ++ micro_rpc_workspace_test/Cargo.lock | 28 ++ oak_crypto/Cargo.toml | 13 +- oak_crypto/src/lib.rs | 2 + oak_crypto/src/noise_handshake/README.md | 14 + .../src/noise_handshake/crypto_wrapper.rs | 207 ++++++++++++++ oak_crypto/src/noise_handshake/error.rs | 25 ++ oak_crypto/src/noise_handshake/mod.rs | 258 ++++++++++++++++++ oak_crypto/src/noise_handshake/noise.rs | 248 +++++++++++++++++ oak_crypto/src/noise_handshake/tests.rs | 47 ++++ oak_restricted_kernel_bin/Cargo.lock | 16 ++ 12 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 oak_crypto/src/noise_handshake/README.md create mode 100644 oak_crypto/src/noise_handshake/crypto_wrapper.rs create mode 100644 oak_crypto/src/noise_handshake/error.rs create mode 100644 oak_crypto/src/noise_handshake/mod.rs create mode 100644 oak_crypto/src/noise_handshake/noise.rs create mode 100644 oak_crypto/src/noise_handshake/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 8f1ab478d0d..fefe8fd1f70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2547,13 +2547,18 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "ecdsa", + "hex", "hkdf", "hpke", "micro_rpc_build", "p256", + "pkcs8", + "primeorder", "prost", "rand_core", "sha2", + "static_assertions", "tokio", "zeroize", ] diff --git a/enclave_apps/Cargo.lock b/enclave_apps/Cargo.lock index f260e11010f..e1505742eb5 100644 --- a/enclave_apps/Cargo.lock +++ b/enclave_apps/Cargo.lock @@ -299,6 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -352,6 +353,7 @@ dependencies = [ "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core", "sec1", @@ -715,13 +717,18 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "ecdsa", + "hex", "hkdf", "hpke", "micro_rpc_build", "p256", + "pkcs8", + "primeorder", "prost", "rand_core", "sha2", + "static_assertions", "zeroize", ] @@ -968,6 +975,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.2" diff --git a/micro_rpc_workspace_test/Cargo.lock b/micro_rpc_workspace_test/Cargo.lock index 8fdc55deb69..b25acd9ac21 100644 --- a/micro_rpc_workspace_test/Cargo.lock +++ b/micro_rpc_workspace_test/Cargo.lock @@ -347,6 +347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -394,6 +395,7 @@ dependencies = [ "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core", "sec1", @@ -576,6 +578,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -836,13 +844,18 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "ecdsa", + "hex", "hkdf", "hpke", "micro_rpc_build", "p256", + "pkcs8", + "primeorder", "prost", "rand_core", "sha2", + "static_assertions", "zeroize", ] @@ -888,6 +901,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1274,6 +1296,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.5.0" diff --git a/oak_crypto/Cargo.toml b/oak_crypto/Cargo.toml index fa8d9c92b0b..11b4210d73a 100644 --- a/oak_crypto/Cargo.toml +++ b/oak_crypto/Cargo.toml @@ -13,20 +13,31 @@ aes-gcm = { version = "*", default-features = false, features = [ anyhow = { version = "*", default-features = false } async-trait = { version = "*", default-features = false } bytes = { version = "*", default-features = false } +ecdsa = { version = "*", default-features = false, features = [ + "der", + "pem", + "pkcs8", + "signing", +] } +hex = { version = "*", default-features = false, features = ["alloc"] } hkdf = { version = "*", default-features = false } hpke = { version = "*", default-features = false, features = [ "alloc", "x25519", ] } p256 = { version = "*", default-features = false, features = [ - "ecdsa", "alloc", + "ecdsa", + "pem", ] } +pkcs8 = { version = "*", default-features = false, features = ["alloc"] } +primeorder = { version = "*", default-features = false } prost = { version = "*", default-features = false, features = ["prost-derive"] } rand_core = { version = "*", default-features = false, features = [ "getrandom", ] } sha2 = { version = "*", default-features = false } +static_assertions = "*" zeroize = "*" [build-dependencies] diff --git a/oak_crypto/src/lib.rs b/oak_crypto/src/lib.rs index c2be48307ad..347ccd4de7a 100644 --- a/oak_crypto/src/lib.rs +++ b/oak_crypto/src/lib.rs @@ -17,6 +17,7 @@ #![no_std] extern crate alloc; +extern crate static_assertions; #[cfg(test)] extern crate std; @@ -35,6 +36,7 @@ pub mod proto { pub mod encryption_key; pub mod encryptor; pub mod hpke; +pub mod noise_handshake; pub mod signer; #[cfg(test)] mod tests; diff --git a/oak_crypto/src/noise_handshake/README.md b/oak_crypto/src/noise_handshake/README.md new file mode 100644 index 00000000000..5c6baa87b31 --- /dev/null +++ b/oak_crypto/src/noise_handshake/README.md @@ -0,0 +1,14 @@ +# Noise protocol handshake + +This is a port from Google's internal enclave app repo, which was approved for +open sourcing. + +- [The noise framework](http://www.noiseprotocol.org/noise.html) +- [Noise explorer](https://noiseexplorer.com/patterns/NK/] + +In general, when communicating secrets to an enclave, it is recommended to use +one of the well-reviewed noise variants for multi-round communication between +clients and servers, and HPKE for launch-and-forget style requests. + +Currently only noise-NK is supported, but a future PR is planned for adding +noise-NN support. diff --git a/oak_crypto/src/noise_handshake/crypto_wrapper.rs b/oak_crypto/src/noise_handshake/crypto_wrapper.rs new file mode 100644 index 00000000000..0120853fb22 --- /dev/null +++ b/oak_crypto/src/noise_handshake/crypto_wrapper.rs @@ -0,0 +1,207 @@ +// Copyright 2024 Oak Authors +// +// 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. + +//! The original code supported multiple back-end crypto libraries, but was +//! heavily influenced by the first: Ring. The result was a bit hackish, and +//! should be cleaned up. + +#![allow(clippy::result_unit_err)] + +pub const NONCE_LEN: usize = 12; +pub const SHA256_OUTPUT_LEN: usize = 32; +pub const SYMMETRIC_KEY_LEN: usize = 32; + +/// The length of an uncompressed, X9.62 encoding of a P-256 point. +pub const P256_X962_LEN: usize = 65; + +/// The length of a P-256 scalar value. +pub const P256_SCALAR_LEN: usize = 32; + +use alloc::vec::Vec; + +use aes_gcm::{self, AeadInPlace, KeyInit}; +use ecdsa; +use hkdf; +use p256::ecdsa::signature::Signer; +use pkcs8::{self, DecodePrivateKey, EncodePrivateKey}; +use primeorder::{ + self, + elliptic_curve::{ + ops::{Mul, MulByGenerator}, + sec1::{FromEncodedPoint, ToEncodedPoint}, + }, + Field, PrimeField, +}; +use sha2::{self, Digest}; + +use crate::noise_handshake::crypto_wrapper::ecdsa::signature::Verifier; + +pub fn rand_bytes(_output: &mut [u8]) { + panic!("unimplemented"); +} + +/// Perform the HKDF operation from https://datatracker.ietf.org/doc/html/rfc5869 +pub fn hkdf_sha256(ikm: &[u8], salt: &[u8], info: &[u8], output: &mut [u8]) -> Result<(), ()> { + hkdf::Hkdf::::new(Some(salt), ikm).expand(info, output).map_err(|_| ()) +} + +pub fn aes_256_gcm_seal_in_place( + key: &[u8; SYMMETRIC_KEY_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &mut Vec, +) { + aes_gcm::Aes256Gcm::new_from_slice(key.as_slice()) + .unwrap() + .encrypt_in_place(nonce.into(), aad, plaintext) + .unwrap(); +} + +pub fn aes_256_gcm_open_in_place( + key: &[u8; SYMMETRIC_KEY_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], + mut ciphertext: Vec, +) -> Result, ()> { + aes_gcm::Aes256Gcm::new_from_slice(key.as_slice()) + .unwrap() + .decrypt_in_place(nonce.into(), aad, &mut ciphertext) + .map_err(|_| ())?; + Ok(ciphertext) +} + +pub fn sha256(input: &[u8]) -> [u8; SHA256_OUTPUT_LEN] { + let mut ctx = sha2::Sha256::new(); + ctx.update(input); + ctx.finalize().into() +} + +/// Compute the SHA-256 hash of the concatenation of two inputs. +pub fn sha256_two_part(input1: &[u8], input2: &[u8]) -> [u8; SHA256_OUTPUT_LEN] { + let mut ctx = sha2::Sha256::new(); + ctx.update(input1); + ctx.update(input2); + ctx.finalize().into() +} + +pub struct P256Scalar { + v: p256::Scalar, +} + +impl P256Scalar { + pub fn generate() -> P256Scalar { + let mut ret = [0u8; P256_SCALAR_LEN]; + // Warning: not very random. + ret[0] = 1; + P256Scalar { v: p256::Scalar::from_repr(ret.into()).unwrap() } + } + + pub fn compute_public_key(&self) -> [u8; P256_X962_LEN] { + p256::ProjectivePoint::mul_by_generator(&self.v) + .to_encoded_point(false) + .as_bytes() + .try_into() + .unwrap() + } + + pub fn bytes(&self) -> [u8; P256_SCALAR_LEN] { + self.v.to_repr().as_slice().try_into().unwrap() + } +} + +impl TryFrom<&[u8]> for P256Scalar { + type Error = (); + + fn try_from(bytes: &[u8]) -> Result { + let array: [u8; P256_SCALAR_LEN] = bytes.try_into().map_err(|_| ())?; + (&array).try_into() + } +} + +impl TryFrom<&[u8; P256_SCALAR_LEN]> for P256Scalar { + type Error = (); + + fn try_from(bytes: &[u8; P256_SCALAR_LEN]) -> Result { + let scalar = p256::Scalar::from_repr((*bytes).into()); + if !bool::from(scalar.is_some()) { + return Err(()); + } + let scalar = scalar.unwrap(); + if scalar.is_zero_vartime() { + return Err(()); + } + Ok(P256Scalar { v: scalar }) + } +} + +pub fn p256_scalar_mult( + scalar: &P256Scalar, + point: &[u8; P256_X962_LEN], +) -> Result<[u8; P256_SCALAR_LEN], ()> { + let point = p256::EncodedPoint::from_bytes(point).map_err(|_| ())?; + let affine_point = p256::AffinePoint::from_encoded_point(&point); + if !bool::from(affine_point.is_some()) { + // The peer's point is considered public input and so we don't need to work in + // constant time. + return Err(()); + } + // unwrap: `is_some` checked just above. + let result = affine_point.unwrap().mul(scalar.v).to_encoded_point(false); + let x = result.x().ok_or(())?; + // unwrap: the length of a P256 field-element had better be 32 bytes. + Ok(x.as_slice().try_into().unwrap()) +} + +pub struct EcdsaKeyPair { + key_pair: p256::ecdsa::SigningKey, +} + +impl EcdsaKeyPair { + pub fn from_pkcs8(pkcs8: &[u8]) -> Result { + let key_pair: p256::ecdsa::SigningKey = + p256::ecdsa::SigningKey::from_pkcs8_der(pkcs8).map_err(|_| ())?; + Ok(EcdsaKeyPair { key_pair }) + } + + pub fn generate_pkcs8() -> Result, ()> { + // WARNING: not actually a random scalar + let mut scalar = [0u8; P256_SCALAR_LEN]; + scalar[0] = 42; + let non_zero_scalar = p256::NonZeroScalar::from_repr(scalar.into()).unwrap(); + let key = p256::ecdsa::SigningKey::from(non_zero_scalar); + Ok(key.to_pkcs8_der().map_err(|_| ())?.to_bytes()) + } + + pub fn public_key(&self) -> impl AsRef<[u8]> + '_ { + p256::ecdsa::VerifyingKey::from(&self.key_pair).to_sec1_bytes() + } + + pub fn sign(&self, signed_data: &[u8]) -> Result, ()> { + let sig: ecdsa::Signature = self.key_pair.sign(signed_data); + Ok(sig.to_der()) + } +} + +#[must_use] +pub fn ecdsa_verify(pub_key: &[u8], signed_data: &[u8], signature: &[u8]) -> bool { + let signature = match ecdsa::der::Signature::from_bytes(signature) { + Ok(signature) => signature, + Err(_) => return false, + }; + let key = match p256::ecdsa::VerifyingKey::from_sec1_bytes(pub_key) { + Ok(key) => key, + Err(_) => return false, + }; + key.verify(signed_data, &signature).is_ok() +} diff --git a/oak_crypto/src/noise_handshake/error.rs b/oak_crypto/src/noise_handshake/error.rs new file mode 100644 index 00000000000..5f88d4372a9 --- /dev/null +++ b/oak_crypto/src/noise_handshake/error.rs @@ -0,0 +1,25 @@ +// Copyright 2024 Oak Authors +// +// 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. + +#[derive(Debug, PartialEq)] +pub enum Error { + DataTooLarge(usize), + DecryptFailed, + DecryptionPadding, + EcdhFailed, + EmptyPlaintext, + InvalidHandshake, + InvalidPrivateKey, + Unknown(&'static str), +} diff --git a/oak_crypto/src/noise_handshake/mod.rs b/oak_crypto/src/noise_handshake/mod.rs new file mode 100644 index 00000000000..ff53cada597 --- /dev/null +++ b/oak_crypto/src/noise_handshake/mod.rs @@ -0,0 +1,258 @@ +// Copyright 2024 Oak Authors +// +// 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. + +//! This was copied from Chromium's third_party/cloud_authenticator, which has +//! compatible copyright and ownership (Apache 2.0, Google). + +#[allow(unused_imports)] // Macros only used in tests. +#[macro_use] + +mod crypto_wrapper; +mod error; +mod noise; +#[cfg(test)] +mod tests; + +use alloc::vec::Vec; +use core::result::Result; + +pub use crate::noise_handshake::crypto_wrapper::{ + aes_256_gcm_open_in_place, aes_256_gcm_seal_in_place, ecdsa_verify, hkdf_sha256, + p256_scalar_mult, rand_bytes, sha256, sha256_two_part, EcdsaKeyPair, P256Scalar, NONCE_LEN, + SHA256_OUTPUT_LEN, SYMMETRIC_KEY_LEN, +}; +use crate::noise_handshake::{ + error::Error, + noise::{HandshakeType, Noise}, +}; + +// This is assumed to be vastly larger than any connection will ever reach. +const MAX_SEQUENCE: u32 = 1u32 << 24; + +/// The length of an uncompressed, X9.62 encoding of a P-256 point. +pub const P256_X962_LENGTH: usize = 65; + +pub struct Nonce { + nonce: u32, +} + +impl Nonce { + fn next(&mut self) -> Result<[u8; NONCE_LEN], Error> { + if self.nonce > MAX_SEQUENCE { + return Err(Error::DecryptFailed); + } + let mut ret = [0u8; NONCE_LEN]; + ret[NONCE_LEN - 4..].copy_from_slice(self.nonce.to_be_bytes().as_slice()); + self.nonce += 1; + Ok(ret) + } +} + +pub struct Crypter { + read_key: [u8; SYMMETRIC_KEY_LEN], + write_key: [u8; SYMMETRIC_KEY_LEN], + read_nonce: Nonce, + write_nonce: Nonce, +} + +/// Utility for encrypting and decrypting traffic between the Noise endpoints. +/// It is created by |respond| and configured with a key for each traffic +/// direction. +impl Crypter { + fn new(read_key: &[u8; SYMMETRIC_KEY_LEN], write_key: &[u8; SYMMETRIC_KEY_LEN]) -> Self { + Self { + read_key: *read_key, + write_key: *write_key, + read_nonce: Nonce { nonce: 0 }, + write_nonce: Nonce { nonce: 0 }, + } + } + + pub fn encrypt(&mut self, plaintext: &[u8]) -> Result, Error> { + const PADDING_GRANULARITY: usize = 32; + static_assertions::const_assert!(PADDING_GRANULARITY < 256); + static_assertions::const_assert!((PADDING_GRANULARITY & (PADDING_GRANULARITY - 1)) == 0); + + let mut padded_size: usize = plaintext.len(); + // AES GCM is limited to encrypting 64GiB of data with the same key. + // TODO(#4917): in a follow-on CL, track total data per key and drop the + // connection after 64 GiB. 256MiB is just a sane upper limit on + // message size, which greatly exceeds the noise specified 64KiB, which + // will be too restrictive for our use cases. + if padded_size > (1usize << 28) { + return Err(Error::DataTooLarge(padded_size)); + } + padded_size += 1; // padding-length byte + // This is standard low-level bit manipulation to round up to the nearest + // multiple of PADDING_GRANULARITY. We know PADDING_GRANULARRITY is a + // power of 2, so we compute the mask with !(PADDING_GRANULARITY - 1). + // If padded_size is not already a multiple of PADDING_GRANULARITY, then + // padded_size will not change. Otherwise, it is rounded up to the next + // multiple of PADDED_GRANULARITY. + padded_size = (padded_size + PADDING_GRANULARITY - 1) & !(PADDING_GRANULARITY - 1); + + let mut padded_encrypt_data = Vec::with_capacity(padded_size); + padded_encrypt_data.extend_from_slice(plaintext); + padded_encrypt_data.resize(padded_size, 0u8); + let num_zeros = padded_size - plaintext.len() - 1; + padded_encrypt_data[padded_size - 1] = num_zeros as u8; + + crypto_wrapper::aes_256_gcm_seal_in_place( + &self.write_key, + &self.write_nonce.next()?, + &[], + &mut padded_encrypt_data, + ); + Ok(padded_encrypt_data) + } + + pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result, Error> { + let plaintext = crypto_wrapper::aes_256_gcm_open_in_place( + &self.read_key, + &self.read_nonce.next()?, + &[], + Vec::from(ciphertext), + ) + .map_err(|_| Error::DecryptFailed)?; + + // Plaintext must have a padding byte, and the unpadded length must be + // at least one. + if plaintext.is_empty() || (plaintext[plaintext.len() - 1] as usize) >= plaintext.len() { + return Err(Error::DecryptionPadding); + } + let unpadded_length = plaintext.len() - (plaintext[plaintext.len() - 1] as usize); + Ok(Vec::from(&plaintext[0..unpadded_length - 1])) + } +} + +pub struct Response { + pub crypter: Crypter, + pub handshake_hash: [u8; SHA256_OUTPUT_LEN], + pub response: Vec, +} + +/// Performs the Responder side of the Noise protocol with the NK pattern. +/// |identity_private_key_bytes| contains the private key scalar for the +/// service's provisioned identity. |in_data| is provided by the Initiator and +/// contains its ephemeral public key and encrypted payload. +/// +/// The identity public key is computed from the private key, but could +/// alternatively be stored separately to reduce computation if needed to +/// reduce per-transaction computation. +/// See https://noiseexplorer.com/patterns/NK/ +pub fn respond(identity_private_key_bytes: &[u8], in_data: &[u8]) -> Result { + if in_data.len() < P256_X962_LENGTH { + return Err(Error::InvalidHandshake); + } + + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_hash(&[0; 1]); // Prologue + + let identity_scalar: P256Scalar = + identity_private_key_bytes.try_into().map_err(|_| Error::InvalidPrivateKey)?; + let identity_pub = identity_scalar.compute_public_key(); + + noise.mix_hash_point(identity_pub.as_slice()); + + // unwrap: we know that `in_data` is `P256_X962_LENGTH` bytes long. + let peer_pub: [u8; P256_X962_LENGTH] = (&in_data[..P256_X962_LENGTH]).try_into().unwrap(); + noise.mix_hash(peer_pub.as_slice()); + noise.mix_key(peer_pub.as_slice()); + + let es_ecdh_bytes = crypto_wrapper::p256_scalar_mult(&identity_scalar, &peer_pub) + .map_err(|_| Error::InvalidHandshake)?; + noise.mix_key(es_ecdh_bytes.as_slice()); + + let plaintext = noise.decrypt_and_hash(&in_data[P256_X962_LENGTH..])?; + if !plaintext.is_empty() { + return Err(Error::InvalidHandshake); + } + + // Generate ephemeral key pair + let ephemeral_priv = P256Scalar::generate(); + let ephemeral_pub_key_bytes = ephemeral_priv.compute_public_key(); + noise.mix_hash(ephemeral_pub_key_bytes.as_slice()); + noise.mix_key(ephemeral_pub_key_bytes.as_slice()); + let ee_ecdh_bytes = crypto_wrapper::p256_scalar_mult(&ephemeral_priv, &peer_pub) + .map_err(|_| Error::InvalidHandshake)?; + noise.mix_key(ee_ecdh_bytes.as_slice()); + + let response_ciphertext = noise.encrypt_and_hash(&[]); + + let keys = noise.traffic_keys(); + Ok(Response { + crypter: Crypter::new(&keys.0, &keys.1), + handshake_hash: noise.handshake_hash(), + response: [ephemeral_pub_key_bytes.as_slice(), &response_ciphertext].concat(), + }) +} + +pub mod test_client { + use super::*; + + pub struct HandshakeInitiator { + noise: Noise, + identity_pub_key: [u8; P256_X962_LENGTH], + ephemeral_priv_key: P256Scalar, + } + + impl HandshakeInitiator { + pub fn new(peer_public_key: &[u8; P256_X962_LENGTH]) -> Self { + Self { + noise: Noise::new(HandshakeType::Nk), + identity_pub_key: *peer_public_key, + ephemeral_priv_key: P256Scalar::generate(), + } + } + + pub fn build_initial_message(&mut self) -> Vec { + self.noise.mix_hash(&[0; 1]); + self.noise.mix_hash_point(self.identity_pub_key.as_slice()); + let ephemeral_pub_key = self.ephemeral_priv_key.compute_public_key(); + let ephemeral_pub_key_bytes = ephemeral_pub_key.as_ref(); + + self.noise.mix_hash(ephemeral_pub_key_bytes); + self.noise.mix_key(ephemeral_pub_key_bytes); + let es_ecdh_bytes = + crypto_wrapper::p256_scalar_mult(&self.ephemeral_priv_key, &self.identity_pub_key) + .unwrap(); + self.noise.mix_key(&es_ecdh_bytes); + + let ciphertext = self.noise.encrypt_and_hash(&[]); + [ephemeral_pub_key_bytes, &ciphertext].concat() + } + + pub fn process_response( + &mut self, + handshake_response: &[u8], + ) -> ([u8; SHA256_OUTPUT_LEN], Crypter) { + let peer_public_key_bytes = &handshake_response[..P256_X962_LENGTH]; + let ciphertext = &handshake_response[P256_X962_LENGTH..]; + + let ee_ecdh_bytes = crypto_wrapper::p256_scalar_mult( + &self.ephemeral_priv_key, + peer_public_key_bytes.try_into().unwrap(), + ) + .unwrap(); + self.noise.mix_hash(peer_public_key_bytes); + self.noise.mix_key(peer_public_key_bytes); + self.noise.mix_key(&ee_ecdh_bytes); + + let plaintext = self.noise.decrypt_and_hash(ciphertext).unwrap(); + assert_eq!(plaintext.len(), 0); + let (write_key, read_key) = self.noise.traffic_keys(); + (self.noise.handshake_hash(), Crypter::new(&read_key, &write_key)) + } + } +} diff --git a/oak_crypto/src/noise_handshake/noise.rs b/oak_crypto/src/noise_handshake/noise.rs new file mode 100644 index 00000000000..0c78231a557 --- /dev/null +++ b/oak_crypto/src/noise_handshake/noise.rs @@ -0,0 +1,248 @@ +// Copyright 2024 Oak Authors +// +// 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. + +use alloc::vec::Vec; + +use crypto_wrapper::{NONCE_LEN, SYMMETRIC_KEY_LEN}; + +use crate::noise_handshake::{crypto_wrapper, error::Error}; + +#[derive(Debug, PartialEq)] +pub enum HandshakeType { + Nk, // https://noiseexplorer.com/patterns/NK/ +} + +// Helper to generate 2 keys. +fn hkdf2( + ck: &[u8; SYMMETRIC_KEY_LEN], + ikm: &[u8], +) -> ([u8; SYMMETRIC_KEY_LEN], [u8; SYMMETRIC_KEY_LEN]) { + let mut output = [0; crypto_wrapper::SHA256_OUTPUT_LEN * 2]; + // unwrap: only fails if the output size is too large, but the output + // size is small and fixed here. + crypto_wrapper::hkdf_sha256(ikm, ck, &[], &mut output).unwrap(); + let key1: [u8; SYMMETRIC_KEY_LEN] = output[..SYMMETRIC_KEY_LEN].try_into().unwrap(); + let key2: [u8; SYMMETRIC_KEY_LEN] = output[SYMMETRIC_KEY_LEN..].try_into().unwrap(); + (key1, key2) +} + +#[derive(PartialEq)] +pub struct Noise { + chaining_key: [u8; SYMMETRIC_KEY_LEN], + h: [u8; SYMMETRIC_KEY_LEN], + symmetric_key: [u8; SYMMETRIC_KEY_LEN], + symmetric_nonce: u32, +} + +impl Noise { + pub fn new(handshake_type: HandshakeType) -> Self { + assert_eq!(handshake_type, HandshakeType::Nk); + let mut chaining_key_in = [0; SYMMETRIC_KEY_LEN]; + let nk_protocol_name = b"Noise_NK_P256_AESGCM_SHA256"; + chaining_key_in[..nk_protocol_name.len()].copy_from_slice(nk_protocol_name); + Noise { + chaining_key: chaining_key_in, + h: chaining_key_in, + symmetric_key: [0; SYMMETRIC_KEY_LEN], + symmetric_nonce: 0, + } + } + + pub fn mix_hash(&mut self, in_data: &[u8]) { + // See https://www.noiseprotocol.org/noise.html#the-symmetricstate-object + self.h = crypto_wrapper::sha256_two_part(&self.h, in_data); + } + + pub fn mix_key(&mut self, ikm: &[u8]) { + // See https://www.noiseprotocol.org/noise.html#the-symmetricstate-object + let derived_keys = hkdf2(&self.chaining_key, ikm); + self.chaining_key.copy_from_slice(&derived_keys.0); + self.initialize_key(&derived_keys.1); + } + + #[cfg(test)] + pub fn mix_key_and_hash(&mut self, ikm: &[u8]) { + // See https://www.noiseprotocol.org/noise.html#the-symmetricstate-object + let mut output = [0; SYMMETRIC_KEY_LEN * 3]; + // unwrap: only fails if the output size is too large, but the output + // size is small and fixed here. + crypto_wrapper::hkdf_sha256(ikm, &self.chaining_key, &[], &mut output).unwrap(); + self.chaining_key.copy_from_slice(&output[..SYMMETRIC_KEY_LEN]); + self.mix_hash(&output[SYMMETRIC_KEY_LEN..SYMMETRIC_KEY_LEN * 2]); + self.initialize_key(&output[SYMMETRIC_KEY_LEN * 2..].try_into().unwrap()); + } + + fn next_nonce(&mut self) -> [u8; NONCE_LEN] { + let mut nonce_bytes = [0; NONCE_LEN]; + nonce_bytes[0] = (self.symmetric_nonce >> 24) as u8; + nonce_bytes[1] = (self.symmetric_nonce >> 16) as u8; + nonce_bytes[2] = (self.symmetric_nonce >> 8) as u8; + nonce_bytes[3] = self.symmetric_nonce as u8; + self.symmetric_nonce += 1; + nonce_bytes + } + + pub fn encrypt_and_hash(&mut self, plaintext: &[u8]) -> Vec { + let mut encrypted_data = Vec::from(plaintext); + let nonce = self.next_nonce(); + crypto_wrapper::aes_256_gcm_seal_in_place( + &self.symmetric_key, + &nonce, + &self.h, + &mut encrypted_data, + ); + self.mix_hash(&encrypted_data); + encrypted_data + } + + pub fn decrypt_and_hash(&mut self, ciphertext: &[u8]) -> Result, Error> { + let h = self.h; + self.mix_hash(ciphertext); + + let ciphertext = Vec::from(ciphertext); + let nonce = self.next_nonce(); + let plaintext = + crypto_wrapper::aes_256_gcm_open_in_place(&self.symmetric_key, &nonce, &h, ciphertext) + .map_err(|_| Error::DecryptFailed)?; + Ok(plaintext) + } + + pub fn handshake_hash(&self) -> [u8; SYMMETRIC_KEY_LEN] { + self.h + } + + // |point| is the contents from either agreement::PublicKey::as_ref() or + // agreement::UnparsedPublicKey::bytes(). + pub fn mix_hash_point(&mut self, point: &[u8]) { + self.mix_hash(point); + } + + pub fn traffic_keys(&self) -> ([u8; SYMMETRIC_KEY_LEN], [u8; SYMMETRIC_KEY_LEN]) { + hkdf2(&self.chaining_key, &[]) + } + + fn initialize_key(&mut self, key: &[u8; SYMMETRIC_KEY_LEN]) { + // See https://www.noiseprotocol.org/noise.html#the-cipherstate-object + self.symmetric_key.copy_from_slice(key); + self.symmetric_nonce = 0; + } +} + +#[cfg(test)] +mod tests { + extern crate hex; + + use super::*; + + // The golden values embedded in these tests were generated by using the + // Noise code from the reference implementation: + // http://google3/experimental/users/agl/cablespec/noise.go;l=1;rcl=549756285 + #[test] + fn mix_hash() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_hash("mixHash".as_bytes()); + assert_eq!( + hex::encode(noise.handshake_hash()), + "04d999779401b40a318f8729c99bec79cc15ec375f4a0bb3de1a00965b61d666" + ); + } + + #[test] + fn mix_hash_point() { + let mut noise = Noise::new(HandshakeType::Nk); + let x962_point: [u8; 65] = [ + 0x04, 0x6f, 0x40, 0x4f, 0xbd, 0xa2, 0x1f, 0x6f, 0x26, 0x26, 0x11, 0xe2, 0x00, 0x5c, + 0x57, 0x14, 0x21, 0x72, 0x5c, 0xcb, 0xe8, 0xdd, 0x88, 0xfd, 0xd3, 0x63, 0xb8, 0x20, + 0xde, 0x29, 0x51, 0x67, 0xd0, 0x8d, 0x49, 0x88, 0x07, 0x7e, 0xc5, 0x21, 0x36, 0xd7, + 0x2f, 0x6c, 0xc0, 0x58, 0xee, 0x9a, 0x78, 0x5c, 0xf6, 0xb1, 0x91, 0xc3, 0xd2, 0xaa, + 0x1e, 0x3f, 0x5f, 0x20, 0xb0, 0xea, 0x9b, 0x2b, 0xa0, + ]; + noise.mix_hash_point(&x962_point); + assert_eq!( + hex::encode(noise.handshake_hash()), + "53741f8d5a69c11d8f6f0865193f15f6d756b0d209fd7116b400a4a8a39439b8" + ); + } + + #[test] + fn mix_key_then_encrypt_and_hash() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_key("encryptAndHash".as_bytes()); + let ciphertext = noise.encrypt_and_hash("plaintext".as_bytes()); + assert_eq!(hex::encode(ciphertext), "fdf1564256dcee03b5babe81df599ec4273c95e4269d747764"); + assert_eq!( + hex::encode(noise.handshake_hash()), + "1cf5b0af5f3365d715e9137382a5fd139a56e25bea8ecdbf3018f42af7432a75" + ); + } + + #[test] + fn mix_key_and_hash_then_encrypt_and_hash() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_key_and_hash("encryptAndHash".as_bytes()); + let ciphertext = noise.encrypt_and_hash("plaintext".as_bytes()); + assert_eq!(hex::encode(ciphertext), "2693c80637c7fd9e686949c46a4d189d41ba7cf74a53a85752"); + assert_eq!( + hex::encode(noise.handshake_hash()), + "b883ed0aa2303502e497c8609a979970bc6292cc316eafa7fc0c434f818b9e01" + ); + } + + #[test] + fn decrypt_and_hash() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_key("encryptAndHash".as_bytes()); + // This test uses the values from MixKeyThenEncryptAndHash, but in the + // other direction. + let plaintext = noise + .decrypt_and_hash( + &hex::decode("fdf1564256dcee03b5babe81df599ec4273c95e4269d747764").unwrap(), + ) + .unwrap(); + assert_eq!(hex::encode(plaintext), "706c61696e74657874"); + assert_eq!( + hex::encode(noise.handshake_hash()), + "1cf5b0af5f3365d715e9137382a5fd139a56e25bea8ecdbf3018f42af7432a75" + ); + } + + #[test] + fn split() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_key_and_hash("split".as_bytes()); + let keys = noise.traffic_keys(); + assert_eq!( + hex::encode(keys.0), + "1f73215029f964ce0a65ef92eaf97bd67c45feff8e49cca94fdf5050aaf25a58" + ); + assert_eq!( + hex::encode(keys.1), + "0c7f90f88233858ecbd6492c273ed328acaef75ebcb31fd2f94033c5d296a623" + ); + } + + #[test] + fn combined() { + let mut noise = Noise::new(HandshakeType::Nk); + noise.mix_hash("mixHash".as_bytes()); + noise.mix_key("mixKey".as_bytes()); + noise.mix_key_and_hash("mixKeyAndHash".as_bytes()); + let ciphertext = noise.encrypt_and_hash("plaintext".as_bytes()); + assert_eq!(hex::encode(ciphertext), "b7235b3d77c9cf5ebb087793b399de2c2276edae52bba5199e"); + assert_eq!( + hex::encode(noise.handshake_hash()), + "db420fd8f9b072eacca4599019303f02b1938fb9096e3b4b063afebc687c9987" + ); + } +} diff --git a/oak_crypto/src/noise_handshake/tests.rs b/oak_crypto/src/noise_handshake/tests.rs new file mode 100644 index 00000000000..a7dcc99776b --- /dev/null +++ b/oak_crypto/src/noise_handshake/tests.rs @@ -0,0 +1,47 @@ +// Copyright 2024 Oak Authors +// +// 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. + +#[cfg(test)] +use alloc::vec; + +use crate::noise_handshake::{respond, test_client::HandshakeInitiator, P256Scalar}; + +#[test] +fn process_handshake() { + let test_messages = vec![vec![1u8, 2u8, 3u8, 4u8], vec![4u8, 3u8, 2u8, 1u8], vec![]]; + let identity_priv = P256Scalar::generate(); + let identity_pub_bytes = identity_priv.compute_public_key(); + let mut initiator = HandshakeInitiator::new(&identity_pub_bytes); + let message = initiator.build_initial_message(); + let handshake_response = respond(identity_priv.bytes().as_slice(), &message).unwrap(); + let mut enclave_crypter = handshake_response.crypter; + + let (client_hash, mut client_crypter) = + initiator.process_response(&handshake_response.response); + assert_eq!(&client_hash, &handshake_response.handshake_hash); + + // Client -> Enclave encrypt+decrypt + for message in &test_messages { + let ciphertext = client_crypter.encrypt(message).unwrap(); + let plaintext = enclave_crypter.decrypt(&ciphertext).unwrap(); + assert_eq!(message, &plaintext); + } + + // Enclave -> Client encrypt+decrypt + for message in &test_messages { + let ciphertext = enclave_crypter.encrypt(message).unwrap(); + let plaintext = client_crypter.decrypt(&ciphertext).unwrap(); + assert_eq!(message, &plaintext); + } +} diff --git a/oak_restricted_kernel_bin/Cargo.lock b/oak_restricted_kernel_bin/Cargo.lock index 6089e04b1a7..2e2a6c5f2c4 100644 --- a/oak_restricted_kernel_bin/Cargo.lock +++ b/oak_restricted_kernel_bin/Cargo.lock @@ -325,6 +325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -372,6 +373,7 @@ dependencies = [ "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core", "sec1", @@ -674,13 +676,18 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "ecdsa", + "hex", "hkdf", "hpke", "micro_rpc_build", "p256", + "pkcs8", + "primeorder", "prost", "rand_core", "sha2", + "static_assertions", "zeroize", ] @@ -853,6 +860,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.4"