From b3274c98d5a7bb19cdc9cfd3f09366e2facaf6e6 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Mon, 27 Apr 2026 01:18:32 -0700 Subject: [PATCH] Implement Hash on non-secret Signature and VerifyingKey types Per #1229: Signature and public-key types lack `Hash`, which prevents use in `HashMap` / `HashSet` keys and similar collections. - ecdsa: Signature, DER Signature, SignatureWithOid - ed25519: Signature, pkcs8::PublicKeyBytes - ed448: Signature, pkcs8::PublicKeyBytes - ml-dsa: Signature, VerifyingKey - slh-dsa: Signature, VerifyingKey Where a trivial derive works (ed25519, ed448), uses derive. Where generic bounds or upstream gaps require it, uses a manual impl over the canonical serialized bytes (`to_bytes` / `encode` / `as_bytes`), which is the natural Hash domain for these types anyway. Followups (separate PRs, blocked on upstream): - ecdsa VerifyingKey (needs PublicKey: Hash in elliptic-curve) - dsa types (need BoxedUint: Hash in crypto-bigint) - lms types (need Mode-generic trait bound threading) Closes #1229 --- ecdsa/src/der.rs | 11 +++++++++++ ecdsa/src/lib.rs | 22 ++++++++++++++++++++++ ed25519/src/lib.rs | 2 +- ed25519/src/pkcs8.rs | 2 +- ed448/src/lib.rs | 2 +- ed448/src/pkcs8.rs | 2 +- ml-dsa/src/lib.rs | 12 ++++++++++++ slh-dsa/src/signature_encoding.rs | 6 ++++++ slh-dsa/src/verifying_key.rs | 6 ++++++ 9 files changed, 61 insertions(+), 4 deletions(-) diff --git a/ecdsa/src/der.rs b/ecdsa/src/der.rs index 3280b393..3801a813 100644 --- a/ecdsa/src/der.rs +++ b/ecdsa/src/der.rs @@ -151,6 +151,17 @@ where } } +impl core::hash::Hash for Signature +where + C: EcdsaCurve, + MaxSize: ArraySize, + as Add>::Output: Add + ArraySize, +{ + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + impl AsRef<[u8]> for Signature where C: EcdsaCurve, diff --git a/ecdsa/src/lib.rs b/ecdsa/src/lib.rs index 2d0c024f..5d63f71f 100644 --- a/ecdsa/src/lib.rs +++ b/ecdsa/src/lib.rs @@ -398,6 +398,16 @@ where } } +impl core::hash::Hash for Signature +where + C: EcdsaCurve, + SignatureSize: ArraySize, +{ + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + impl fmt::LowerHex for Signature where C: EcdsaCurve, @@ -667,6 +677,18 @@ where { } +#[cfg(feature = "digest")] +impl core::hash::Hash for SignatureWithOid +where + C: EcdsaCurve, + SignatureSize: ArraySize, +{ + fn hash(&self, state: &mut H) { + self.signature.hash(state); + self.oid.hash(state); + } +} + #[cfg(feature = "digest")] impl From> for Signature where diff --git a/ed25519/src/lib.rs b/ed25519/src/lib.rs index 897ddcd0..3be7ff13 100644 --- a/ed25519/src/lib.rs +++ b/ed25519/src/lib.rs @@ -316,7 +316,7 @@ pub type SignatureBytes = [u8; Signature::BYTE_SIZE]; /// /// Signature verification libraries are expected to reject invalid field /// elements at the time a signature is verified. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, Hash, PartialEq)] #[cfg_attr( feature = "zerocopy", derive(FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned) diff --git a/ed25519/src/pkcs8.rs b/ed25519/src/pkcs8.rs index 0ef257a0..48d9461b 100644 --- a/ed25519/src/pkcs8.rs +++ b/ed25519/src/pkcs8.rs @@ -229,7 +229,7 @@ impl str::FromStr for KeypairBytes { /// /// Note that this type operates on raw bytes and performs no validation that /// public keys represent valid compressed Ed25519 y-coordinates. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, Hash, PartialEq)] #[cfg_attr( feature = "zerocopy", derive(IntoBytes, FromBytes, Unaligned, KnownLayout, Immutable,) diff --git a/ed448/src/lib.rs b/ed448/src/lib.rs index 0cf6488d..3f26546b 100644 --- a/ed448/src/lib.rs +++ b/ed448/src/lib.rs @@ -115,7 +115,7 @@ pub type SignatureBytes = [u8; Signature::BYTE_SIZE]; /// /// Signature verification libraries are expected to reject invalid field /// elements at the time a signature is verified. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, Hash, PartialEq)] #[repr(C)] pub struct Signature { R: ComponentBytes, diff --git a/ed448/src/pkcs8.rs b/ed448/src/pkcs8.rs index 6254fa95..c5443be5 100644 --- a/ed448/src/pkcs8.rs +++ b/ed448/src/pkcs8.rs @@ -205,7 +205,7 @@ impl fmt::Debug for KeypairBytes { /// /// Note that this type operates on raw bytes and performs no validation that /// public keys represent valid compressed Ed448 y-coordinates. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct PublicKeyBytes(pub [u8; Self::BYTE_SIZE]); impl PublicKeyBytes { diff --git a/ml-dsa/src/lib.rs b/ml-dsa/src/lib.rs index 16a11c8a..7bc4bf70 100644 --- a/ml-dsa/src/lib.rs +++ b/ml-dsa/src/lib.rs @@ -151,6 +151,12 @@ impl signature::SignatureEncoding for Signature

{ type Repr = EncodedSignature

; } +impl core::hash::Hash for Signature

{ + fn hash(&self, state: &mut H) { + self.encode().hash(state); + } +} + struct MuBuilder(H); impl MuBuilder { @@ -834,6 +840,12 @@ impl VerifyingKey

{ } } +impl core::hash::Hash for VerifyingKey

{ + fn hash(&self, state: &mut H) { + self.encode().hash(state); + } +} + impl signature::Verifier> for VerifyingKey

{ fn verify(&self, msg: &[u8], signature: &Signature

) -> Result<(), Error> { self.multipart_verify(&[msg], signature) diff --git a/slh-dsa/src/signature_encoding.rs b/slh-dsa/src/signature_encoding.rs index 314bef1f..d8c44fee 100644 --- a/slh-dsa/src/signature_encoding.rs +++ b/slh-dsa/src/signature_encoding.rs @@ -71,6 +71,12 @@ impl Signature

{ } } +impl core::hash::Hash for Signature

{ + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + impl TryFrom<&[u8]> for Signature

{ type Error = Error; diff --git a/slh-dsa/src/verifying_key.rs b/slh-dsa/src/verifying_key.rs index 8bfbc7ca..30c7d5af 100644 --- a/slh-dsa/src/verifying_key.rs +++ b/slh-dsa/src/verifying_key.rs @@ -136,6 +136,12 @@ impl VerifyingKey

{ } } +impl core::hash::Hash for VerifyingKey

{ + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + impl Clone for VerifyingKey

{ fn clone(&self) -> Self { VerifyingKey {