-
Notifications
You must be signed in to change notification settings - Fork 231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace signatory
with ed25519-dalek
and k256
crates
#522
Conversation
let raw_pk: [u8; PUBLIC_KEY_SIZE] = [ | ||
0x79, 0xce, 0xd, 0xe0, 0x43, 0x33, 0x4a, 0xec, 0xe0, 0x8b, 0x7b, 0xb5, 0x61, 0xbc, | ||
0xe7, 0xc1, 0xd4, 0x69, 0xc3, 0x44, 0x26, 0xec, 0xef, 0xc0, 0x72, 0xa, 0x52, 0x4d, | ||
0x37, 0x32, 0xef, 0xed, | ||
let raw_pk: [u8; PUBLIC_KEY_LENGTH] = [ | ||
0xaf, 0xf3, 0x94, 0xc5, 0xb7, 0x5c, 0xfb, 0xd, 0xd9, 0x28, 0xe5, 0x8a, 0x92, 0xdd, | ||
0x76, 0x55, 0x2b, 0x2e, 0x8d, 0x19, 0x6f, 0xe9, 0x12, 0x14, 0x50, 0x80, 0x6b, 0xd0, | ||
0xd9, 0x3f, 0xd0, 0xcb, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ed(wards)25519 point decompression was failing for this? (the old implementation didn't attempt to decompress points)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've tested against a few freshly generated pub-keys and they all pass! I think that raw_pk
wasn't generated using ed25519.GenPrivKey().PubKey()
and we didn't notice before as the old lib didn't attempt to decompress points.
@@ -4,6 +4,7 @@ | |||
//! blockchain networks, including chain information types, secret connections, | |||
//! and remote procedure calls (JSONRPC). | |||
|
|||
#![cfg_attr(docsrs, feature(doc_cfg))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add this to all our other crates too.
/// Ed25519 keypairs | ||
#[derive(Zeroize)] | ||
#[zeroize(drop)] | ||
pub struct Ed25519Keypair([u8; ED25519_KEYPAIR_SIZE]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to replace this with ed25519-dalek's own ed25519_dalek::Keypair
type, which validates keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the only reason why we were seeing: https://github.com/informalsystems/tendermint-rs/pull/522/files#r469674391
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this type previously didn't check that the associated point would decompress correctly (or was computed correctly from the Ed25519 "seed" in the keypair)
pub fn verify_signature(&self, sign_bytes: &[u8], signature: &[u8]) -> bool { | ||
if let Some(pk) = &self.pub_key.ed25519() { | ||
let verifier = Ed25519Verifier::from(pk); | ||
if let Ok(sig) = ed25519::Signature::from_bytes(signature) { | ||
return verifier.verify(sign_bytes, &sig).is_ok(); | ||
} | ||
} | ||
false | ||
pub fn verify_signature(&self, sign_bytes: &[u8], signature: &Signature) -> Result<(), Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made signature
typed here, and changed it from returning a bool
to a Result
. I very strongly suggest doing this for a few reasons:
Result
is#[must_use]
, so it significantly lowers the risk of ignoring signature validation by making most cases of it a compile-time errorResult
provides better type safety. Here's an example of a real-world signature validation bug which resulted from the use of combinators +bool
instead ofResult
: https://github.com/libp2p/rust-libp2p/pull/1127/files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this one!
let v3 = make_validator( | ||
"EB6B732C4BD86B5FA3F3BC3DB688DA0ED182A7411F81C2D405506B298FC19E52", | ||
"76A2B3F5CBB567F0D689D9DF7155FC89A4C878F040D7A5BB85FF68B74D253FC7", | ||
770_561_664_770_006_272, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another example of an Ed(wards)25519 point which failed to validate. It looks like it might've been munged slightly from the similar looking point below (L284).
It looks like these vectors were tested against the Go implementation, so they need to be rechecked for compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll recheck them during the weekend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect this is a similar problem as above and the key was simply not valid. On the other hand I'm surprised that the other validator keys here (v1, v2) are valid 🤔 As we probably generated them in the same way. From looking at the decompress
code in the rust lib I don't see any discrepancies though.
I would assume that both libraries are properly tested against many test-vectors? https://ed25519.cr.yp.to/python/sign.input (true for the go lib not sure about the rust impl)
A simple way to confirm that this pub-key would also fail the go-implementation is to feed that old bytes into the internal package edwards25519
:
var A edwards25519.ExtendedGroupElement
var publicKeyBytes [32]byte
copy(publicKeyBytes[:], publicKey)
asster.False(t, A.FromBytes(&publicKeyBytes))
I'll give that a try shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so this key won't work in the go implementation either. Tested via:
func TestKey(t *testing.T) {
publicKey, err := hex.DecodeString("EB6B732C4BD86B5FA3F3BC3DB688DA0ED182A7411F81C2D405506B298FC19E52")
if err != nil {
t.Fatalf("%v", err)
}
var A edwards25519.ExtendedGroupElement
var publicKeyBytes [32]byte
copy(publicKeyBytes[:], publicKey)
if A.FromBytes(&publicKeyBytes) {
panic("You shall not pass")
} else {
t.Log("OK")
}
}
And same for the other case. I have no idea where the previous two test-vectors originated from but they never were valid pub-keys and could not have been generated with the equivalent go-code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the take away from this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make sure that the test-vectors we use are generated properly, i.e. they don't just satisfy the properties needed for the test-case (here: 32 random bytes did the job) but rather they should be valid domain objects (here: a valid pub-key) - unless of course we want to test that an invalid case.
testgen/src/validator.rs
Outdated
@@ -32,7 +36,7 @@ impl Validator { | |||
set_option!(proposer_priority, i64); | |||
|
|||
/// Get a signer from this validator companion. | |||
pub fn get_signer(&self) -> Result<Ed25519Signer, SimpleError> { | |||
pub fn get_signer(&self) -> Result<Ed25519Keypair, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this should be renamed to:
pub fn get_signer(&self) -> Result<Ed25519Keypair, SimpleError> { | |
pub fn get_keypair(&self) -> Result<Ed25519Keypair, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
testgen/src/validator.rs
Outdated
let public_key = try_with!(signer.public_key(), "failed to get public key"); | ||
let verifier = Ed25519Verifier::from(&public_key); | ||
Ok(verifier) | ||
pub fn get_verifier(&self) -> Result<Ed25519PublicKey, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise, perhaps rename this to:
pub fn get_verifier(&self) -> Result<Ed25519PublicKey, SimpleError> { | |
pub fn get_verifier(&self) -> Result<Ed25519Verifier, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
tendermint/src/public_key.rs
Outdated
/// Secp256k1 keys | ||
#[cfg(feature = "secp256k1")] | ||
#[cfg_attr(docsrs, doc(cfg(feature = "secp256k1")))] | ||
#[serde( | ||
rename = "tendermint/PubKeySecp256k1", | ||
serialize_with = "serialize_secp256k1_base64", | ||
deserialize_with = "deserialize_secp256k1_base64" | ||
)] | ||
Secp256k1(secp256k1::PublicKey), | ||
Secp256k1(Secp256k1PublicKey), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to keep this gated under a secp256k1
feature, it might make sense to make pub enum PublicKey
a #[non_exhaustive]
enum. Otherwise clients might assume that variants do or do not exist depending on what features are enabled, and if they try to exhaustively match, they'll break if things don't match up to their expectations.
Or, perhaps the (lightweight, pure Rust) k256
crate is a less onerous dependency than signatory-secp256k1
(C library wrapper) to the point that the secp256k1
feature can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All for the enum
with non_exhaustive - how much would it take to remove the secp256k1
feature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove it as a feature, or remove all secp256k1-related code? I think the k256
crate is lightweight enough it doesn't really need to be feature-gated anymore unless there's some other reason for that (e.g. the eventual goal is deprecation).
For what it's worth, the KMS still relies on it for the transaction signing feature.
The clippy failures seem unrelated? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dope! Reviewed mostly for style, consistency and structure.
tendermint/src/public_key.rs
Outdated
/// Secp256k1 keys | ||
#[cfg(feature = "secp256k1")] | ||
#[cfg_attr(docsrs, doc(cfg(feature = "secp256k1")))] | ||
#[serde( | ||
rename = "tendermint/PubKeySecp256k1", | ||
serialize_with = "serialize_secp256k1_base64", | ||
deserialize_with = "deserialize_secp256k1_base64" | ||
)] | ||
Secp256k1(secp256k1::PublicKey), | ||
Secp256k1(Secp256k1PublicKey), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All for the enum
with non_exhaustive - how much would it take to remove the secp256k1
feature?
pub fn verify_signature(&self, sign_bytes: &[u8], signature: &[u8]) -> bool { | ||
if let Some(pk) = &self.pub_key.ed25519() { | ||
let verifier = Ed25519Verifier::from(pk); | ||
if let Ok(sig) = ed25519::Signature::from_bytes(signature) { | ||
return verifier.verify(sign_bytes, &sig).is_ok(); | ||
} | ||
} | ||
false | ||
pub fn verify_signature(&self, sign_bytes: &[u8], signature: &Signature) -> Result<(), Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this one!
testgen/src/validator.rs
Outdated
@@ -32,7 +36,7 @@ impl Validator { | |||
set_option!(proposer_priority, i64); | |||
|
|||
/// Get a signer from this validator companion. | |||
pub fn get_signer(&self) -> Result<Ed25519Signer, SimpleError> { | |||
pub fn get_signer(&self) -> Result<Ed25519Keypair, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
testgen/src/validator.rs
Outdated
let public_key = try_with!(signer.public_key(), "failed to get public key"); | ||
let verifier = Ed25519Verifier::from(&public_key); | ||
Ok(verifier) | ||
pub fn get_verifier(&self) -> Result<Ed25519PublicKey, SimpleError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
5f10d14
to
67466e7
Compare
Yeah, it's a bit weird as the go library also does point decompression as far as I remember. And the test vectors surely were generated using the go implementation (I wish we documented how exactly these were generated). I guess as long as we can "roundtrip" pubkeys between the go and rust implementation we should be fine. A while ago @ValarDragon suggested to cache these decompressions and add a verification method directly on the uncompressed pk as an optimization: tendermint/tendermint#1784. But as far as I know this never materialized. |
Actually, we did document were these test-vectors are coming from, besides, how the raw bytes came about: tendermint-rs/tendermint/src/amino_types/ed25519.rs Lines 106 to 110 in 48a11d1
Note that the go code does not do any checks on these bytes but I would be surprised that the existing test-vectors were not generated from a priv-key. In that case the pubkey should have been compressed, as this is called while generating a key(pair): https://github.com/golang/crypto/blob/123391ffb6de907695e1066dc40c1ff09322aeb6/ed25519/internal/edwards25519/edwards25519.go#L708 My suggestion is to generate a fresh key pair in go and use this as a new test-vector (maybe this is what you did anyways?) and verify everything works as expected. |
https://github.com/informalsystems/tendermint-rs/pull/522/files#r472102039 This is not a compatibility issue! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @tony-iqlusion 👍 I'll try to confirm that the pubkeys that fail to decompress will also fail in the go version and then we are good to merge.
futures = "0.3" | ||
k256 = { version = "0.4", optional = true, features = ["ecdsa"] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't look into k256 but tendermint-rs doesn't really use secp256k1. Hence this is not critical from tendermint's POV.
@@ -4,6 +4,7 @@ | |||
//! blockchain networks, including chain information types, secret connections, | |||
//! and remote procedure calls (JSONRPC). | |||
|
|||
#![cfg_attr(docsrs, feature(doc_cfg))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add this to all our other crates too.
#[serde( | ||
rename = "tendermint/PrivKeyEd25519", | ||
serialize_with = "serialize_ed25519_keypair", | ||
deserialize_with = "deserialize_ed25519_keypair" | ||
)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious: why did you decide to go with the [de]serialize_with
attribute instead of the impl [DE]Serialize for ...
on the type as previously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Ed25519
struct this enum variant wraps is now from a different crate (ed25519_dalek::Keypair
), but the serde serialization logic is intended to match priv_validator.json
(or the bare Base64 encoded format used by the KMS)
/// Ed25519 keypairs | ||
#[derive(Zeroize)] | ||
#[zeroize(drop)] | ||
pub struct Ed25519Keypair([u8; ED25519_KEYPAIR_SIZE]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the only reason why we were seeing: https://github.com/informalsystems/tendermint-rs/pull/522/files#r469674391
let v3 = make_validator( | ||
"EB6B732C4BD86B5FA3F3BC3DB688DA0ED182A7411F81C2D405506B298FC19E52", | ||
"76A2B3F5CBB567F0D689D9DF7155FC89A4C878F040D7A5BB85FF68B74D253FC7", | ||
770_561_664_770_006_272, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect this is a similar problem as above and the key was simply not valid. On the other hand I'm surprised that the other validator keys here (v1, v2) are valid 🤔 As we probably generated them in the same way. From looking at the decompress
code in the rust lib I don't see any discrepancies though.
I would assume that both libraries are properly tested against many test-vectors? https://ed25519.cr.yp.to/python/sign.input (true for the go lib not sure about the rust impl)
A simple way to confirm that this pub-key would also fail the go-implementation is to feed that old bytes into the internal package edwards25519
:
var A edwards25519.ExtendedGroupElement
var publicKeyBytes [32]byte
copy(publicKeyBytes[:], publicKey)
asster.False(t, A.FromBytes(&publicKeyBytes))
I'll give that a try shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
@xla looks like this is blocked on your suggested changes, but I think they've all been done. Any ideas on the clippy error? Will it block merging? |
Native support for the `signature` traits used by the `signatory*` was recently added to `ed25519-dalek` in v1.0.0-pre.4, and the `k256` crate recently added a native pure Rust implementation of ECDSA/secp256k1. Together these changes eliminate the need for the `signatory*` wrapper crates. Additionally, `k256` eliminates the need for the rust-secp256k1 C-based library, which has been problematic with e.g. WASM (#391). This commit eliminates all `signatory` dependencies and uses `ed25519-dalek` and `k256` directly instead. Both of these were already dependencies, so it's not adding any additional dependencies, but instead removes several of them.
67466e7
to
8cfe9a4
Compare
After some investigation the clippy error seems to be related to the enum variants the Specifically the I threw it in a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛂 ♻️ ◻️ 🆔
Native support for the
signature
traits used by thesignatory*
was recently added toed25519-dalek
in v1.0.0-pre.4, and thek256
crate recently added a native pure Rust implementation of ECDSA/secp256k1.Together these changes eliminate the need for the
signatory*
wrapper crates. Additionally,k256
eliminates the need for the rust-secp256k1 C-based library, which has been problematic with e.g. WASM (#391).This commit eliminates all
signatory
dependencies and usesed25519-dalek
andk256
directly instead. Both of these were already dependencies, so it's not adding any additional dependencies, but instead removes several of them.