How to unify RustCrypto signature curves with generics?

I'm looking to a build a structure that stores/serializes/deserializes an ECDSA signature as part of a larger structure. This structure can use NIST P-256, NIST P-384, or secp256k1 as part of user input. This is what I'm trying to do (simplified):

#![allow(dead_code, unused_imports)]

use ecdsa::{signature::Signer, SigningKey};
use elliptic_curve::{PublicKey, SecretKey};
use k256::Secp256k1;
use p256::NistP256;
use p384::NistP384;
use rand_core::OsRng;

#[derive(Debug)]
pub struct Signature<T> {
    signature: ecdsa::Signature<T>,
}

impl<T> Signature<T> {
    pub fn sign(secret_key: SecretKey<T>, message: &[u8]) -> Signature<T> {
        let signature = SigningKey::from(&secret_key).sign(&message);

        Signature { signature }
    }
}

fn main() {
    let key: SecretKey<NistP256> = SecretKey::random(&mut OsRng);

    let sig: Signature<NistP256> = Signature::sign(key, "foo".as_bytes());

    dbg!(sig);
}

My intent here is to pass in the appropriate Curve as a Generic.. and it seems like it should work and I /think/ I'm actually doing this mostly right, but I keep getting the errors:

error[E0277]: the trait bound `T: elliptic_curve::Curve` is not satisfied
  --> src/main.rs:16:29
   |
16 |     pub fn sign(secret_key: SecretKey<T>, message: &[u8]) -> Signature<T> {
   |                             ^^^^^^^^^^^^ the trait `elliptic_curve::Curve` is not implemented for `T`
   |
note: required by a bound in `SecretKey`
  --> /home/xxx/.cargo/registry/src/github.com-1ecc6299db9ec823/elliptic-curve-0.12.3/src/secret_key.rs:84:25
   |
84 | pub struct SecretKey<C: Curve> {
   |                         ^^^^^ required by this bound in `SecretKey`
help: consider restricting type parameter `T`
   |
15 | impl<T: elliptic_curve::Curve> Signature<T> {
   |       +++++++++++++++++++++++

error[E0277]: the trait bound `T: ecdsa::PrimeCurve` is not satisfied
   --> src/main.rs:12:16
    |
12  |     signature: ecdsa::Signature<T>,
    |                ^^^^^^^^^^^^^^^^^^^ the trait `ecdsa::PrimeCurve` is not implemented for `T`
    |
note: required by a bound in `ecdsa::Signature`
   --> /home/xxx/.cargo/registry/src/github.com-1ecc6299db9ec823/ecdsa-0.14.4/src/lib.rs:132:25
    |
132 | pub struct Signature<C: PrimeCurve>
    |                         ^^^^^^^^^^ required by this bound in `ecdsa::Signature`
help: consider restricting type parameter `T`
    |
11  | pub struct Signature<T: ecdsa::PrimeCurve> {
    |                       +++++++++++++++++++

The errors thrown complain about traits which the curve NistP256 does, in fact, implement.

I can do this just fine:

#![allow(dead_code, unused_imports)]

use ecdsa::{signature::Signer, SigningKey};
use elliptic_curve::{PublicKey, SecretKey};
use k256::Secp256k1;
use p256::NistP256;
use p384::NistP384;
use rand_core::OsRng;

#[derive(Debug)]
pub struct Signature {
    signature: ecdsa::Signature<NistP256>,
}

impl Signature {
    pub fn sign(secret_key: SecretKey<NistP256>, message: &[u8]) -> Self {
        let signature = SigningKey::from(&secret_key).sign(&message);

        Self { signature }
    }
}

fn main() {
    let key: SecretKey<NistP256> = SecretKey::random(&mut OsRng);

    let sig: Signature = Signature::sign(key, "foo".as_bytes());

    dbg!(sig);
}

I don't want to have to create structs and impls for all the curves I intend to support and then figure out how to unify them into the final structure.

So either I'm getting something fundamentally wrong with generics conceptually or the implementation of the RustCrypto signatures crates are weird enough that they can't be generic'd.

Where am I doing this wrong?

Generic implementations must make sense for all types that meet the bounds -- and this is checked at the implementation, not when specific types are instantiated against the implementation (in contrast with C++ templates, say). So you can't just impl<T> Signature<T> and write something that only works when T implements Curve, even if you only instantiate Signature<NistP256> in your own code. The implementation header says it should be valid for every T, so the implementation body must satisfy that.

So you need impl<T: Curve> instead.

In this case, it seems there's a bound on ecdsa::Signature itself, which means you'll additionally need to add the T: Curve bound to your own Signature<T> type, and mention that bound pretty much everywhere. (The necessity to mention the bound everywhere is why it's more common to not put bounds on data structures and only mention them when needed for methods etc., but that decision is out of your control in this case.)

okay.. then am I correct in saying that both every mentioned generic parameter needs to be accounted for directly /and/ that the the RustCrypto implementation here breaks a sort-of-but-not-strongly-enforced convention?

I'm not sure what you mean by "accounted for directly". From your post it looks like you'll have to put that bound a lot of places.

And yes, there's a sort-of-but-not-strongly-enforced convention around not putting bounds where they're not strongly required. You can create a HashSet::<f32> and check it's capacity and whatnot, for example, even though you can't do anything useful like insert into it.

The convention may change if implied bounds someday removes the need to repeat the bound so many places.

I took a closer look and RustCrypto does strongly require the bound at the struct level as ultimately a SecretKey<C> is a <C as Curve>::Uint. So there's no getting around it in this case.

To your point, the RustCrypto team definitely threw a hell of a lot of bounds around. To get what I wanted I had to dig back thru the upstream code, track down, and satisfy every damn generic I could find. This is what I eventually came up with (and it works, but I haven't tried integrating it yet into the non-simplified form):

#![allow(dead_code, unused_imports)]

use ecdsa::{
    hazmat::{DigestPrimitive, SignPrimitive},
    signature::{DigestSigner, Signer},
    PrimeCurve, SignatureSize, SigningKey,
};
use elliptic_curve::{
    generic_array::ArrayLength, Curve, ProjectiveArithmetic, ScalarArithmetic, SecretKey,
};
use k256::Secp256k1;
use p256::NistP256;
use p384::NistP384;
use rand_core::OsRng;

#[derive(Debug)]
pub struct Signature<T: Curve + PrimeCurve>
where
    SignatureSize<T>: ArrayLength<u8>,
{
    signature: ecdsa::Signature<T>,
}

impl<T> Signature<T>
where
    T: PrimeCurve,
    SignatureSize<T>: ArrayLength<u8>,
{
    pub fn sign(secret_key: SecretKey<T>, message: &[u8]) -> Signature<T>
    where
        T: PrimeCurve + ProjectiveArithmetic + DigestPrimitive,
        <T as ScalarArithmetic>::Scalar: SignPrimitive<T>,
        SigningKey<T>: DigestSigner<<T as DigestPrimitive>::Digest, ecdsa::Signature<T>>,
    {
        let signing_key = SigningKey::from(&secret_key);
        let signature = signing_key.sign(&message);

        Self { signature }
    }
}

fn main() {
    let key: SecretKey<Secp256k1> = SecretKey::random(&mut OsRng);

    let sig: Signature<Secp256k1> = Signature::sign(key, "foo".as_bytes());

    dbg!(sig);
}

tho as an aside, nowhere did I find

SigningKey<T>: DigestSigner<<T as DigestPrimitive>::Digest, ecdsa::Signature<T>>

in the upstream impl's but that's what the compiler said it wanted so I did as it suggested.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.