diff --git a/Cargo.lock b/Cargo.lock index a7282507..4ff71d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2642,6 +2642,7 @@ dependencies = [ "criterion", "ed25519-dalek", "hex-literal", + "hkdf", "libsecp256k1", "log", "multihash", @@ -2928,6 +2929,7 @@ dependencies = [ "quickcheck", "quinn", "rand 0.8.5", + "ring", "rustls 0.21.7", "socket2 0.5.4", "thiserror", diff --git a/identity/CHANGELOG.md b/identity/CHANGELOG.md index b52f0018..89fd8dfa 100644 --- a/identity/CHANGELOG.md +++ b/identity/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.4 - unreleased + +- Implement `Keypair::derive_secret`, to deterministically derive a new secret from the embedded secret key. + See [PR 4554]. + +[PR 4554]: https://github.com/libp2p/rust-libp2p/pull/4554 + ## 0.2.3 - Fix [RUSTSEC-2022-0093] by updating `ed25519-dalek` to `2.0`. diff --git a/identity/Cargo.toml b/identity/Cargo.toml index d8f4d9ef..b90588ba 100644 --- a/identity/Cargo.toml +++ b/identity/Cargo.toml @@ -15,6 +15,7 @@ categories = ["cryptography"] asn1_der = { version = "0.7.6", optional = true } bs58 = { version = "0.5.0", optional = true } ed25519-dalek = { version = "2.0", optional = true, features = ["rand_core"] } +hkdf = { version = "0.12.3", optional = true } libsecp256k1 = { version = "0.7.0", optional = true } log = "0.4" multihash = { version = "0.19.1", optional = true } @@ -32,11 +33,11 @@ zeroize = { version = "1.6", optional = true } ring = { version = "0.16.9", features = ["alloc", "std"], default-features = false, optional = true} [features] -secp256k1 = [ "dep:libsecp256k1", "dep:asn1_der", "dep:rand", "dep:sha2", "dep:zeroize" ] -ecdsa = [ "dep:p256", "dep:rand", "dep:void", "dep:zeroize", "dep:sec1" ] +secp256k1 = [ "dep:libsecp256k1", "dep:asn1_der", "dep:rand", "dep:sha2", "dep:hkdf", "dep:zeroize" ] +ecdsa = [ "dep:p256", "dep:rand", "dep:void", "dep:zeroize", "dep:sec1", "dep:sha2", "dep:hkdf" ] rsa = [ "dep:ring", "dep:asn1_der", "dep:rand", "dep:zeroize" ] -ed25519 = [ "dep:ed25519-dalek", "dep:rand", "dep:zeroize" ] -peerid = [ "dep:multihash", "dep:bs58", "dep:rand", "dep:thiserror", "dep:sha2" ] +ed25519 = [ "dep:ed25519-dalek", "dep:rand", "dep:zeroize", "dep:sha2", "dep:hkdf" ] +peerid = [ "dep:multihash", "dep:bs58", "dep:rand", "dep:thiserror", "dep:sha2", "dep:hkdf" ] [dev-dependencies] quickcheck = { workspace = true } diff --git a/identity/src/ed25519.rs b/identity/src/ed25519.rs index 06708ce6..8b6b9e0d 100644 --- a/identity/src/ed25519.rs +++ b/identity/src/ed25519.rs @@ -197,6 +197,10 @@ impl SecretKey { sk_bytes.zeroize(); Ok(SecretKey(secret)) } + + pub(crate) fn to_bytes(&self) -> [u8; 32] { + self.0 + } } #[cfg(test)] diff --git a/identity/src/keypair.rs b/identity/src/keypair.rs index ac6edec8..e55fcef2 100644 --- a/identity/src/keypair.rs +++ b/identity/src/keypair.rs @@ -342,6 +342,59 @@ impl Keypair { KeyPairInner::Ecdsa(_) => KeyType::Ecdsa, } } + + /// Deterministically derive a new secret from this [`Keypair`], taking into account the provided domain. + /// + /// This works for all key types except RSA where it returns `None`. + /// + /// # Example + /// + /// ``` + /// # fn main() { + /// # use libp2p_identity as identity; + /// + /// let key = identity::Keypair::generate_ed25519(); + /// + /// let new_key = key.derive_secret(b"my encryption key").expect("can derive secret for ed25519"); + /// # } + /// ``` + #[allow(unused_variables, unreachable_code)] + pub fn derive_secret(&self, domain: &[u8]) -> Option<[u8; 32]> { + #[cfg(any( + feature = "ecdsa", + feature = "secp256k1", + feature = "ed25519", + feature = "rsa" + ))] + return Some( + hkdf::Hkdf::::extract(None, &[domain, &self.secret()?].concat()) + .0 + .into(), + ); + + None + } + + /// Return the secret key of the [`Keypair`]. + #[allow(dead_code)] + pub(crate) fn secret(&self) -> Option<[u8; 32]> { + match self.keypair { + #[cfg(feature = "ed25519")] + KeyPairInner::Ed25519(ref inner) => Some(inner.secret().to_bytes()), + #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] + KeyPairInner::Rsa(_) => return None, + #[cfg(feature = "secp256k1")] + KeyPairInner::Secp256k1(ref inner) => Some(inner.secret().to_bytes()), + #[cfg(feature = "ecdsa")] + KeyPairInner::Ecdsa(ref inner) => Some( + inner + .secret() + .to_bytes() + .try_into() + .expect("Ecdsa's private key should be 32 bytes"), + ), + } + } } #[cfg(feature = "ecdsa")] @@ -901,4 +954,11 @@ mod tests { assert_eq!(converted_pubkey, pubkey); assert_eq!(converted_pubkey.key_type(), KeyType::Ecdsa) } + + #[test] + #[cfg(feature = "ecdsa")] + fn test_secret_from_ecdsa_private_key() { + let keypair = Keypair::generate_ecdsa(); + assert!(keypair.derive_secret(b"domain separator!").is_some()) + } } diff --git a/transports/quic/CHANGELOG.md b/transports/quic/CHANGELOG.md index 7cf54e89..a7ad810b 100644 --- a/transports/quic/CHANGELOG.md +++ b/transports/quic/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.9.3 - unreleased + +- Support QUIC stateless resets for supported `libp2p_identity::Keypair`s. See [PR 4554]. + +[PR 4554]: https://github.com/libp2p/rust-libp2p/pull/4554 + ## 0.9.2 - Cut stable release. diff --git a/transports/quic/Cargo.toml b/transports/quic/Cargo.toml index 671032a8..cac3c05a 100644 --- a/transports/quic/Cargo.toml +++ b/transports/quic/Cargo.toml @@ -25,6 +25,7 @@ rustls = { version = "0.21.7", default-features = false } thiserror = "1.0.48" tokio = { version = "1.32.0", default-features = false, features = ["net", "rt", "time"], optional = true } socket2 = "0.5.4" +ring = "0.16.20" [features] tokio = ["dep:tokio", "if-watch/tokio", "quinn/runtime-tokio"] diff --git a/transports/quic/src/config.rs b/transports/quic/src/config.rs index 201594e2..5351a537 100644 --- a/transports/quic/src/config.rs +++ b/transports/quic/src/config.rs @@ -61,6 +61,8 @@ pub struct Config { client_tls_config: Arc, /// TLS server config for the inner [`quinn::ServerConfig`]. server_tls_config: Arc, + /// Libp2p identity of the node. + keypair: libp2p_identity::Keypair, } impl Config { @@ -80,6 +82,7 @@ impl Config { // Ensure that one stream is not consuming the whole connection. max_stream_data: 10_000_000, + keypair: keypair.clone(), } } } @@ -104,6 +107,7 @@ impl From for QuinnConfig { max_stream_data, support_draft_29, handshake_timeout: _, + keypair, } = config; let mut transport = quinn::TransportConfig::default(); // Disable uni-directional streams. @@ -128,7 +132,14 @@ impl From for QuinnConfig { let mut client_config = quinn::ClientConfig::new(client_tls_config); client_config.transport_config(transport); - let mut endpoint_config = quinn::EndpointConfig::default(); + let mut endpoint_config = keypair + .derive_secret(b"libp2p quic stateless reset key") + .map(|secret| { + let reset_key = Arc::new(ring::hmac::Key::new(ring::hmac::HMAC_SHA256, &secret)); + quinn::EndpointConfig::new(reset_key) + }) + .unwrap_or_default(); + if !support_draft_29 { endpoint_config.supported_versions(vec![1]); }