diff --git a/.changelog/5121.internal.md b/.changelog/5121.internal.md new file mode 100644 index 00000000000..7b0d1989853 --- /dev/null +++ b/.changelog/5121.internal.md @@ -0,0 +1 @@ +crypto/x25519: Add type-safe X25519 private/public key types diff --git a/keymanager/src/crypto/kdf.rs b/keymanager/src/crypto/kdf.rs index 4b1b901d4ee..4a2554647bb 100644 --- a/keymanager/src/crypto/kdf.rs +++ b/keymanager/src/crypto/kdf.rs @@ -12,14 +12,13 @@ use lru::LruCache; use rand::{rngs::OsRng, Rng}; use sgx_isa::Keypolicy; use sp800_185::{CShake, KMac}; -use x25519_dalek; use zeroize::Zeroize; use oasis_core_runtime::{ common::{ crypto::{ mrae::deoxysii::{DeoxysII, NONCE_SIZE, TAG_SIZE}, - signature, + signature, x25519, }, namespace::Namespace, sgx::egetkey::egetkey, @@ -37,7 +36,7 @@ use crate::{ ReplicateResponse, SignedInitResponse, }, client::{KeyManagerClient, RemoteClient}, - crypto::{KeyPair, MasterSecret, PrivateKey, PublicKey, SignedPublicKey, StateKey}, + crypto::{KeyPair, MasterSecret, SignedPublicKey, StateKey}, policy::Policy, runtime::context::Context as KmContext, }; @@ -203,16 +202,11 @@ impl Inner { // Public/private keypair. xof.squeeze(&mut k); - let sk = x25519_dalek::StaticSecret::from(k); + let sk = x25519::PrivateKey::from(k); k.zeroize(); - let pk = x25519_dalek::PublicKey::from(&sk); + let pk = x25519::PublicKey::from(&sk); - Ok(KeyPair::new( - PublicKey(*pk.as_bytes()), - PrivateKey(sk.to_bytes()), - state_key, - checksum, - )) + Ok(KeyPair::new(pk, sk, state_key, checksum)) } fn derive_secret(&self, kdf_custom: &[u8], seed: &[u8]) -> Result> { @@ -429,7 +423,7 @@ impl Kdf { } /// Get the public part of the key. - pub fn get_public_key(&self, req: &impl KeyRequest) -> Result { + pub fn get_public_key(&self, req: &impl KeyRequest) -> Result { let keys = self.get_or_create_keys(req)?; Ok(keys.input_keypair.pk) } @@ -437,7 +431,7 @@ impl Kdf { /// Signs the public key using the key manager key. pub fn sign_public_key( &self, - key: PublicKey, + key: x25519::PublicKey, runtime_id: Namespace, key_pair_id: KeyPairId, epoch: Option, @@ -562,11 +556,14 @@ mod tests { use lru::LruCache; use rustc_hex::{FromHex, ToHex}; - use oasis_core_runtime::common::{crypto::signature::PrivateKey, namespace::Namespace}; + use oasis_core_runtime::common::{ + crypto::{signature::PrivateKey, x25519}, + namespace::Namespace, + }; use crate::{ api::{EphemeralKeyRequest, LongTermKeyRequest}, - crypto::{KeyPairId, MasterSecret, PublicKey}, + crypto::{KeyPairId, MasterSecret}, }; use super::{ @@ -631,7 +628,10 @@ mod tests { .get_or_create_keys(&req) .expect("private key should be created"); - assert_eq!(sk1.input_keypair.sk.0, sk2.input_keypair.sk.0); + assert_eq!( + sk1.input_keypair.sk.0.to_bytes(), + sk2.input_keypair.sk.0.to_bytes() + ); assert_eq!(sk1.input_keypair.pk.0, sk2.input_keypair.pk.0); } @@ -649,7 +649,10 @@ mod tests { .get_or_create_keys(&req) .expect("private key should be created"); - assert_ne!(sk1.input_keypair.sk.0, sk2.input_keypair.sk.0); + assert_ne!( + sk1.input_keypair.sk.0.to_bytes(), + sk2.input_keypair.sk.0.to_bytes() + ); assert_ne!(sk1.input_keypair.pk.0, sk2.input_keypair.pk.0); } @@ -671,7 +674,7 @@ mod tests { fn public_key_signature_is_valid() { let kdf = Kdf::default(); - let pk = PublicKey::from(vec![1u8; 32]); + let pk = x25519::PublicKey::from([1u8; 32]); let runtime_id = Namespace::from(vec![1u8; 32]); let key_pair_id = KeyPairId::from(vec![1u8; 32]); let epoch = Some(10); @@ -681,7 +684,7 @@ mod tests { .sign_public_key(pk, runtime_id, key_pair_id, epoch) .expect("public key should be signed"); - let mut body = pk.as_ref().to_vec(); + let mut body = pk.0.to_bytes().to_vec(); let checksum = kdf.inner.into_inner().unwrap().checksum.unwrap(); body.extend_from_slice(&checksum); @@ -869,8 +872,8 @@ mod tests { .get_or_create_keys(&req) .expect("private key should be created"); - assert_eq!(sk.input_keypair.sk.0.to_hex::(), v.sk); - assert_eq!(sk.input_keypair.pk.0.to_hex::(), v.pk); + assert_eq!(sk.input_keypair.sk.0.to_bytes().to_hex::(), v.sk); + assert_eq!(sk.input_keypair.pk.0.to_bytes().to_hex::(), v.pk); } } } diff --git a/keymanager/src/crypto/types.rs b/keymanager/src/crypto/types.rs index f5e6e840d99..ee387174e38 100644 --- a/keymanager/src/crypto/types.rs +++ b/keymanager/src/crypto/types.rs @@ -3,12 +3,14 @@ use std::sync::Arc; use anyhow::Result; use rand::{rngs::OsRng, Rng}; use thiserror::Error; -use x25519_dalek; use zeroize::Zeroize; use oasis_core_runtime::{ common::{ - crypto::signature::{PublicKey as EdPublicKey, Signature, Signer}, + crypto::{ + signature::{self, Signature, Signer}, + x25519, + }, namespace::Namespace, }, consensus::beacon::EpochTime, @@ -16,7 +18,6 @@ use oasis_core_runtime::{ }; impl_bytes!(KeyPairId, 32, "A 256-bit key pair identifier."); -impl_bytes!(PublicKey, 32, "A public key."); /// Context used for the public key signature. const PUBLIC_KEY_SIGNATURE_CONTEXT: &[u8] = b"oasis-core/keymanager: pk signature"; @@ -27,12 +28,6 @@ const MAX_SIGNED_EPHEMERAL_PUBLIC_KEY_AGE: EpochTime = 10; /// The size of the key manager state checksum. const CHECKSUM_SIZE: usize = 32; -/// A private key. -#[derive(Clone, Default, cbor::Encode, cbor::Decode, Zeroize)] -#[cbor(transparent)] -#[zeroize(drop)] -pub struct PrivateKey(pub [u8; 32]); - /// A state encryption key. #[derive(Clone, Default, cbor::Encode, cbor::Decode, Zeroize)] #[cbor(transparent)] @@ -71,39 +66,39 @@ pub struct KeyPair { impl KeyPair { /// Generate a new random key (for testing). pub fn generate_mock() -> Self { - let mut rng = OsRng {}; - let sk = x25519_dalek::StaticSecret::new(&mut rng); - let pk = x25519_dalek::PublicKey::from(&sk); + let sk = x25519::PrivateKey::generate(); + let pk = x25519::PublicKey::from(&sk); + let mut rng = OsRng {}; let mut state_key = StateKey::default(); rng.fill(&mut state_key.0); - KeyPair::new( - PublicKey(*pk.as_bytes()), - PrivateKey(sk.to_bytes()), - state_key, - vec![], - ) + KeyPair::new(pk, sk, state_key, vec![]) } /// Create a `KeyPair`. - pub fn new(pk: PublicKey, sk: PrivateKey, k: StateKey, sum: Vec) -> Self { + pub fn new( + pk: x25519::PublicKey, + sk: x25519::PrivateKey, + state_key: StateKey, + checksum: Vec, + ) -> Self { Self { input_keypair: InputKeyPair { pk, sk }, - state_key: k, - checksum: sum, + state_key, + checksum, } } /// Create a `KeyPair` with only the public key. - pub fn from_public_key(k: PublicKey, sum: Vec) -> Self { + pub fn from_public_key(pk: x25519::PublicKey, checksum: Vec) -> Self { Self { input_keypair: InputKeyPair { - pk: k, - sk: PrivateKey::default(), + pk, + ..Default::default() }, - state_key: StateKey::default(), - checksum: sum, + checksum, + ..Default::default() } } } @@ -111,9 +106,9 @@ impl KeyPair { #[derive(Clone, Default, cbor::Encode, cbor::Decode)] pub struct InputKeyPair { /// Public key. - pub pk: PublicKey, + pub pk: x25519::PublicKey, /// Private key. - pub sk: PrivateKey, + pub sk: x25519::PrivateKey, } /// Signed public key error. @@ -133,7 +128,7 @@ enum SignedPublicKeyError { #[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)] pub struct SignedPublicKey { /// Public key. - pub key: PublicKey, + pub key: x25519::PublicKey, /// Checksum of the key manager state. pub checksum: Vec, /// Sign(sk, (key || checksum || runtime id || key pair id || epoch || expiration epoch)) from @@ -147,7 +142,7 @@ pub struct SignedPublicKey { impl SignedPublicKey { /// Create a new signed public key. pub fn new( - key: PublicKey, + key: x25519::PublicKey, checksum: Vec, runtime_id: Namespace, key_pair_id: KeyPairId, @@ -177,7 +172,7 @@ impl SignedPublicKey { key_pair_id: KeyPairId, epoch: Option, now: Option, - pk: &EdPublicKey, + pk: &signature::PublicKey, ) -> Result<()> { // Checksum validation. if self.checksum.len() != CHECKSUM_SIZE { @@ -212,14 +207,14 @@ impl SignedPublicKey { } fn body( - key: PublicKey, + key: x25519::PublicKey, checksum: &[u8], runtime_id: Namespace, key_pair_id: KeyPairId, epoch: Option, expiration: Option, ) -> Vec { - let mut body = key.as_ref().to_vec(); + let mut body = key.0.as_bytes().to_vec(); body.extend_from_slice(checksum); body.extend_from_slice(runtime_id.as_ref()); body.extend_from_slice(key_pair_id.as_ref()); @@ -239,15 +234,16 @@ mod test { use oasis_core_runtime::{ common::{ - crypto::signature::{PrivateKey, Signer}, + crypto::{ + signature::{self, Signer}, + x25519, + }, namespace::Namespace, }, consensus::beacon::EpochTime, }; - use crate::crypto::{ - types::MAX_SIGNED_EPHEMERAL_PUBLIC_KEY_AGE, KeyPairId, PublicKey, SignedPublicKey, - }; + use crate::crypto::{types::MAX_SIGNED_EPHEMERAL_PUBLIC_KEY_AGE, KeyPairId, SignedPublicKey}; #[test] fn test_signed_public_key_with_epoch() { @@ -260,10 +256,10 @@ mod test { } fn test_signed_public_key(epoch: Option, now: Option) { - let sk = Arc::new(PrivateKey::from_test_seed("seed".to_string())); + let sk = Arc::new(signature::PrivateKey::from_test_seed("seed".to_string())); let pk = sk.public_key(); - let key = PublicKey([1u8; 32]); + let key = x25519::PublicKey::from([1u8; 32]); let checksum = [1u8; 32].to_vec(); let runtime_id = Namespace::from(vec![1u8; 32]); let key_pair_id = KeyPairId::from(vec![1u8; 32]); @@ -366,7 +362,7 @@ mod test { // Verify the signature with different key. let invalid_signed_pk = SignedPublicKey { - key: PublicKey([2u8; 32]), + key: x25519::PublicKey::from([2u8; 32]), checksum: signed_pk.checksum.clone(), signature: signed_pk.signature.clone(), expiration: signed_pk.expiration, diff --git a/runtime/src/common/crypto/mod.rs b/runtime/src/common/crypto/mod.rs index f6ebd615013..4245a2a2203 100644 --- a/runtime/src/common/crypto/mod.rs +++ b/runtime/src/common/crypto/mod.rs @@ -3,3 +3,4 @@ pub mod hash; pub mod mrae; pub mod signature; +pub mod x25519; diff --git a/runtime/src/common/crypto/mrae/deoxysii.rs b/runtime/src/common/crypto/mrae/deoxysii.rs index f7a7ad55e29..c7cb1f9e5bd 100644 --- a/runtime/src/common/crypto/mrae/deoxysii.rs +++ b/runtime/src/common/crypto/mrae/deoxysii.rs @@ -1,25 +1,21 @@ //! Deoxys-II-256-128 MRAE primitives implementation. +use anyhow::Result; +use rand::rngs::OsRng; +use x25519_dalek::{PublicKey, StaticSecret}; pub use super::deoxysii_rust::{DeoxysII, KEY_SIZE, NONCE_SIZE, TAG_SIZE}; use super::{ hmac::{Hmac, Mac, NewMac}, sha2::Sha512Trunc256, - x25519_dalek, }; -use anyhow::Result; -use rand::rngs::OsRng; - type Kdf = Hmac; /// Derives a MRAE AEAD symmetric key suitable for use with the asymmetric /// box primitives from the provided X25519 public and private keys. -fn derive_symmetric_key(public: &[u8; 32], private: &[u8; 32]) -> [u8; KEY_SIZE] { - let public = x25519_dalek::PublicKey::from(*public); - let private = x25519_dalek::StaticSecret::from(*private); - - let pmk = private.diffie_hellman(&public); +fn derive_symmetric_key(public: &PublicKey, private: &StaticSecret) -> [u8; KEY_SIZE] { + let pmk = private.diffie_hellman(public); let mut kdf = Kdf::new_from_slice(b"MRAE_Box_Deoxys-II-256-128").expect("Hmac::new_from_slice"); kdf.update(pmk.as_bytes()); @@ -34,13 +30,12 @@ fn derive_symmetric_key(public: &[u8; 32], private: &[u8; 32]) -> [u8; KEY_SIZE] /// Generates a public/private key pair suitable for use with /// `derive_symmetric_key`, `box_seal`, and `box_open`. -pub fn generate_key_pair() -> ([u8; 32], [u8; 32]) { +pub fn generate_key_pair() -> (PublicKey, StaticSecret) { let mut rng = OsRng {}; + let sk = StaticSecret::new(&mut rng); + let pk = PublicKey::from(&sk); - let sk = x25519_dalek::StaticSecret::new(&mut rng); - let pk = x25519_dalek::PublicKey::from(&sk); - - (*pk.as_bytes(), sk.to_bytes()) + (pk, sk) } /// Boxes ("seals") the provided additional data and plaintext via @@ -52,8 +47,8 @@ pub fn box_seal( nonce: &[u8; NONCE_SIZE], plaintext: Vec, additional_data: Vec, - peers_public_key: &[u8; 32], - private_key: &[u8; 32], + peers_public_key: &PublicKey, + private_key: &StaticSecret, ) -> Result> { let key = derive_symmetric_key(peers_public_key, private_key); @@ -71,8 +66,8 @@ pub fn box_open( nonce: &[u8; NONCE_SIZE], ciphertext: Vec, additional_data: Vec, - peers_public_key: &[u8; 32], - private_key: &[u8; 32], + peers_public_key: &PublicKey, + private_key: &StaticSecret, ) -> Result> { let key = derive_symmetric_key(peers_public_key, private_key); @@ -96,10 +91,10 @@ mod tests { let (b_pub, b_priv) = generate_key_pair(); // Bob // None of the generated keys should be the same. - assert_ne!(a_pub, b_pub); - assert_ne!(a_priv, b_priv); - assert_ne!(a_pub, a_priv); - assert_ne!(b_pub, b_priv); + assert_ne!(a_pub.to_bytes(), b_pub.to_bytes()); + assert_ne!(a_priv.to_bytes(), b_priv.to_bytes()); + assert_ne!(a_pub.to_bytes(), a_priv.to_bytes()); + assert_ne!(b_pub.to_bytes(), b_priv.to_bytes()); // Should successfully seal the text in a box. let nonce = [1u8; NONCE_SIZE]; diff --git a/runtime/src/common/crypto/x25519.rs b/runtime/src/common/crypto/x25519.rs new file mode 100644 index 00000000000..a396716fe9c --- /dev/null +++ b/runtime/src/common/crypto/x25519.rs @@ -0,0 +1,125 @@ +//! CBOR serializable X25519 types. +use anyhow::Result; +use rand::rngs::OsRng; +use x25519_dalek; +use zeroize::Zeroize; + +/// The length of an X25519 private key, in bytes. +pub const PRIVATE_KEY_LENGTH: usize = 32; + +/// The length of an X25519 public key, in bytes. +pub const PUBLIC_KEY_LENGTH: usize = 32; + +/// A CBOR serializable Diffie-Hellman X25519 private key. +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct PrivateKey(pub x25519_dalek::StaticSecret); + +impl PrivateKey { + /// Generate a new private key. + pub fn generate() -> Self { + let mut rng = OsRng {}; + + PrivateKey(x25519_dalek::StaticSecret::new(&mut rng)) + } + + /// Compute corresponding public key. + pub fn public_key(&self) -> PublicKey { + PublicKey(x25519_dalek::PublicKey::from(&self.0)) + } +} + +impl From<[u8; PRIVATE_KEY_LENGTH]> for PrivateKey { + /// Load private key from a byte array. + fn from(bytes: [u8; PRIVATE_KEY_LENGTH]) -> PrivateKey { + PrivateKey(x25519_dalek::StaticSecret::from(bytes)) + } +} + +impl Default for PrivateKey { + fn default() -> Self { + Self::from([0; PRIVATE_KEY_LENGTH]) + } +} + +impl cbor::Encode for PrivateKey { + fn into_cbor_value(self) -> cbor::Value { + cbor::to_value(self.0.to_bytes()) + } +} + +impl cbor::Decode for PrivateKey { + fn try_from_cbor_value(value: cbor::Value) -> Result { + let mut bytes: [u8; PRIVATE_KEY_LENGTH] = cbor::Decode::try_from_cbor_value(value)?; + let pk = PrivateKey(x25519_dalek::StaticSecret::from(bytes)); + bytes.zeroize(); + Ok(pk) + } +} + +/// A CBOR serializable Diffie-Hellman X25519 public key. +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub struct PublicKey(pub x25519_dalek::PublicKey); + +impl From<[u8; PUBLIC_KEY_LENGTH]> for PublicKey { + /// Load public key from a byte array. + fn from(bytes: [u8; PUBLIC_KEY_LENGTH]) -> PublicKey { + PublicKey(x25519_dalek::PublicKey::from(bytes)) + } +} + +impl From<&PrivateKey> for PublicKey { + /// Given an X25519 private key, compute its corresponding public key. + fn from(sk: &PrivateKey) -> PublicKey { + PublicKey(x25519_dalek::PublicKey::from(&sk.0)) + } +} + +impl Default for PublicKey { + fn default() -> Self { + Self::from([0; PUBLIC_KEY_LENGTH]) + } +} + +impl cbor::Encode for PublicKey { + fn into_cbor_value(self) -> cbor::Value { + cbor::to_value(*self.0.as_bytes()) + } +} + +impl cbor::Decode for PublicKey { + fn try_from_cbor_value(value: cbor::Value) -> Result { + let bytes: [u8; PUBLIC_KEY_LENGTH] = cbor::Decode::try_from_cbor_value(value)?; + let pk = PublicKey(x25519_dalek::PublicKey::from(bytes)); + Ok(pk) + } +} + +#[cfg(test)] +mod tests { + use crate::common::crypto::x25519::{PrivateKey, PublicKey, PRIVATE_KEY_LENGTH}; + + #[test] + fn cbor_serialization() { + let sk = PrivateKey::from([1; PRIVATE_KEY_LENGTH]); + let pk = PublicKey::from(&sk); + + // Encode/decode private key. + let enc = cbor::to_vec(sk.clone()); + let dec: PrivateKey = cbor::from_slice(&enc).expect("deserialization should succeed"); + assert_eq!( + sk.0.to_bytes(), + dec.0.to_bytes(), + "serialization should round-trip" + ); + + // Encode/decode public key. + let enc = cbor::to_vec(pk.clone()); + let dec: PublicKey = cbor::from_slice(&enc).expect("deserialization should succeed"); + assert_eq!( + pk.0.to_bytes(), + dec.0.to_bytes(), + "serialization should round-trip" + ); + } +} diff --git a/tests/runtimes/simple-keyvalue/src/methods.rs b/tests/runtimes/simple-keyvalue/src/methods.rs index 3a9890369ad..04f806bb73a 100644 --- a/tests/runtimes/simple-keyvalue/src/methods.rs +++ b/tests/runtimes/simple-keyvalue/src/methods.rs @@ -3,7 +3,6 @@ use std::{collections::BTreeMap, convert::TryInto}; use io_context::Context as IoContext; -use x25519_dalek; use super::{crypto::EncryptionContext, types::*, Context, TxContext}; use oasis_core_keymanager::crypto::KeyPairId; @@ -12,6 +11,7 @@ use oasis_core_runtime::{ crypto::{ hash::Hash, mrae::deoxysii::{self, NONCE_SIZE}, + x25519, }, key_format::KeyFormat, versioned::Versioned, @@ -377,8 +377,8 @@ impl Methods { .map_err(|err| err.to_string())?; // Generate ephemeral key. Not secure, but good enough for testing purposes. - let ephemeral_sk = x25519_dalek::StaticSecret::from(hash); - let ephemeral_pk = x25519_dalek::PublicKey::from(&ephemeral_sk); + let ephemeral_sk = x25519::PrivateKey::from(hash); + let ephemeral_pk = x25519::PublicKey::from(&ephemeral_sk); // ElGamal encryption. let ciphertext = deoxysii::box_seal( @@ -386,12 +386,12 @@ impl Methods { args.plaintext, vec![], &long_term_pk.key.0, - &ephemeral_sk.to_bytes(), + &ephemeral_sk.0, ) .map_err(|err| format!("failed to encrypt plaintext: {}", err))?; // Return ephemeral_pk || ciphertext. - let mut c = ephemeral_pk.as_bytes().to_vec(); + let mut c = ephemeral_pk.0.as_bytes().to_vec(); c.extend(ciphertext); Ok(Some(c)) @@ -418,24 +418,26 @@ impl Methods { .map_err(|err| format!("private ephemeral key not available: {}", err))?; // Decode ephemeral_pk || ciphertext. - let ephemeral_pk = args + let ephemeral_pk: [u8; x25519::PUBLIC_KEY_LENGTH] = args .ciphertext - .get(0..32) + .get(0..x25519::PUBLIC_KEY_LENGTH) .ok_or("invalid ciphertext")? .try_into() .unwrap(); let ciphertext = args .ciphertext - .get(32..) + .get(x25519::PUBLIC_KEY_LENGTH..) .ok_or("invalid ciphertext")? .to_vec(); + let ephemeral_pk = x25519::PublicKey::from(ephemeral_pk); + // ElGamal decryption. let plaintext = deoxysii::box_open( &[0u8; NONCE_SIZE], ciphertext, vec![], - ephemeral_pk, + &ephemeral_pk.0, &long_term_sk.input_keypair.sk.0, ) .map_err(|err| format!("failed to decrypt ciphertext: {}", err))?;