Skip to main content

arcana/ecc/
curves.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! High-level curve API: [`Curve`] trait + per-curve unit structs.
5//!
6//! This module exposes the user-facing dispatch layer shared by ECDSA
7//! and ECDH over short Weierstrass curves. A curve key pair is
8//! primitive-agnostic -- a [`SecretKey`] is just a scalar, a
9//! [`PublicKey`] is just a point -- so the same unit struct
10//! (`P256`, `P384`, ...) dispatches ECDSA sign/verify **and** ECDH
11//! derive to the underlying LIMBS-generic implementations in
12//! [`super::ecdsa`].
13//!
14//! Supported curves:
15//!
16//! | Wrapper             | Curve              | Canonical hash (ECDSA) |
17//! |---------------------|--------------------|------------------------|
18//! | [`P256`]            | NIST P-256         | SHA-256                |
19//! | [`P384`]            | NIST P-384         | SHA-384                |
20//! | [`P521`]            | NIST P-521         | SHA-512                |
21//! | [`Secp256k1`]       | secp256k1 (SECG)   | SHA-256                |
22//! | [`BrainpoolP256r1`] | brainpoolP256r1    | SHA-256                |
23//! | [`BrainpoolP384r1`] | brainpoolP384r1    | SHA-384                |
24//! | [`BrainpoolP512r1`] | brainpoolP512r1    | SHA-512                |
25
26use super::curve::*;
27use super::ecdsa::{
28    Signature, compress_pubkey, decompress_pubkey, ecdh_internal, keygen_internal, sign_random_internal,
29    sign_rfc6979_internal, verify_internal,
30};
31use crate::Hasher;
32
33// ============================================================================
34// Shared ECDSA / ECDH key types
35// ============================================================================
36
37/// A simple RNG trait for randomized nonce generation and key
38/// generation.
39///
40/// Implementors fill the supplied buffer with cryptographically
41/// strong pseudo-random bytes. The trait is deliberately minimal
42/// (no `fn try_fill_bytes` returning `Result`, no error type) to
43/// keep the public surface small. Callers running on platforms
44/// where the underlying entropy source can fail should provide a
45/// wrapper that panics or aborts on failure.
46pub trait CryptoRng {
47    /// Fill `dest` with cryptographically random bytes.
48    fn fill_bytes(&mut self, dest: &mut [u8]);
49}
50
51/// Public key on a short Weierstrass curve: an SEC1-encoded point
52/// (uncompressed `0x04 || X || Y` or compressed `0x02/0x03 || X`).
53///
54/// Shared between ECDSA and ECDH on the same curve: there is no
55/// "ECDH-only" or "ECDSA-only" key type.
56#[derive(Clone, Debug)]
57pub struct PublicKey {
58    /// SEC1-encoded curve point.
59    pub bytes: Vec<u8>,
60}
61
62/// Secret key on a short Weierstrass curve: a scalar in `[1, n-1]`
63/// encoded as `felem_bytes` big-endian octets (per the curve's SEC1
64/// octet length).
65///
66/// Shared between ECDSA and ECDH on the same curve.
67#[derive(Clone)]
68pub struct SecretKey {
69    /// Big-endian encoding of the secret scalar `d`.
70    pub bytes: Vec<u8>,
71}
72
73// ============================================================================
74// Curve trait
75// ============================================================================
76//
77// Public API. Two axes are exposed orthogonally:
78//
79//   * The curve  -- one unit struct per curve, implementing `Curve`.
80//   * The hash   -- a generic type parameter on the methods that need it
81//                   (RFC 6979 internal HMAC; the message-input convenience
82//                   wrappers that hash before signing).
83//
84// `sign_random` and `verify` do **not** carry an `H` parameter: they
85// consume the digest as opaque bytes (interpreted via `bits2int`) and
86// have no algebraic dependency on which hash produced it. The asymmetry
87// reflects the actual algorithm.
88//
89// Naming convention: the **digest-input** form is the canonical, short
90// name (this is the form that maps directly to OpenSSL `ECDSA_sign`,
91// PKCS#11, HSM offload paths, X.509/CMS pipelines, ...). The **message
92// input** convenience form has a `_msg` suffix.
93//
94// Per-curve unit structs (`P256`, `P384`, ...) carry no data; they exist
95// only to dispatch the trait methods to the LIMBS-generic internals with
96// the right `params()` and the right `LIMBS` const.
97
98/// Operations on a short Weierstrass curve: keygen, ECDSA sign / verify,
99/// and ECDH key agreement.
100///
101/// Implementations are unit structs ([`P256`], [`P384`], [`P521`],
102/// [`Secp256k1`], [`BrainpoolP256r1`], [`BrainpoolP384r1`],
103/// [`BrainpoolP512r1`]) and the methods are called as e.g.
104/// `P256::sign_rfc6979::<Sha256>(&sk, &digest)` or
105/// `P256::ecdh(&sk, &peer_pk)`.
106///
107/// # Method overview
108///
109/// | Method                                       | Input         | `H` needed?                |
110/// |----------------------------------------------|---------------|----------------------------|
111/// | [`keygen`](Self::keygen)                     | --            | no                         |
112/// | [`ecdh`](Self::ecdh)                         | peer SEC1 pk  | no                         |
113/// | [`sign_rfc6979`](Self::sign_rfc6979)         | digest        | yes (HMAC inside RFC 6979) |
114/// | [`sign_random`](Self::sign_random)           | digest        | no                         |
115/// | [`verify`](Self::verify)                     | digest        | no                         |
116/// | [`sign_rfc6979_msg`](Self::sign_rfc6979_msg) | message       | yes (hash + HMAC)          |
117/// | [`sign_random_msg`](Self::sign_random_msg)   | message       | yes (hash)                 |
118/// | [`verify_msg`](Self::verify_msg)             | message       | yes (hash)                 |
119///
120/// Key pairs returned by [`keygen`](Self::keygen) are interchangeable
121/// across ECDSA and ECDH on the same curve -- a [`SecretKey`] is just a
122/// scalar, a [`PublicKey`] is just a point. There is no "ECDH-only" or
123/// "ECDSA-only" key type.
124///
125/// # Hash choice (ECDSA)
126///
127/// Any [`Hasher`] implementation may be paired with any curve.
128/// The standard pairings (P-256+SHA-256, P-384+SHA-384, brainpoolP512r1
129/// +SHA-512, etc.) are common, but **not** mandated by this crate. If you
130/// need P-256 + SHA-512 for an exotic protocol, that works too -- the
131/// `bits2int` step will truncate the digest to the curve's qlen as
132/// specified in RFC 6979 §2.3.2.
133///
134/// Per FIPS 186-5 §6.4.2, the hash output should provide at least the
135/// security level of the curve (e.g. don't pair P-384 with SHA-1). The
136/// crate does not enforce this; document and test your protocol's choice.
137pub trait Curve: Sized {
138    /// Generate a key pair on this curve.
139    fn keygen(rng: &mut dyn CryptoRng) -> (PublicKey, SecretKey);
140
141    /// Compress a public key from SEC1 uncompressed (`0x04 || X || Y`)
142    /// to SEC1 compressed (`0x02/0x03 || X`). If the input is already
143    /// compressed, returns a validated clone. Returns `None` for
144    /// malformed or off-curve input.
145    fn compress_pubkey(pk: &PublicKey) -> Option<Vec<u8>>;
146
147    /// Decompress a SEC1 compressed public key (`0x02/0x03 || X`) to
148    /// uncompressed form (`0x04 || X || Y`), recovering Y via the
149    /// field square-root. If the input is already uncompressed, acts
150    /// as a validate-and-clone. Returns `None` if the input is
151    /// malformed, if X is not a valid x-coordinate on the curve, or
152    /// if the decompressed point fails the on-curve check.
153    fn decompress_pubkey(compressed: &[u8]) -> Option<PublicKey>;
154
155    /// **ECDH** key agreement: derive the shared secret from our secret
156    /// key and the peer's SEC1 uncompressed public key.
157    ///
158    /// Returns the **raw X coordinate** of `sk * peer_pk` as `LIMBS * 8`
159    /// big-endian bytes (matches NIST SP 800-56A §5.7.1.2 "ECC CDH
160    /// Primitive" and TLS / IKE conventions).
161    ///
162    /// Returns `None` if any of the following holds:
163    /// - peer's pk has the wrong SEC1 length / tag
164    /// - peer's pk is not on the curve (defends against invalid-curve
165    ///   attacks; see [`super::curve::is_on_curve`])
166    /// - our secret scalar is not in `[1, n-1]`
167    /// - the resulting shared point is the point at infinity
168    ///
169    /// Higher-level KDFs (HKDF, X9.63 KDF, ...) are out of scope for this
170    /// layer -- callers feed the raw X bytes into their KDF of choice.
171    fn ecdh(sk: &SecretKey, peer_pk: &PublicKey) -> Option<Vec<u8>>;
172
173    /// Sign a precomputed digest with the deterministic RFC 6979 nonce.
174    ///
175    /// `H` is the hash that produced `digest`; it is required because RFC
176    /// 6979 derives the nonce via HMAC-`H` internally. Two calls with the
177    /// same `(sk, digest, H)` produce byte-identical signatures.
178    fn sign_rfc6979<H: Hasher>(sk: &SecretKey, digest: &[u8]) -> Signature;
179
180    /// Sign a precomputed digest with a uniformly random nonce drawn from
181    /// `rng`. The hash function is irrelevant -- only the digest bytes are
182    /// consumed (via `bits2int`). **Each call must consume fresh entropy**;
183    /// reusing `k` across two signatures with the same key recovers the
184    /// secret key.
185    fn sign_random(sk: &SecretKey, digest: &[u8], rng: &mut dyn CryptoRng) -> Signature;
186
187    /// Verify a signature against a precomputed digest.
188    fn verify(pk: &PublicKey, digest: &[u8], sig: &Signature) -> bool;
189
190    // ----- Convenience: hash the message in-place -----
191
192    /// Convenience: hash `msg` with `H`, then call [`Self::sign_rfc6979`].
193    fn sign_rfc6979_msg<H: Hasher>(sk: &SecretKey, msg: &[u8]) -> Signature {
194        let digest = H::hash(msg);
195        Self::sign_rfc6979::<H>(sk, &digest)
196    }
197
198    /// Convenience: hash `msg` with `H`, then call [`Self::sign_random`].
199    fn sign_random_msg<H: Hasher>(sk: &SecretKey, msg: &[u8], rng: &mut dyn CryptoRng) -> Signature {
200        let digest = H::hash(msg);
201        Self::sign_random(sk, &digest, rng)
202    }
203
204    /// Convenience: hash `msg` with `H`, then call [`Self::verify`].
205    fn verify_msg<H: Hasher>(pk: &PublicKey, msg: &[u8], sig: &Signature) -> bool {
206        let digest = H::hash(msg);
207        Self::verify(pk, &digest, sig)
208    }
209}
210
211// ============================================================================
212// Per-curve dispatch
213// ============================================================================
214
215/// Generate the per-curve unit struct + `Curve` impl for one curve. The
216/// macro pins `LIMBS` and the `params_fn`; everything else flows through
217/// the LIMBS-generic internals in [`super::ecdsa`].
218macro_rules! curve_dispatch {
219    ($name:ident, $params_fn:path, $limbs:expr, $doc:literal) => {
220        #[doc = $doc]
221        pub struct $name;
222
223        impl Curve for $name {
224            fn keygen(rng: &mut dyn CryptoRng) -> (PublicKey, SecretKey) {
225                keygen_internal::<$limbs>(&$params_fn(), rng)
226            }
227
228            fn ecdh(sk: &SecretKey, peer_pk: &PublicKey) -> Option<Vec<u8>> {
229                ecdh_internal::<$limbs>(&$params_fn(), sk, peer_pk)
230            }
231
232            fn compress_pubkey(pk: &PublicKey) -> Option<Vec<u8>> {
233                compress_pubkey::<$limbs>(&$params_fn(), pk)
234            }
235
236            fn decompress_pubkey(compressed: &[u8]) -> Option<PublicKey> {
237                decompress_pubkey::<$limbs>(&$params_fn(), compressed)
238            }
239
240            fn sign_rfc6979<H: Hasher>(sk: &SecretKey, digest: &[u8]) -> Signature {
241                sign_rfc6979_internal::<H, $limbs>(&$params_fn(), sk, digest)
242            }
243
244            fn sign_random(sk: &SecretKey, digest: &[u8], rng: &mut dyn CryptoRng) -> Signature {
245                sign_random_internal::<$limbs>(&$params_fn(), sk, digest, rng)
246            }
247
248            fn verify(pk: &PublicKey, digest: &[u8], sig: &Signature) -> bool {
249                verify_internal::<$limbs>(&$params_fn(), pk, digest, sig)
250            }
251        }
252    };
253}
254
255curve_dispatch!(P256, p256_params, 4, "NIST P-256 (secp256r1).");
256curve_dispatch!(P384, p384_params, 6, "NIST P-384 (secp384r1).");
257curve_dispatch!(Secp256k1, secp256k1_params, 4, "secp256k1 (SECG / Bitcoin / Ethereum).");
258curve_dispatch!(
259    BrainpoolP256r1,
260    brainpoolp256r1_params,
261    4,
262    "brainpoolP256r1 (BSI / RFC 5639)."
263);
264curve_dispatch!(
265    BrainpoolP384r1,
266    brainpoolp384r1_params,
267    6,
268    "brainpoolP384r1 (BSI / RFC 5639)."
269);
270curve_dispatch!(
271    BrainpoolP512r1,
272    brainpoolp512r1_params,
273    8,
274    "brainpoolP512r1 (BSI / RFC 5639)."
275);
276curve_dispatch!(
277    P521,
278    secp521r1_params,
279    9,
280    "NIST P-521 (secp521r1). Uses LIMBS=9; qlen=521 is not a multiple \
281     of 8, so all RFC 6979 byte-length arithmetic uses rlen_bytes=66 \
282     (not LIMBS*8=72). The canonical hash pairing is SHA-512."
283);
284
285// ============================================================================
286// Tests
287// ============================================================================
288
289#[cfg(test)]
290mod tests {
291    use super::super::ecdsa::fe_to_felem_bytes;
292    use super::super::field::FieldElement;
293    use super::*;
294    use crate::hash::sha256::Sha256;
295    use crate::hash::sha384::Sha384;
296    use crate::hash::sha512::Sha512;
297
298    fn hex_to_bytes(hex: &str) -> Vec<u8> {
299        (0..hex.len())
300            .step_by(2)
301            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
302            .collect()
303    }
304
305    /// Build (sk, pk) for d=1 on a curve, given its params. Both `sk`
306    /// and `pk` use the SEC1 `felem_bytes` external width (same as what
307    /// `keygen_internal` emits), so for P-521 this returns a 66-byte
308    /// secret key and a 133-byte uncompressed public key, not
309    /// 72 / 145.
310    fn d1_keypair<const LIMBS: usize>(params: &CurveParams<LIMBS>) -> (SecretKey, PublicKey) {
311        let felem = params.felem_bytes;
312        let mut sk_bytes = vec![0u8; felem];
313        sk_bytes[felem - 1] = 1; // d = 1 (big-endian)
314
315        let mut pk_bytes = Vec::with_capacity(1 + 2 * felem);
316        pk_bytes.push(0x04);
317        pk_bytes.extend_from_slice(&fe_to_felem_bytes(&params.gx, felem));
318        pk_bytes.extend_from_slice(&fe_to_felem_bytes(&params.gy, felem));
319
320        (SecretKey { bytes: sk_bytes }, PublicKey { bytes: pk_bytes })
321    }
322
323    /// Tiny deterministic xorshift64 RNG, for tests only. Not cryptographic --
324    /// the goal is just to feed `sign_random` a known, reproducible byte
325    /// stream so that test failures are debuggable.
326    struct TestRng {
327        state: u64,
328    }
329
330    impl TestRng {
331        fn new(seed: u64) -> Self {
332            // xorshift state must be non-zero.
333            Self {
334                state: if seed == 0 { 0xdeadbeefcafef00d } else { seed },
335            }
336        }
337    }
338
339    impl CryptoRng for TestRng {
340        fn fill_bytes(&mut self, dest: &mut [u8]) {
341            for chunk in dest.chunks_mut(8) {
342                let mut x = self.state;
343                x ^= x << 13;
344                x ^= x >> 7;
345                x ^= x << 17;
346                self.state = x;
347                for (i, b) in chunk.iter_mut().enumerate() {
348                    *b = (x >> (8 * i)) as u8;
349                }
350            }
351        }
352    }
353
354    // ----------------------------------------------------------------------
355    // ECDSA sign/verify -- P-256 canonical roundtrip
356    // ----------------------------------------------------------------------
357
358    #[test]
359    fn test_ecdsa_p256_sign_verify_rfc6979() {
360        let sk_bytes = hex_to_bytes("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721");
361        let sk = SecretKey {
362            bytes: sk_bytes.clone(),
363        };
364
365        // Compute public key from secret key.
366        let params = p256_params();
367        let g = JacobianPoint::from_affine(params.gx, params.gy);
368        let d = FieldElement::<4>::from_bytes_be(&sk_bytes);
369        let q = scalar_mul_point(&d, &g, &params);
370        let (qx, qy) = q.to_affine(&params.p).unwrap();
371        let mut pk_bytes = vec![0x04];
372        pk_bytes.extend_from_slice(&qx.to_bytes_be());
373        pk_bytes.extend_from_slice(&qy.to_bytes_be());
374        let pk = PublicKey { bytes: pk_bytes };
375
376        let msg = b"sample";
377        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
378
379        let expected_r = hex_to_bytes("EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716");
380        let expected_s = hex_to_bytes("F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8");
381
382        assert_eq!(sig.r, expected_r, "Signature r mismatch");
383        assert_eq!(sig.s, expected_s, "Signature s mismatch");
384
385        // Verify the signature.
386        assert!(
387            P256::verify_msg::<Sha256>(&pk, msg, &sig),
388            "Signature verification failed"
389        );
390    }
391
392    #[test]
393    fn test_ecdsa_p256_verify_rejects_bad_sig() {
394        let sk_bytes = hex_to_bytes("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721");
395        let sk = SecretKey {
396            bytes: sk_bytes.clone(),
397        };
398
399        let params = p256_params();
400        let g = JacobianPoint::from_affine(params.gx, params.gy);
401        let d = FieldElement::<4>::from_bytes_be(&sk_bytes);
402        let q = scalar_mul_point(&d, &g, &params);
403        let (qx, qy) = q.to_affine(&params.p).unwrap();
404        let mut pk_bytes = vec![0x04];
405        pk_bytes.extend_from_slice(&qx.to_bytes_be());
406        pk_bytes.extend_from_slice(&qy.to_bytes_be());
407        let pk = PublicKey { bytes: pk_bytes };
408
409        let msg = b"sample";
410        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
411
412        // Modify message.
413        assert!(!P256::verify(&pk, b"tampered", &sig), "Should reject tampered message");
414
415        // Modify signature.
416        let mut bad_sig = sig.clone();
417        bad_sig.r[0] ^= 0x01;
418        assert!(!P256::verify(&pk, msg, &bad_sig), "Should reject modified signature");
419    }
420
421    #[test]
422    fn test_ecdsa_p256_sign_verify_roundtrip() {
423        let sk_bytes = hex_to_bytes("0000000000000000000000000000000000000000000000000000000000000001");
424        let sk = SecretKey {
425            bytes: sk_bytes.clone(),
426        };
427
428        // Public key for d=1 is just G.
429        let params = p256_params();
430        let mut pk_bytes = vec![0x04];
431        pk_bytes.extend_from_slice(&params.gx.to_bytes_be());
432        pk_bytes.extend_from_slice(&params.gy.to_bytes_be());
433        let pk = PublicKey { bytes: pk_bytes };
434
435        let msg = b"test message for ECDSA P-256";
436        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
437        assert!(
438            P256::verify_msg::<Sha256>(&pk, msg, &sig),
439            "Roundtrip verification failed"
440        );
441    }
442
443    // ----------------------------------------------------------------------
444    // Roundtrip tests for the new curves.
445    //
446    // Strategy: use d = 1 so the public key is exactly the generator G --
447    // avoids re-doing scalar multiplication in the test setup. Sign with
448    // RFC 6979 (deterministic, no rng needed), then verify.
449    // ----------------------------------------------------------------------
450
451    #[test]
452    fn test_ecdsa_secp256k1_sign_verify_roundtrip() {
453        let (sk, pk) = d1_keypair(&secp256k1_params());
454        let msg = b"hello secp256k1";
455        let sig = Secp256k1::sign_rfc6979_msg::<Sha256>(&sk, msg);
456        assert!(Secp256k1::verify_msg::<Sha256>(&pk, msg, &sig));
457        assert!(!Secp256k1::verify(&pk, b"tampered", &sig));
458        let mut bad = sig.clone();
459        bad.s[0] ^= 0x01;
460        assert!(!Secp256k1::verify(&pk, msg, &bad));
461    }
462
463    #[test]
464    fn test_ecdsa_brainpoolp256r1_sign_verify_roundtrip() {
465        let (sk, pk) = d1_keypair(&brainpoolp256r1_params());
466        let msg = b"hello brainpoolP256r1";
467        let sig = BrainpoolP256r1::sign_rfc6979_msg::<Sha256>(&sk, msg);
468        assert!(BrainpoolP256r1::verify_msg::<Sha256>(&pk, msg, &sig));
469    }
470
471    #[test]
472    fn test_ecdsa_brainpoolp384r1_sign_verify_roundtrip() {
473        let (sk, pk) = d1_keypair(&brainpoolp384r1_params());
474        let msg = b"hello brainpoolP384r1";
475        let sig = BrainpoolP384r1::sign_rfc6979_msg::<Sha384>(&sk, msg);
476        assert!(BrainpoolP384r1::verify_msg::<Sha384>(&pk, msg, &sig));
477    }
478
479    #[test]
480    fn test_ecdsa_brainpoolp512r1_sign_verify_roundtrip() {
481        let (sk, pk) = d1_keypair(&brainpoolp512r1_params());
482        let msg = b"hello brainpoolP512r1";
483        let sig = BrainpoolP512r1::sign_rfc6979_msg::<Sha512>(&sk, msg);
484        assert!(BrainpoolP512r1::verify_msg::<Sha512>(&pk, msg, &sig));
485    }
486
487    // ----------------------------------------------------------------------
488    // sign_random tests: exercise the random-nonce path.
489    // ----------------------------------------------------------------------
490
491    #[test]
492    fn test_ecdsa_p256_sign_random_roundtrip() {
493        let (sk, pk) = d1_keypair(&p256_params());
494        let mut rng = TestRng::new(1);
495        let msg = b"random nonce P-256";
496        let sig = P256::sign_random_msg::<Sha256>(&sk, msg, &mut rng);
497        assert!(P256::verify_msg::<Sha256>(&pk, msg, &sig));
498    }
499
500    #[test]
501    fn test_ecdsa_p384_sign_random_roundtrip() {
502        let (sk, pk) = d1_keypair(&p384_params());
503        let mut rng = TestRng::new(11);
504        let msg = b"random nonce P-384";
505        let sig = P384::sign_random_msg::<Sha384>(&sk, msg, &mut rng);
506        assert!(P384::verify_msg::<Sha384>(&pk, msg, &sig));
507    }
508
509    /// Diagnostic: sweep many seeds and confirm each random signature
510    /// verifies.
511    #[test]
512    fn test_p384_random_seed_sweep() {
513        let (sk, pk) = d1_keypair(&p384_params());
514        let msg = b"sweep";
515        for seed in 1u64..=20 {
516            let mut rng = TestRng::new(seed);
517            let sig = P384::sign_random_msg::<Sha384>(&sk, msg, &mut rng);
518            assert!(
519                P384::verify_msg::<Sha384>(&pk, msg, &sig),
520                "verify failed for P-384 seed {}",
521                seed,
522            );
523        }
524    }
525
526    #[test]
527    fn test_ecdsa_p384_sign_rfc6979_roundtrip() {
528        let (sk, pk) = d1_keypair(&p384_params());
529        let msg = b"P-384 RFC 6979 baseline";
530        let sig = P384::sign_rfc6979_msg::<Sha384>(&sk, msg);
531        assert!(P384::verify_msg::<Sha384>(&pk, msg, &sig));
532    }
533
534    #[test]
535    fn test_ecdsa_secp256k1_sign_random_roundtrip() {
536        let (sk, pk) = d1_keypair(&secp256k1_params());
537        let mut rng = TestRng::new(3);
538        let msg = b"random nonce secp256k1";
539        let sig = Secp256k1::sign_random_msg::<Sha256>(&sk, msg, &mut rng);
540        assert!(Secp256k1::verify_msg::<Sha256>(&pk, msg, &sig));
541    }
542
543    #[test]
544    fn test_ecdsa_brainpoolp256r1_sign_random_roundtrip() {
545        let (sk, pk) = d1_keypair(&brainpoolp256r1_params());
546        let mut rng = TestRng::new(4);
547        let msg = b"random nonce brainpoolP256r1";
548        let sig = BrainpoolP256r1::sign_random_msg::<Sha256>(&sk, msg, &mut rng);
549        assert!(BrainpoolP256r1::verify_msg::<Sha256>(&pk, msg, &sig));
550    }
551
552    #[test]
553    fn test_ecdsa_brainpoolp384r1_sign_random_roundtrip() {
554        let (sk, pk) = d1_keypair(&brainpoolp384r1_params());
555        let mut rng = TestRng::new(5);
556        let msg = b"random nonce brainpoolP384r1";
557        let sig = BrainpoolP384r1::sign_random_msg::<Sha384>(&sk, msg, &mut rng);
558        assert!(BrainpoolP384r1::verify_msg::<Sha384>(&pk, msg, &sig));
559    }
560
561    #[test]
562    fn test_ecdsa_brainpoolp512r1_sign_random_roundtrip() {
563        let (sk, pk) = d1_keypair(&brainpoolp512r1_params());
564        let mut rng = TestRng::new(6);
565        let msg = b"random nonce brainpoolP512r1";
566        let sig = BrainpoolP512r1::sign_random_msg::<Sha512>(&sk, msg, &mut rng);
567        assert!(BrainpoolP512r1::verify_msg::<Sha512>(&pk, msg, &sig));
568    }
569
570    /// Two `sign_random` calls on the same `(sk, msg)` with different rng
571    /// streams must produce different signatures (different `r`, since the
572    /// nonce differs). Both must still verify.
573    #[test]
574    fn test_sign_random_is_nondeterministic() {
575        let (sk, pk) = d1_keypair(&p256_params());
576        let msg = b"same message, different rng";
577
578        let mut rng1 = TestRng::new(0xAA11);
579        let mut rng2 = TestRng::new(0xBB22);
580        let sig1 = P256::sign_random_msg::<Sha256>(&sk, msg, &mut rng1);
581        let sig2 = P256::sign_random_msg::<Sha256>(&sk, msg, &mut rng2);
582
583        assert_ne!(sig1.r, sig2.r, "random sigs should differ on r");
584        assert!(P256::verify_msg::<Sha256>(&pk, msg, &sig1));
585        assert!(P256::verify_msg::<Sha256>(&pk, msg, &sig2));
586    }
587
588    /// Conversely, two `sign_rfc6979` calls on the same `(sk, msg)` MUST
589    /// produce identical signatures.
590    #[test]
591    fn test_sign_rfc6979_is_deterministic() {
592        let (sk, _pk) = d1_keypair(&p256_params());
593        let msg = b"same message, no rng";
594
595        let sig1 = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
596        let sig2 = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
597
598        assert_eq!(sig1.r, sig2.r);
599        assert_eq!(sig1.s, sig2.s);
600    }
601
602    /// The digest-input form and the message-input convenience form must
603    /// produce byte-identical signatures when the caller pre-hashes the
604    /// message themselves.
605    #[test]
606    fn test_digest_vs_msg_forms_agree() {
607        let (sk, _pk) = d1_keypair(&p256_params());
608        let msg = b"agreement";
609
610        let from_msg = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
611        let digest = Sha256::hash(msg);
612        let from_digest = P256::sign_rfc6979::<Sha256>(&sk, &digest);
613
614        assert_eq!(from_msg.r, from_digest.r);
615        assert_eq!(from_msg.s, from_digest.s);
616    }
617
618    // ----------------------------------------------------------------------
619    // Public-key validation tests for verify_internal
620    // ----------------------------------------------------------------------
621
622    fn fresh_p256_signed() -> (SecretKey, PublicKey, Vec<u8>, Signature) {
623        let (sk, pk) = d1_keypair(&p256_params());
624        let msg = b"verify hardening sample";
625        let digest = Sha256::hash(msg);
626        let sig = P256::sign_rfc6979::<Sha256>(&sk, &digest);
627        assert!(P256::verify(&pk, &digest, &sig));
628        (sk, pk, digest, sig)
629    }
630
631    #[test]
632    fn test_verify_rejects_wrong_length_pubkey() {
633        let (_sk, mut pk, digest, sig) = fresh_p256_signed();
634        pk.bytes.pop();
635        assert!(!P256::verify(&pk, &digest, &sig));
636    }
637
638    #[test]
639    fn test_verify_rejects_wrong_tag_pubkey() {
640        let (_sk, mut pk, digest, sig) = fresh_p256_signed();
641        pk.bytes[0] = 0x02;
642        assert!(!P256::verify(&pk, &digest, &sig));
643    }
644
645    #[test]
646    fn test_verify_rejects_off_curve_pubkey() {
647        let (_sk, mut pk, digest, sig) = fresh_p256_signed();
648        let last = pk.bytes.len() - 1;
649        pk.bytes[last] ^= 0x01;
650        assert!(!P256::verify(&pk, &digest, &sig), "off-curve pubkey must be rejected");
651    }
652
653    #[test]
654    fn test_verify_rejects_infinity_pubkey() {
655        let (_sk, _real_pk, digest, sig) = fresh_p256_signed();
656        let mut bytes = vec![0u8; 65];
657        bytes[0] = 0x04;
658        let bad_pk = PublicKey { bytes };
659        assert!(!P256::verify(&bad_pk, &digest, &sig));
660    }
661
662    #[test]
663    fn test_verify_rejects_off_curve_pubkey_brainpoolp384r1() {
664        let (sk, mut pk) = d1_keypair(&brainpoolp384r1_params());
665        let msg = b"bp384 hardening";
666        let sig = BrainpoolP384r1::sign_rfc6979_msg::<Sha384>(&sk, msg);
667        assert!(BrainpoolP384r1::verify_msg::<Sha384>(&pk, msg, &sig));
668        let last = pk.bytes.len() - 1;
669        pk.bytes[last] ^= 0x01;
670        assert!(!BrainpoolP384r1::verify_msg::<Sha384>(&pk, msg, &sig));
671    }
672
673    // ----------------------------------------------------------------------
674    // Non-canonical (curve, hash) pairings
675    // ----------------------------------------------------------------------
676
677    #[test]
678    fn test_p256_sha512_pairing_roundtrip() {
679        let (sk, pk) = d1_keypair(&p256_params());
680        let msg = b"P-256 paired with SHA-512";
681
682        let sig_det = P256::sign_rfc6979_msg::<Sha512>(&sk, msg);
683        assert!(P256::verify_msg::<Sha512>(&pk, msg, &sig_det));
684
685        let mut rng = TestRng::new(0xC001);
686        let sig_rand = P256::sign_random_msg::<Sha512>(&sk, msg, &mut rng);
687        assert!(P256::verify_msg::<Sha512>(&pk, msg, &sig_rand));
688
689        let sig_det2 = P256::sign_rfc6979_msg::<Sha512>(&sk, msg);
690        assert_eq!(sig_det.r, sig_det2.r);
691        assert_eq!(sig_det.s, sig_det2.s);
692
693        let sig_sha256 = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
694        assert_ne!(
695            sig_det.r, sig_sha256.r,
696            "P-256+SHA-512 and P-256+SHA-256 must differ on r"
697        );
698    }
699
700    #[test]
701    fn test_brainpoolp256r1_sha512_pairing_roundtrip() {
702        let (sk, pk) = d1_keypair(&brainpoolp256r1_params());
703        let msg = b"brainpoolP256r1 + SHA-512";
704        let sig = BrainpoolP256r1::sign_rfc6979_msg::<Sha512>(&sk, msg);
705        assert!(BrainpoolP256r1::verify_msg::<Sha512>(&pk, msg, &sig));
706    }
707
708    #[test]
709    fn test_secp256k1_sha512_pairing_roundtrip() {
710        let (sk, pk) = d1_keypair(&secp256k1_params());
711        let msg = b"secp256k1 + SHA-512";
712        let sig = Secp256k1::sign_rfc6979_msg::<Sha512>(&sk, msg);
713        assert!(Secp256k1::verify_msg::<Sha512>(&pk, msg, &sig));
714    }
715
716    // ----------------------------------------------------------------------
717    // P-521 (secp521r1) end-to-end
718    // ----------------------------------------------------------------------
719
720    #[test]
721    fn test_p521_sign_rfc6979_roundtrip() {
722        let (sk, pk) = d1_keypair(&secp521r1_params());
723        assert_eq!(sk.bytes.len(), 66);
724        assert_eq!(pk.bytes.len(), 1 + 2 * 66);
725
726        let msg = b"P-521 RFC 6979 baseline";
727        let sig = P521::sign_rfc6979_msg::<Sha512>(&sk, msg);
728        assert_eq!(sig.r.len(), 66);
729        assert_eq!(sig.s.len(), 66);
730        assert!(P521::verify_msg::<Sha512>(&pk, msg, &sig));
731    }
732
733    #[test]
734    fn test_p521_sign_random_roundtrip() {
735        let (sk, pk) = d1_keypair(&secp521r1_params());
736        let mut rng = TestRng::new(0x521);
737        let msg = b"P-521 random nonce";
738        let sig = P521::sign_random_msg::<Sha512>(&sk, msg, &mut rng);
739        assert!(P521::verify_msg::<Sha512>(&pk, msg, &sig));
740    }
741
742    #[test]
743    fn test_p521_rfc6979_is_deterministic() {
744        let (sk, _pk) = d1_keypair(&secp521r1_params());
745        let msg = b"determinism";
746        let sig1 = P521::sign_rfc6979_msg::<Sha512>(&sk, msg);
747        let sig2 = P521::sign_rfc6979_msg::<Sha512>(&sk, msg);
748        assert_eq!(sig1.r, sig2.r);
749        assert_eq!(sig1.s, sig2.s);
750    }
751
752    #[test]
753    fn test_p521_sha512_der_roundtrip() {
754        let (sk, pk) = d1_keypair(&secp521r1_params());
755        let msg = b"P-521 DER";
756        let sig = P521::sign_rfc6979_msg::<Sha512>(&sk, msg);
757        let der = sig.to_der();
758        let parsed = Signature::from_der(&der).expect("from_der");
759        assert!(P521::verify_msg::<Sha512>(&pk, msg, &parsed));
760    }
761
762    #[test]
763    fn test_p521_sec1_compressed_roundtrip() {
764        let (_sk, pk) = d1_keypair(&secp521r1_params());
765        assert_eq!(pk.bytes.len(), 133);
766
767        let compressed = P521::compress_pubkey(&pk).expect("compress");
768        assert_eq!(compressed.len(), 67);
769        assert!(compressed[0] == 0x02 || compressed[0] == 0x03);
770
771        let decompressed = P521::decompress_pubkey(&compressed).expect("decompress");
772        assert_eq!(decompressed.bytes.len(), 133);
773        assert_eq!(decompressed.bytes, pk.bytes);
774    }
775
776    #[test]
777    fn test_p521_ecdh_roundtrip() {
778        let mut rng = TestRng::new(0x5EC5);
779        let (pk_a, sk_a) = P521::keygen(&mut rng);
780        let (pk_b, sk_b) = P521::keygen(&mut rng);
781        assert_eq!(pk_a.bytes.len(), 133);
782        assert_eq!(sk_a.bytes.len(), 66);
783        let s_ab = P521::ecdh(&sk_a, &pk_b).expect("alice ecdh");
784        let s_ba = P521::ecdh(&sk_b, &pk_a).expect("bob ecdh");
785        assert_eq!(s_ab, s_ba);
786        assert_eq!(s_ab.len(), 66);
787    }
788
789    /// **SEC1 interop pin**: decompress the known SEC1 compressed
790    /// encoding of the P-521 generator G and verify it matches the
791    /// official (Gx, Gy) from FIPS 186-5 Appendix C.2.3 byte-for-byte.
792    #[test]
793    fn test_p521_sec1_pinned_interop() {
794        let compressed_g = hex_to_bytes(
795            "0200C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66",
796        );
797        assert_eq!(compressed_g.len(), 67);
798
799        let uncompressed_g = hex_to_bytes(
800            "04\
801             00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66\
802             011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650",
803        );
804        assert_eq!(uncompressed_g.len(), 133);
805
806        let decompressed = P521::decompress_pubkey(&compressed_g).expect("decompress");
807        assert_eq!(decompressed.bytes, uncompressed_g);
808
809        let pk = PublicKey {
810            bytes: uncompressed_g.clone(),
811        };
812        let recompressed = P521::compress_pubkey(&pk).expect("compress");
813        assert_eq!(recompressed, compressed_g);
814
815        let dummy_digest = [0u8; 64];
816        let bogus_sig = Signature {
817            r: vec![0x01],
818            s: vec![0x01],
819        };
820        let _ = P521::verify(&pk, &dummy_digest, &bogus_sig);
821    }
822
823    #[test]
824    fn test_p521_verify_rejects_tampered() {
825        let (sk, pk) = d1_keypair(&secp521r1_params());
826        let msg = b"tamper";
827        let mut sig = P521::sign_rfc6979_msg::<Sha512>(&sk, msg);
828        sig.r[0] ^= 0x01;
829        assert!(!P521::verify_msg::<Sha512>(&pk, msg, &sig));
830    }
831
832    // ----------------------------------------------------------------------
833    // SEC1 compressed public keys
834    // ----------------------------------------------------------------------
835
836    #[test]
837    fn test_sec1_compressed_roundtrip_p256() {
838        let (_sk, pk) = d1_keypair(&p256_params());
839        assert_eq!(pk.bytes.len(), 65);
840        assert_eq!(pk.bytes[0], 0x04);
841
842        let compressed = P256::compress_pubkey(&pk).expect("compress");
843        assert_eq!(compressed.len(), 33);
844        assert!(compressed[0] == 0x02 || compressed[0] == 0x03);
845
846        let decompressed = P256::decompress_pubkey(&compressed).expect("decompress");
847        assert_eq!(decompressed.bytes, pk.bytes);
848    }
849
850    #[test]
851    fn test_sec1_compressed_roundtrip_all_curves() {
852        fn rt<C: Curve>() -> Option<()> {
853            let mut rng = TestRng::new(0x5EC1);
854            let (pk, _sk) = C::keygen(&mut rng);
855            let compressed = C::compress_pubkey(&pk)?;
856            let decompressed = C::decompress_pubkey(&compressed)?;
857            assert_eq!(decompressed.bytes, pk.bytes);
858            Some(())
859        }
860        assert!(rt::<P256>().is_some());
861        assert!(rt::<P384>().is_some());
862        assert!(rt::<Secp256k1>().is_some());
863        assert!(rt::<BrainpoolP256r1>().is_some());
864        assert!(rt::<BrainpoolP384r1>().is_some());
865        assert!(rt::<BrainpoolP512r1>().is_some());
866        assert!(rt::<P521>().is_some());
867    }
868
869    #[test]
870    fn test_verify_accepts_compressed_pubkey() {
871        let (sk, pk) = d1_keypair(&p256_params());
872        let msg = b"compressed pk end-to-end";
873        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
874
875        let compressed = P256::compress_pubkey(&pk).unwrap();
876        let compressed_pk = PublicKey { bytes: compressed };
877        assert!(P256::verify_msg::<Sha256>(&compressed_pk, msg, &sig));
878    }
879
880    #[test]
881    fn test_ecdh_accepts_compressed_pubkey() {
882        let mut rng = TestRng::new(0x5EC2);
883        let (pk_a, sk_a) = P256::keygen(&mut rng);
884        let (pk_b, sk_b) = P256::keygen(&mut rng);
885
886        let pk_a_c = PublicKey {
887            bytes: P256::compress_pubkey(&pk_a).unwrap(),
888        };
889        let pk_b_c = PublicKey {
890            bytes: P256::compress_pubkey(&pk_b).unwrap(),
891        };
892
893        let s_ab = P256::ecdh(&sk_a, &pk_b_c).unwrap();
894        let s_ba = P256::ecdh(&sk_b, &pk_a_c).unwrap();
895        assert_eq!(s_ab, s_ba);
896    }
897
898    #[test]
899    fn test_sec1_compressed_parity_bit() {
900        let mut rng = TestRng::new(0xBEEF);
901        for _ in 0..20 {
902            let (pk, _sk) = P256::keygen(&mut rng);
903            let compressed = P256::compress_pubkey(&pk).unwrap();
904            let y_lsb = pk.bytes[64] & 1;
905            let tag = compressed[0];
906            assert!(tag == 0x02 || tag == 0x03);
907            assert_eq!((tag & 1), y_lsb, "compressed tag parity must match Y LSB");
908        }
909    }
910
911    #[test]
912    fn test_decompress_rejects_wrong_length() {
913        assert!(P256::decompress_pubkey(&[0x02; 32]).is_none());
914        assert!(P256::decompress_pubkey(&[0x02; 34]).is_none());
915        assert!(P256::decompress_pubkey(&[]).is_none());
916    }
917
918    #[test]
919    fn test_decompress_rejects_unknown_tag() {
920        let mut bytes = vec![0u8; 33];
921        bytes[0] = 0x05;
922        assert!(P256::decompress_pubkey(&bytes).is_none());
923    }
924
925    // ----------------------------------------------------------------------
926    // DER roundtrip via Curve::sign_rfc6979 / verify
927    // ----------------------------------------------------------------------
928
929    /// DER round-trip on a freshly-signed signature: sig -> DER -> sig ->
930    /// verify. Value-level round-trip.
931    #[test]
932    fn test_der_sig_roundtrip_verifies() {
933        let (sk, pk) = d1_keypair(&p256_params());
934        let msg = b"DER round-trip";
935        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
936
937        let der = sig.to_der();
938        let parsed = Signature::from_der(&der).expect("from_der");
939        assert!(P256::verify_msg::<Sha256>(&pk, msg, &parsed));
940    }
941
942    /// DER bytes round-trip: from_der(to_der(sig)) gives the same bytes
943    /// when re-encoded. Idempotence on the DER side.
944    #[test]
945    fn test_der_idempotent_on_der_side() {
946        let (sk, _pk) = d1_keypair(&p256_params());
947        let msg = b"idempotent";
948        let sig = P256::sign_rfc6979_msg::<Sha256>(&sk, msg);
949
950        let der1 = sig.to_der();
951        let parsed = Signature::from_der(&der1).unwrap();
952        let der2 = parsed.to_der();
953        assert_eq!(der1, der2);
954    }
955}