Skip to main content

confium_tc_frost_ed25519/
group.rs

1//! Group primitives for FROST over ed25519.
2//!
3//! FROST (draft-irtf-cfrg-frost) is defined generically over a prime-order
4//! group. The "ed25519" instantiation works in the ristretto255-free
5//! Edwards form factor used by RFC 8032 ed25519 signatures so that the
6//! aggregate signature verifies under any standard ed25519 verifier
7//! (e.g. `ed25519-dalek`, libsodium, Go's `crypto/ed25519`).
8//!
9//! Scalars are 32-byte little-endian values reduced modulo the group
10//! order `ℓ = 2^252 + 27742317777372353535851937790883648493`. Group
11//! elements are Edwards points; their wire form is the 32-byte
12//! compressed-Y encoding (`CompressedEdwardsY`) used by ed25519 keys and
13//! signatures.
14//!
15//! All FROST math is scalar / point arithmetic in this group; this module
16//! adds nothing cryptographically — it just pins the right curve types
17//! and serialization.
18
19use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
20use curve25519_dalek::edwards::CompressedEdwardsY;
21use curve25519_dalek::edwards::EdwardsPoint;
22use curve25519_dalek::scalar::Scalar;
23
24use crate::error::{CODE_INVALID_COMMITMENT, CODE_MALFORMED_SHARE, FrostError, Result};
25
26/// Byte length of a scalar in its canonical wire encoding.
27pub const SCALAR_BYTES: usize = 32;
28
29/// Byte length of a compressed group element (commitment, public key).
30pub const ELEMENT_BYTES: usize = 32;
31
32/// The base point B used throughout FROST and ed25519.
33#[inline]
34pub fn base_point() -> EdwardsPoint {
35    ED25519_BASEPOINT_POINT
36}
37
38/// Multiply the base point B by a scalar — `s·B`.
39#[inline]
40pub fn mul_base(s: &Scalar) -> EdwardsPoint {
41    EdwardsPoint::mul_base(s)
42}
43
44/// Encode a scalar to its 32-byte little-endian wire form.
45#[inline]
46pub fn scalar_to_bytes(s: &Scalar) -> [u8; SCALAR_BYTES] {
47    s.to_bytes()
48}
49
50/// Decode a scalar from 32 bytes, reducing mod ℓ. Used for inputs like
51/// the local share where the encoding may have come from a wide hash.
52pub fn scalar_from_bytes_mod_order(bytes: &[u8; SCALAR_BYTES]) -> Scalar {
53    Scalar::from_bytes_mod_order(*bytes)
54}
55
56/// Decode a scalar from a slice, validating length.
57pub fn scalar_from_slice(bytes: &[u8]) -> Result<Scalar> {
58    if bytes.len() != SCALAR_BYTES {
59        return Err(FrostError::MalformedShare {
60            reason: "scalar must be exactly 32 bytes",
61            code: CODE_MALFORMED_SHARE,
62        });
63    }
64    let mut arr = [0u8; SCALAR_BYTES];
65    arr.copy_from_slice(bytes);
66    Ok(scalar_from_bytes_mod_order(&arr))
67}
68
69/// Compress a point to its 32-byte wire form.
70#[inline]
71pub fn point_to_bytes(p: &EdwardsPoint) -> [u8; ELEMENT_BYTES] {
72    p.compress().to_bytes()
73}
74
75/// Decompress a 32-byte encoded point. Returns `None` if the encoding is
76/// not a valid point on the curve.
77pub fn point_from_bytes(bytes: &[u8; ELEMENT_BYTES]) -> Option<EdwardsPoint> {
78    CompressedEdwardsY::from_slice(bytes).ok()?.decompress()
79}
80
81/// Decompress a point from a slice, validating both length and curve
82/// membership.
83pub fn point_from_slice(bytes: &[u8], party: &str) -> Result<EdwardsPoint> {
84    if bytes.len() != ELEMENT_BYTES {
85        return Err(FrostError::InvalidCommitment {
86            party: party.to_string(),
87            reason: "commitment must be exactly 32 bytes",
88            code: CODE_INVALID_COMMITMENT,
89        });
90    }
91    let mut arr = [0u8; ELEMENT_BYTES];
92    arr.copy_from_slice(bytes);
93    point_from_bytes(&arr).ok_or(FrostError::InvalidCommitment {
94        party: party.to_string(),
95        reason: "encoded point is not a valid curve point",
96        code: CODE_INVALID_COMMITMENT,
97    })
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn scalar_round_trip() {
106        let s = Scalar::from_bytes_mod_order([42u8; 32]);
107        let bytes = scalar_to_bytes(&s);
108        let back = scalar_from_bytes_mod_order(&bytes);
109        assert_eq!(s, back);
110    }
111
112    #[test]
113    fn base_point_times_one_is_base() {
114        let one = Scalar::ONE;
115        assert_eq!(mul_base(&one), base_point());
116    }
117
118    #[test]
119    fn point_round_trip_via_base() {
120        let s = Scalar::from(123u64);
121        let p = mul_base(&s);
122        let bytes = point_to_bytes(&p);
123        let back = point_from_bytes(&bytes).expect("round trips");
124        assert_eq!(p, back);
125    }
126
127    #[test]
128    fn point_from_slice_rejects_bad_length() {
129        let err = point_from_slice(&[0u8; 31], "x").unwrap_err();
130        match err {
131            FrostError::InvalidCommitment { .. } => {}
132            other => panic!("expected InvalidCommitment, got {other:?}"),
133        }
134    }
135
136    #[test]
137    fn point_from_slice_rejects_invalid_encoding() {
138        // `[2u8; 32]` is a known off-curve encoding: Y=2 has no
139        // corresponding x on the Edwards curve, so decompress fails.
140        let err = point_from_slice(&[2u8; 32], "x").unwrap_err();
141        match err {
142            FrostError::InvalidCommitment { reason, .. } => {
143                assert!(reason.contains("valid curve point"));
144            }
145            other => panic!("expected InvalidCommitment, got {other:?}"),
146        }
147    }
148}