diff --git a/.changelog/unreleased/features/1364-secp256k1-keys.md b/.changelog/unreleased/features/1364-secp256k1-keys.md new file mode 100644 index 000000000..51ea5faa9 --- /dev/null +++ b/.changelog/unreleased/features/1364-secp256k1-keys.md @@ -0,0 +1,2 @@ +- `[tendermint]` Add support for secp256k1 consensus keys + ([\#1364](https://github.com/informalsystems/tendermint-rs/issues/1364)) \ No newline at end of file diff --git a/config/Cargo.toml b/config/Cargo.toml index 8587e97e1..391081fef 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -33,3 +33,6 @@ url = { version = "2.2" } [dev-dependencies] pretty_assertions = "1.3.0" + +[features] +secp256k1 = ["tendermint/secp256k1"] diff --git a/config/tests/mod.rs b/config/tests/mod.rs index 767a491a2..c333c2b84 100644 --- a/config/tests/mod.rs +++ b/config/tests/mod.rs @@ -213,10 +213,10 @@ fn node_key_parser() { ); } -/// Parse an example `priv_validator_key.json` to a `PrivValidatorKey` struct +/// Parse an example `priv_validator_key.ed25519.json` to a `PrivValidatorKey` struct #[test] -fn priv_validator_json_parser() { - let raw_priv_validator_key = read_fixture("priv_validator_key.json"); +fn priv_validator_ed25519_json_parser() { + let raw_priv_validator_key = read_fixture("priv_validator_key.ed25519.json"); let priv_validator_key = PrivValidatorKey::parse_json(raw_priv_validator_key).unwrap(); assert_eq!( priv_validator_key.consensus_pubkey().public_key().to_hex(), @@ -224,6 +224,18 @@ fn priv_validator_json_parser() { ); } +/// Parse an example `priv_validator_key.secp256k1.json` to a `PrivValidatorKey` struct +#[test] +#[cfg(feature = "secp256k1")] +fn priv_validator_secp256k1_json_parser() { + let raw_priv_validator_key = read_fixture("priv_validator_key.secp256k1.json"); + let priv_validator_key = PrivValidatorKey::parse_json(raw_priv_validator_key).unwrap(); + assert_eq!( + priv_validator_key.consensus_pubkey().public_key().to_hex(), + "03685D6F62E54D0FACC6AA649E4036BF446043A6BA438B793B085E6C8A92AFCAAE" + ); +} + /// Parse an example `config.toml` file to a `TendermintConfig` struct, then /// serialize it and parse again. #[test] diff --git a/config/tests/support/config/priv_validator_key.json b/config/tests/support/config/priv_validator_key.ed25519.json similarity index 100% rename from config/tests/support/config/priv_validator_key.json rename to config/tests/support/config/priv_validator_key.ed25519.json diff --git a/config/tests/support/config/priv_validator_key.secp256k1.json b/config/tests/support/config/priv_validator_key.secp256k1.json new file mode 100644 index 000000000..ad62f0d4c --- /dev/null +++ b/config/tests/support/config/priv_validator_key.secp256k1.json @@ -0,0 +1,11 @@ +{ + "address": "0799B6E1F13F1ECC7E891765F499A73665F42866", + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A2hdb2LlTQ+sxqpknkA2v0RgQ6a6Q4t5OwhebIqSr8qu" + }, + "priv_key": { + "type": "tendermint/PrivKeySecp256k1", + "value": "aVmw0npSBEqPU4v6qEZ9o3z5nCpHjicwFDdIBRUbSrM=" + } +} \ No newline at end of file diff --git a/tendermint/src/private_key.rs b/tendermint/src/private_key.rs index f5b06a988..052899d56 100644 --- a/tendermint/src/private_key.rs +++ b/tendermint/src/private_key.rs @@ -1,6 +1,9 @@ //! Cryptographic private keys pub use crate::crypto::ed25519::SigningKey as Ed25519; +#[cfg(feature = "secp256k1")] +pub use k256::ecdsa::SigningKey as Secp256k1; + use crate::prelude::*; #[cfg(feature = "rust-crypto")] @@ -14,6 +17,7 @@ use subtle_encoding::{Base64, Encoding}; use zeroize::Zeroizing; pub const ED25519_KEYPAIR_SIZE: usize = 64; +pub const SECP256K1_KEY_SIZE: usize = 32; /// Private keys as parsed from configuration files #[cfg_attr(feature = "rust-crypto", derive(Serialize, Deserialize))] @@ -30,6 +34,15 @@ pub enum PrivateKey { ) )] Ed25519(Ed25519), + + #[cfg(feature = "secp256k1")] + #[cfg_attr(docsrs, doc(cfg(feature = "secp256k1")))] + #[serde( + rename = "tendermint/PrivKeySecp256k1", + serialize_with = "serialize_secp256k1_privkey", + deserialize_with = "deserialize_secp256k1_privkey" + )] + Secp256k1(Secp256k1), } impl PrivateKey { @@ -38,6 +51,11 @@ impl PrivateKey { pub fn public_key(&self) -> PublicKey { match self { PrivateKey::Ed25519(signing_key) => PublicKey::Ed25519(signing_key.verification_key()), + + #[cfg(feature = "secp256k1")] + PrivateKey::Secp256k1(signing_key) => { + PublicKey::Secp256k1(*signing_key.verifying_key()) + }, } } @@ -45,10 +63,46 @@ impl PrivateKey { pub fn ed25519_signing_key(&self) -> Option<&Ed25519> { match self { PrivateKey::Ed25519(signing_key) => Some(signing_key), + + #[cfg(feature = "secp256k1")] + PrivateKey::Secp256k1(_signing_key) => None, } } } +/// Serialize a Secp256k1 privkey as Base64 +#[cfg(feature = "secp256k1")] +fn serialize_secp256k1_privkey(signing_key: &Secp256k1, serializer: S) -> Result +where + S: ser::Serializer, +{ + Zeroizing::new(String::from_utf8(Base64::default().encode(signing_key.to_bytes())).unwrap()) + .serialize(serializer) +} + +/// Deserialize a Secp256k1 privkey from Base64 +#[cfg(feature = "secp256k1")] +fn deserialize_secp256k1_privkey<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + use de::Error; + let string = Zeroizing::new(String::deserialize(deserializer)?); + let mut privkey_bytes = Zeroizing::new([0u8; SECP256K1_KEY_SIZE]); + let decoded_len = Base64::default() + .decode_to_slice(string.as_bytes(), &mut *privkey_bytes) + .map_err(D::Error::custom)?; + + if decoded_len != SECP256K1_KEY_SIZE { + return Err(D::Error::custom("invalid sepc256k1 privkey size")); + } + + let signing_key = Secp256k1::try_from(&privkey_bytes[0..SECP256K1_KEY_SIZE]) + .map_err(|_| D::Error::custom("invalid signing key"))?; + + Ok(signing_key) +} + /// Serialize an Ed25519 keypair as Base64 #[cfg(feature = "rust-crypto")] fn serialize_ed25519_keypair(signing_key: &Ed25519, serializer: S) -> Result diff --git a/tendermint/src/public_key.rs b/tendermint/src/public_key.rs index 17abfb9ba..5040dd497 100644 --- a/tendermint/src/public_key.rs +++ b/tendermint/src/public_key.rs @@ -290,8 +290,11 @@ impl TendermintKey { #[allow(unreachable_patterns)] match public_key { PublicKey::Ed25519(_) => Ok(TendermintKey::AccountKey(public_key)), + #[cfg(feature = "secp256k1")] + PublicKey::Secp256k1(_) => Ok(TendermintKey::AccountKey(public_key)), + _ => Err(Error::invalid_key( - "only ed25519 consensus keys are supported".to_string(), + "only ed25519 or secp256k1 consensus keys are supported".to_string(), )), } }