Skip to content

Commit 95e4505

Browse files
deps: Use ed25519-consensus instead of ed25519-dalek (informalsystems#1067) (informalsystems#1245)
* deps: Use ed25519-consensus instead of ed25519-dalek (informalsystems#1067) * Use ed25519-consensus instead of ed25519-dalek Closes informalsystems#355 (see that issue for more context; `ed25519-consensus` is a fork of `ed25519-zebra` that's Zcash-independent). This change ensures that `tendermint-rs` has the same signature verification as `tendermint`, which uses the validation criteria provided by the Go `ed25519consensus` library. Co-authored-by: Thane Thomson <connect@thanethomson.com> Co-authored-by: Henry de Valence <hdevalence@penumbra.zone>
1 parent 5020067 commit 95e4505

21 files changed

+124
-113
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `[tendermint, tendermint-p2p]` Replaced the `ed25519-dalek` dependency with
2+
`ed25519-consensus`
3+
([#1046](https://github.com/informalsystems/tendermint-rs/pull/1046))

config/src/node_key.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl NodeKey {
3636
pub fn public_key(&self) -> PublicKey {
3737
#[allow(unreachable_patterns)]
3838
match &self.priv_key {
39-
PrivateKey::Ed25519(keypair) => keypair.public.into(),
39+
PrivateKey::Ed25519(signing_key) => PublicKey::Ed25519(signing_key.verification_key()),
4040
_ => unreachable!(),
4141
}
4242
}

p2p/Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "tendermint-p2p"
33
version = "0.28.0"
4-
edition = "2018"
4+
edition = "2021"
55
license = "Apache-2.0"
66
repository = "https://github.com/informalsystems/tendermint-rs"
77
homepage = "https://tendermint.com"
@@ -28,7 +28,7 @@ amino = ["prost-derive"]
2828

2929
[dependencies]
3030
chacha20poly1305 = { version = "0.8", default-features = false, features = ["reduced-round"] }
31-
ed25519-dalek = { version = "1", default-features = false }
31+
ed25519-consensus = { version = "2", default-features = false }
3232
eyre = { version = "0.6", default-features = false }
3333
flume = { version = "0.10.7", default-features = false }
3434
hkdf = { version = "0.10.0", default-features = false }
@@ -37,7 +37,7 @@ prost = { version = "0.11", default-features = false }
3737
rand_core = { version = "0.5", default-features = false, features = ["std"] }
3838
sha2 = { version = "0.9", default-features = false }
3939
subtle = { version = "2", default-features = false }
40-
x25519-dalek = { version = "1.1", default-features = false }
40+
x25519-dalek = { version = "1.1", default-features = false, features = ["u64_backend"] }
4141
zeroize = { version = "1", default-features = false }
4242
signature = { version = "1", default-features = false }
4343
aead = { version = "0.4.1", default-features = false }

p2p/src/error.rs

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use flex_error::{define_error, DisplayOnly};
88
use prost::DecodeError;
9-
use signature::Error as SignatureError;
109

1110
define_error! {
1211
Error {
@@ -40,7 +39,6 @@ define_error! {
4039
| _ | { "public key missing" },
4140

4241
Signature
43-
[ DisplayOnly<SignatureError> ]
4442
| _ | { "signature error" },
4543

4644
UnsupportedKey

p2p/src/secret_connection.rs

+16-24
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use chacha20poly1305::{
1616
aead::{generic_array::GenericArray, AeadInPlace, NewAead},
1717
ChaCha20Poly1305,
1818
};
19-
use ed25519_dalek::{self as ed25519, Signer, Verifier};
2019
use merlin::Transcript;
2120
use rand_core::OsRng;
2221
use subtle::ConstantTimeEq;
@@ -61,7 +60,7 @@ pub struct Handshake<S> {
6160
6261
/// `AwaitingEphKey` means we're waiting for the remote ephemeral pubkey.
6362
pub struct AwaitingEphKey {
64-
local_privkey: ed25519::Keypair,
63+
local_privkey: ed25519_consensus::SigningKey,
6564
local_eph_privkey: Option<EphemeralSecret>,
6665
}
6766

@@ -71,15 +70,15 @@ pub struct AwaitingAuthSig {
7170
kdf: Kdf,
7271
recv_cipher: ChaCha20Poly1305,
7372
send_cipher: ChaCha20Poly1305,
74-
local_signature: ed25519::Signature,
73+
local_signature: ed25519_consensus::Signature,
7574
}
7675

7776
#[allow(clippy::use_self)]
7877
impl Handshake<AwaitingEphKey> {
7978
/// Initiate a handshake.
8079
#[must_use]
8180
pub fn new(
82-
local_privkey: ed25519::Keypair,
81+
local_privkey: ed25519_consensus::SigningKey,
8382
protocol_version: Version,
8483
) -> (Self, EphemeralPublic) {
8584
// Generate an ephemeral key for perfect forward secrecy.
@@ -151,9 +150,9 @@ impl Handshake<AwaitingEphKey> {
151150

152151
// Sign the challenge bytes for authentication.
153152
let local_signature = if self.protocol_version.has_transcript() {
154-
sign_challenge(&sc_mac, &self.state.local_privkey)?
153+
self.state.local_privkey.sign(&sc_mac)
155154
} else {
156-
sign_challenge(&kdf.challenge, &self.state.local_privkey)?
155+
self.state.local_privkey.sign(&kdf.challenge)
157156
};
158157

159158
Ok(Handshake {
@@ -186,22 +185,23 @@ impl Handshake<AwaitingAuthSig> {
186185

187186
let remote_pubkey = match pk_sum {
188187
proto::crypto::public_key::Sum::Ed25519(ref bytes) => {
189-
ed25519::PublicKey::from_bytes(bytes).map_err(Error::signature)
188+
ed25519_consensus::VerificationKey::try_from(&bytes[..])
189+
.map_err(|_| Error::signature())
190190
},
191191
proto::crypto::public_key::Sum::Secp256k1(_) => Err(Error::unsupported_key()),
192192
}?;
193193

194-
let remote_sig =
195-
ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()).map_err(Error::signature)?;
194+
let remote_sig = ed25519_consensus::Signature::try_from(auth_sig_msg.sig.as_slice())
195+
.map_err(|_| Error::signature())?;
196196

197197
if self.protocol_version.has_transcript() {
198198
remote_pubkey
199-
.verify(&self.state.sc_mac, &remote_sig)
200-
.map_err(Error::signature)?;
199+
.verify(&remote_sig, &self.state.sc_mac)
200+
.map_err(|_| Error::signature())?;
201201
} else {
202202
remote_pubkey
203-
.verify(&self.state.kdf.challenge, &remote_sig)
204-
.map_err(Error::signature)?;
203+
.verify(&remote_sig, &self.state.kdf.challenge)
204+
.map_err(|_| Error::signature())?;
205205
}
206206

207207
// We've authorized.
@@ -279,7 +279,7 @@ impl<IoHandler: Read + Write + Send + Sync> SecretConnection<IoHandler> {
279279
/// * if receiving the signature fails
280280
pub fn new(
281281
mut io_handler: IoHandler,
282-
local_privkey: ed25519::Keypair,
282+
local_privkey: ed25519_consensus::SigningKey,
283283
protocol_version: Version,
284284
) -> Result<Self, Error> {
285285
// Start a handshake process.
@@ -470,20 +470,12 @@ fn share_eph_pubkey<IoHandler: Read + Write + Send + Sync>(
470470
protocol_version.decode_initial_handshake(&buf)
471471
}
472472

473-
/// Sign the challenge with the local private key
474-
fn sign_challenge(
475-
challenge: &[u8; 32],
476-
local_privkey: &dyn Signer<ed25519::Signature>,
477-
) -> Result<ed25519::Signature, Error> {
478-
local_privkey.try_sign(challenge).map_err(Error::signature)
479-
}
480-
481473
// TODO(ismail): change from DecodeError to something more generic
482474
// this can also fail while writing / sending
483475
fn share_auth_signature<IoHandler: Read + Write + Send + Sync>(
484476
sc: &mut SecretConnection<IoHandler>,
485-
pubkey: &ed25519::PublicKey,
486-
local_signature: &ed25519::Signature,
477+
pubkey: &ed25519_consensus::VerificationKey,
478+
local_signature: &ed25519_consensus::Signature,
487479
) -> Result<proto::p2p::AuthSigMessage, Error> {
488480
let buf = sc
489481
.protocol_version

p2p/src/secret_connection/amino_types.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Amino types used by Secret Connection
22
33
use core::convert::TryFrom;
4-
5-
use ed25519_dalek as ed25519;
64
use prost_derive::Message;
75
use tendermint_proto as proto;
86

@@ -24,13 +22,16 @@ pub struct AuthSigMessage {
2422
}
2523

2624
impl AuthSigMessage {
27-
pub fn new(pub_key: &ed25519::PublicKey, sig: &ed25519::Signature) -> Self {
25+
pub fn new(
26+
pub_key: &ed25519_consensus::VerificationKey,
27+
sig: &ed25519_consensus::Signature,
28+
) -> Self {
2829
let mut pub_key_bytes = Vec::from(PUB_KEY_ED25519_AMINO_PREFIX);
29-
pub_key_bytes.extend_from_slice(pub_key.as_ref());
30+
pub_key_bytes.extend_from_slice(pub_key.as_bytes());
3031

3132
Self {
3233
pub_key: pub_key_bytes,
33-
sig: sig.as_ref().to_vec(),
34+
sig: sig.to_bytes().to_vec(),
3435
}
3536
}
3637
}

p2p/src/secret_connection/protocol.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use std::convert::TryInto;
44

5-
use ed25519_dalek as ed25519;
65
use prost::Message as _;
76
use tendermint_proto as proto;
87
use x25519_dalek::PublicKey as EphemeralPublic;
@@ -110,8 +109,8 @@ impl Version {
110109
#[must_use]
111110
pub fn encode_auth_signature(
112111
self,
113-
pub_key: &ed25519::PublicKey,
114-
signature: &ed25519::Signature,
112+
pub_key: &ed25519_consensus::VerificationKey,
113+
signature: &ed25519_consensus::Signature,
115114
) -> Vec<u8> {
116115
if self.is_protobuf() {
117116
// Protobuf `AuthSigMessage`
@@ -123,7 +122,7 @@ impl Version {
123122

124123
let msg = proto::p2p::AuthSigMessage {
125124
pub_key: Some(pub_key),
126-
sig: signature.as_ref().to_vec(),
125+
sig: signature.to_bytes().to_vec(),
127126
};
128127

129128
let mut buf = Vec::new();
@@ -165,8 +164,8 @@ impl Version {
165164
#[cfg(feature = "amino")]
166165
fn encode_auth_signature_amino(
167166
self,
168-
pub_key: &ed25519::PublicKey,
169-
signature: &ed25519::Signature,
167+
pub_key: &ed25519_consensus::VerificationKey,
168+
signature: &ed25519_consensus::Signature,
170169
) -> Vec<u8> {
171170
// Legacy Amino encoded `AuthSigMessage`
172171
let msg = amino_types::AuthSigMessage::new(pub_key, signature);
@@ -181,8 +180,8 @@ impl Version {
181180
#[cfg(not(feature = "amino"))]
182181
const fn encode_auth_signature_amino(
183182
self,
184-
_: &ed25519::PublicKey,
185-
_: &ed25519::Signature,
183+
_: &ed25519_consensus::VerificationKey,
184+
_: &ed25519_consensus::Signature,
186185
) -> Vec<u8> {
187186
panic!("attempted to encode auth signature using amino, but 'amino' feature is not present")
188187
}

p2p/src/secret_connection/public_key.rs

+9-10
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
33
use std::fmt::{self, Display};
44

5-
use ed25519_dalek as ed25519;
65
use sha2::{digest::Digest, Sha256};
76
use tendermint::{error::Error, node};
87

98
/// Secret Connection peer public keys (signing, presently Ed25519-only)
109
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1110
pub enum PublicKey {
1211
/// Ed25519 Secret Connection Keys
13-
Ed25519(ed25519::PublicKey),
12+
Ed25519(ed25519_consensus::VerificationKey),
1413
}
1514

1615
impl PublicKey {
@@ -20,14 +19,14 @@ impl PublicKey {
2019
///
2120
/// * if the bytes given are invalid
2221
pub fn from_raw_ed25519(bytes: &[u8]) -> Result<Self, Error> {
23-
ed25519::PublicKey::from_bytes(bytes)
22+
ed25519_consensus::VerificationKey::try_from(bytes)
2423
.map(Self::Ed25519)
25-
.map_err(Error::signature)
24+
.map_err(|_| Error::signature())
2625
}
2726

2827
/// Get Ed25519 public key
2928
#[must_use]
30-
pub const fn ed25519(self) -> Option<ed25519::PublicKey> {
29+
pub const fn ed25519(self) -> Option<ed25519_consensus::VerificationKey> {
3130
match self {
3231
Self::Ed25519(pk) => Some(pk),
3332
}
@@ -54,14 +53,14 @@ impl Display for PublicKey {
5453
}
5554
}
5655

57-
impl From<&ed25519::Keypair> for PublicKey {
58-
fn from(sk: &ed25519::Keypair) -> Self {
59-
Self::Ed25519(sk.public)
56+
impl From<&ed25519_consensus::SigningKey> for PublicKey {
57+
fn from(sk: &ed25519_consensus::SigningKey) -> Self {
58+
Self::Ed25519(sk.verification_key())
6059
}
6160
}
6261

63-
impl From<ed25519::PublicKey> for PublicKey {
64-
fn from(pk: ed25519::PublicKey) -> Self {
62+
impl From<ed25519_consensus::VerificationKey> for PublicKey {
63+
fn from(pk: ed25519_consensus::VerificationKey) -> Self {
6564
Self::Ed25519(pk)
6665
}
6766
}

tendermint/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ rustdoc-args = ["--cfg", "docsrs"]
3232
[dependencies]
3333
bytes = { version = "1.2", default-features = false, features = ["serde"] }
3434
ed25519 = { version = "1.3", default-features = false }
35-
ed25519-dalek = { version = "1", default-features = false, features = ["u64_backend"] }
35+
ed25519-consensus = { version = "2", default-features = false }
3636
futures = { version = "0.3", default-features = false }
3737
num-traits = { version = "0.2", default-features = false }
3838
once_cell = { version = "1.3", default-features = false }

tendermint/src/account.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ mod tests {
161161
let id_bytes = Id::from_str(id_hex).expect("expected id_hex to decode properly");
162162

163163
// get id for pubkey
164-
let pubkey = Ed25519::from_bytes(pubkey_bytes).unwrap();
164+
let pubkey = Ed25519::try_from(&pubkey_bytes[..]).unwrap();
165165
let id = Id::from(pubkey);
166166

167167
assert_eq!(id_bytes.ct_eq(&id).unwrap_u8(), 1);

tendermint/src/error.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,7 @@ define_error! {
209209
|_| { format_args!("subtle encoding error") },
210210

211211
Signature
212-
[ DisplayOnly<signature::Error> ]
213-
|_| { format_args!("signature error") },
212+
|_| { "signature error" },
214213

215214
TrustThresholdTooLarge
216215
|_| { "trust threshold is too large (must be <= 1)" },

tendermint/src/private_key.rs

+28-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
//! Cryptographic private keys
22
3-
pub use ed25519_dalek::{Keypair as Ed25519, EXPANDED_SECRET_KEY_LENGTH as ED25519_KEYPAIR_SIZE};
3+
pub use ed25519_consensus::SigningKey as Ed25519;
4+
5+
use crate::prelude::*;
6+
use crate::public_key::PublicKey;
7+
use ed25519_consensus::VerificationKey;
48
use serde::{de, ser, Deserialize, Serialize};
59
use subtle_encoding::{Base64, Encoding};
610
use zeroize::Zeroizing;
711

8-
use crate::{prelude::*, public_key::PublicKey};
12+
pub const ED25519_KEYPAIR_SIZE: usize = 64;
913

1014
/// Private keys as parsed from configuration files
1115
#[derive(Serialize, Deserialize)]
@@ -25,24 +29,28 @@ impl PrivateKey {
2529
/// Get the public key associated with this private key
2630
pub fn public_key(&self) -> PublicKey {
2731
match self {
28-
PrivateKey::Ed25519(private_key) => private_key.public.into(),
32+
PrivateKey::Ed25519(signing_key) => PublicKey::Ed25519(signing_key.verification_key()),
2933
}
3034
}
3135

3236
/// If applicable, borrow the Ed25519 keypair
33-
pub fn ed25519_keypair(&self) -> Option<&Ed25519> {
37+
pub fn ed25519_signing_key(&self) -> Option<&Ed25519> {
3438
match self {
35-
PrivateKey::Ed25519(keypair) => Some(keypair),
39+
PrivateKey::Ed25519(signing_key) => Some(signing_key),
3640
}
3741
}
3842
}
3943

4044
/// Serialize an Ed25519 keypair as Base64
41-
fn serialize_ed25519_keypair<S>(keypair: &Ed25519, serializer: S) -> Result<S::Ok, S::Error>
45+
fn serialize_ed25519_keypair<S>(signing_key: &Ed25519, serializer: S) -> Result<S::Ok, S::Error>
4246
where
4347
S: ser::Serializer,
4448
{
45-
let keypair_bytes = Zeroizing::new(keypair.to_bytes());
49+
// Tendermint uses a serialization format inherited from Go that includes
50+
// a cached copy of the public key as the second half.
51+
let mut keypair_bytes = Zeroizing::new([0u8; ED25519_KEYPAIR_SIZE]);
52+
keypair_bytes[0..32].copy_from_slice(signing_key.as_bytes());
53+
keypair_bytes[32..64].copy_from_slice(signing_key.verification_key().as_bytes());
4654
Zeroizing::new(String::from_utf8(Base64::default().encode(&keypair_bytes[..])).unwrap())
4755
.serialize(serializer)
4856
}
@@ -63,5 +71,17 @@ where
6371
return Err(D::Error::custom("invalid Ed25519 keypair size"));
6472
}
6573

66-
Ed25519::from_bytes(&*keypair_bytes).map_err(D::Error::custom)
74+
// Tendermint uses a serialization format inherited from Go that includes a
75+
// cached copy of the public key as the second half. This is somewhat
76+
// dangerous, since there's no validation that the two parts are consistent
77+
// with each other, so we ignore the second half and just check consistency
78+
// with the re-derived data.
79+
let signing_key = Ed25519::try_from(&keypair_bytes[0..32])
80+
.map_err(|_| D::Error::custom("invalid signing key"))?;
81+
let verification_key = VerificationKey::from(&signing_key);
82+
if &keypair_bytes[32..64] != verification_key.as_bytes() {
83+
return Err(D::Error::custom("keypair mismatch"));
84+
}
85+
86+
Ok(signing_key)
6787
}

0 commit comments

Comments
 (0)