-
Notifications
You must be signed in to change notification settings - Fork 228
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
Add support for Secp256k1 signatures #962
Conversation
#[derive(Copy, Clone, Debug, PartialEq)] | ||
#[non_exhaustive] | ||
pub enum Signature { | ||
/// Ed25519 block signature | ||
Ed25519(Ed25519Signature), | ||
/// No signature present | ||
None, /* This could have been implemented as an `Option<>` but then handling it would be | ||
* outside the scope of this enum. */ | ||
} | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub struct Signature(Vec<u8>); |
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.
It seems somewhat problematic to me to discard all information about the signature algorithm.
Based on the TryFrom
impl below it looks like this would accept any bytestring as a "signature" except for empty ones, and only tries to validate that the signature is well-formed for a particular algorithm lazily at verification time, which means this type could accept all sorts of potential garbage data as a signature.
This includes signatures that are not the valid size for a particular algorithm, or signatures whose elements are't a valid element of e.g. the scalar field for a particular elliptic curve.
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 we tried finding another way around this, but there's no discriminant built into the signatures themselves that allows you to determine what kind of signature it is.
How else would one know this up-front, so that you could validate them beforehand?
The only way I can think of would require changing our serialization approach substantially, such that we can only deserialize signatures from within data structures that provide information about what algorithm was used. This complicates the serialization picture significantly.
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 wondering if there was a multihash (https://github.com/multiformats/multihash) equivalent for signatures.
I haven't been able to find one but it would be nice to have some sort of standardized self-describing signatures.
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.
Self-describing signatures would be amazing, but that change would need to happen in the Tendermint RPC.
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.
At the very least the only two supported signature types are both 64-bytes, so you could check for that.
It would probably be a good idea to eventually fix this upstream in Tendermint, but if it doesn't do that already I agree there's nothing relevant to this PR.
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 agree, a 64-byte length check would be good 👍
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.
try using k256::ecdsa::Signature::from_scalars to create signature
pub fn new_secp256k1_signature(value : &[u8]) -> Result<Secp256k1Signature, Error> {
if value.len() != 64 {
return Err(Kind::InvalidSignatureIdLength.into());
}
let (r_bytes, s_bytes) = value.split_at(32);
let r = match NonZeroScalar::from_repr(GenericArray::clone_from_slice(r_bytes)) {
Some(v) => v,
None => return Err(Kind::InvalidSignature.into()),
};
let s = match NonZeroScalar::from_repr(GenericArray::clone_from_slice(s_bytes)) {
Some(v) => v,
None => return Err(Kind::InvalidSignature.into()),
};
Secp256k1Signature::from_scalars(r,s).map_err(|_| Kind::InvalidSignature.into())
}
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.
TryFrom<&[u8]>
will also accomplish the same thing, and is a bit simpler, e.g.
Secp256k1Signature::try_from(value).map_err(|_| Kind::InvalidSignature.into())
PublicKey::Ed25519(pk) => { | ||
match ed25519_dalek::Signature::try_from(signature.as_bytes()) { | ||
Ok(sig) => pk.verify(msg, &sig).map_err(|_| { | ||
Error::signature_invalid( | ||
"Ed25519 signature verification failed".to_string(), | ||
) | ||
}), | ||
Err(e) => Err(Error::signature_invalid(format!( | ||
"invalid Ed25519 signature: {}", | ||
e | ||
))), | ||
} | ||
} | ||
#[cfg(feature = "secp256k1")] | ||
PublicKey::Secp256k1(_) => Err(Error::invalid_key( | ||
"unsupported signature algorithm (ECDSA/secp256k1)".to_string(), | ||
)), | ||
PublicKey::Secp256k1(pk) => { | ||
match k256::ecdsa::Signature::try_from(signature.as_bytes()) { | ||
Ok(sig) => pk.verify(msg, &sig).map_err(|_| { | ||
Error::signature_invalid( | ||
"Secp256k1 signature verification failed".to_string(), | ||
) | ||
}), | ||
Err(e) => Err(Error::signature_invalid(format!( | ||
"invalid Secp256k1 signature: {}", | ||
e | ||
))), | ||
} | ||
} |
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.
It might be good to add some test vectors for both of these, just to ensure that a valid signature verifies and an invalid signature is rejected.
I can get some if you'd like.
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.
Here are the RFC 8032 test vectors for Ed25519:
https://datatracker.ietf.org/doc/html/rfc8032#section-7.1
Here is an ECDSA/secp256k1 test vector:
There are quite a few ECDSA/secp256k1 test vectors in the Wycheproof test suite if you want 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.
I've tried adding a few test vectors from RFC 8032 and the Wycheproof test suite in #964.
Codecov Report
@@ Coverage Diff @@
## master #962 +/- ##
======================================
Coverage 72.0% 72.1%
======================================
Files 203 203
Lines 16522 16589 +67
======================================
+ Hits 11912 11969 +57
- Misses 4610 4620 +10
Continue to review full report at Codecov.
|
* outside the scope of this enum. */ | ||
} | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub struct Signature(Vec<u8>); | ||
|
||
impl Protobuf<Vec<u8>> for Signature {} | ||
|
||
impl TryFrom<Vec<u8>> for Signature { |
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 somewhat orthogonal to the changes in this PR, but I'm curious why this is TryFrom<Vec<u8>>
instead of TryFrom<&[u8]>
. The latter is a lot more flexible since you can use it with anything which can be borrowed as a byte slice (e.g. Bytes
)
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 TryFrom<Vec<u8>>
instance is required by the Protobuf<Vec<u8>>
instance, but I agree that we should add a TryFrom<&[u8]>
instance as well.
pub trait Protobuf<T: Message + From<Self> + Default>
where
Self: Sized + Clone + TryFrom<T>, // <---------
<Self as TryFrom<T>>::Error: Display,
@tony-iqlusion since @romac is on vacation at the moment, I'll make updates to this PR in #964. Once we're happy with #964, we can merge into this PR and subsequently into |
* Add fallible constructor for Signature to facilitate TryFrom<&[u8]> Signed-off-by: Thane Thomson <connect@thanethomson.com> * Add Ed25519 test vectors from RFC Signed-off-by: Thane Thomson <connect@thanethomson.com> * Add test vectors for secp256k1 signature validation Signed-off-by: Thane Thomson <connect@thanethomson.com>
Close: #939
Signature
enum into a wrapper over a byte array, so that it can hold signatures of any type. As theSignature
is only ever used/useful in conjunction with aPublicKey
, the latter is enough to determine which type of signature it holds.Option<Signature>
instead ofSignature::None
in various places.Signature
constructor.changelog/