Skip to main content

hopr_crypto_packet/sphinx/
ec_groups.rs

1use hopr_types::crypto::errors::Result;
2#[cfg(feature = "x25519")]
3use hopr_types::crypto::primitives::Curve25519MontgomeryPoint;
4#[cfg(any(feature = "x25519", feature = "ed25519"))]
5use hopr_types::crypto::primitives::{Curve25519CompressedPoint, Curve25519Point, Curve25519Scalar, IsIdentity};
6#[cfg(any(feature = "x25519", feature = "ed25519"))]
7use hopr_types::crypto::types::{ExpandedOffchainPublicKey, OffchainPublicKey};
8#[cfg(feature = "secp256k1")]
9use hopr_types::crypto::{
10    crypto_traits::elliptic_curve::{
11        self, Group,
12        field::FieldBytes,
13        sec1::{FromSec1Point, Sec1Point, ToSec1Point},
14    },
15    keypairs::ChainKeypair,
16    prelude::{CryptoError, Secp256k1},
17    types::PublicKey,
18};
19
20use super::shared_keys::{Alpha, GroupElement, Scalar, SphinxSuite};
21
22/// Newtype for secp256k1 scalars — avoids Rust 2024 coherence issues
23/// that arise when using transparent type aliases to `elliptic_curve::Scalar<Secp256k1>`.
24#[cfg(feature = "secp256k1")]
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
26pub struct Secp256k1Scalar(pub elliptic_curve::Scalar<Secp256k1>);
27
28/// Newtype for secp256k1 projective points — avoids Rust 2024 coherence issues
29/// that arise when using transparent type aliases to `elliptic_curve::ProjectivePoint<Secp256k1>`.
30#[cfg(feature = "secp256k1")]
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct Secp256k1Point(pub elliptic_curve::ProjectivePoint<Secp256k1>);
33
34#[cfg(feature = "secp256k1")]
35impl Default for Secp256k1Point {
36    fn default() -> Self {
37        Self(elliptic_curve::ProjectivePoint::<Secp256k1>::IDENTITY)
38    }
39}
40
41// Forwarding impls needed by SphinxSuite trait bounds
42#[cfg(feature = "secp256k1")]
43impl std::ops::Mul for Secp256k1Scalar {
44    type Output = Self;
45
46    fn mul(self, rhs: Self) -> Self::Output {
47        Self(self.0 * rhs.0)
48    }
49}
50
51#[cfg(feature = "secp256k1")]
52impl<'a> std::ops::Mul<&'a Secp256k1Scalar> for Secp256k1Point {
53    type Output = Self;
54
55    fn mul(self, rhs: &'a Secp256k1Scalar) -> Self::Output {
56        Self(self.0 * rhs.0)
57    }
58}
59
60#[cfg(feature = "secp256k1")]
61impl From<&ChainKeypair> for Secp256k1Scalar {
62    fn from(value: &ChainKeypair) -> Self {
63        Self(elliptic_curve::Scalar::<Secp256k1>::from(value))
64    }
65}
66
67#[cfg(feature = "secp256k1")]
68impl From<&PublicKey> for Secp256k1Point {
69    fn from(value: &PublicKey) -> Self {
70        Self(elliptic_curve::ProjectivePoint::<Secp256k1>::from(value))
71    }
72}
73
74#[cfg(any(feature = "x25519", feature = "ed25519"))]
75impl Scalar for Curve25519Scalar {
76    fn random() -> Self {
77        let bytes = hopr_types::crypto_random::random_bytes::<32>();
78        Self::from_bytes(&bytes).unwrap()
79    }
80
81    fn from_bytes(bytes: &[u8]) -> Result<Self> {
82        hopr_types::crypto::utils::x25519_scalar_from_bytes(bytes)
83    }
84}
85
86#[cfg(feature = "secp256k1")]
87impl Scalar for Secp256k1Scalar {
88    fn random() -> Self {
89        // Beware, this is not constant-time
90        let mut rng = hopr_types::crypto_random::rng();
91        let mut bytes = FieldBytes::<Secp256k1>::default();
92        use elliptic_curve::PrimeField;
93        use hopr_types::crypto_random::Rng;
94        // Needs manual implementation due to incompatible rand crates
95        // elliptic_curve::Scalar::<Secp256k1>::generate_vartime(&mut hopr_types::crypto_random::rng())
96
97        loop {
98            rng.fill_bytes(&mut bytes);
99            if let Some(scalar) = elliptic_curve::Scalar::<Secp256k1>::from_repr(bytes).into() {
100                return Secp256k1Scalar(scalar);
101            }
102        }
103    }
104
105    fn from_bytes(bytes: &[u8]) -> Result<Self> {
106        Ok(Secp256k1Scalar(hopr_types::crypto::utils::k256_scalar_from_bytes(
107            bytes,
108        )?))
109    }
110}
111
112#[cfg(feature = "x25519")]
113impl GroupElement<Curve25519Scalar> for Curve25519MontgomeryPoint {
114    type AlphaLen = hopr_types::primitive::typenum::U32;
115
116    fn to_alpha(&self) -> Alpha<hopr_types::primitive::typenum::U32> {
117        self.0.into()
118    }
119
120    fn from_alpha(alpha: Alpha<hopr_types::primitive::typenum::U32>) -> Result<Self> {
121        Ok(Curve25519MontgomeryPoint(alpha.into()))
122    }
123
124    fn generate(scalar: &Curve25519Scalar) -> Self {
125        Curve25519Point::mul_base(scalar).to_montgomery()
126    }
127
128    fn is_valid(&self) -> bool {
129        use IsIdentity;
130        !self.is_identity()
131    }
132}
133
134#[cfg(feature = "ed25519")]
135impl GroupElement<Curve25519Scalar> for Curve25519Point {
136    type AlphaLen = hopr_types::primitive::typenum::U32;
137
138    fn to_alpha(&self) -> Alpha<hopr_types::primitive::typenum::U32> {
139        self.compress().0.into()
140    }
141
142    fn from_alpha(alpha: Alpha<hopr_types::primitive::typenum::U32>) -> Result<Self> {
143        Curve25519CompressedPoint(alpha.into())
144            .decompress()
145            .ok_or(hopr_types::crypto::errors::CryptoError::InvalidInputValue("alpha"))
146    }
147
148    fn generate(scalar: &Curve25519Scalar) -> Self {
149        Curve25519Point::mul_base(scalar)
150    }
151
152    fn is_valid(&self) -> bool {
153        // Ed25519 scalars always come clamped (pre-multiplied by the curve's co-factor)
154        // and therefore cannot result into points of small order.
155        // See `x25519_scalar_from_bytes` for more details.
156        use IsIdentity;
157        !self.is_identity()
158    }
159}
160
161#[cfg(feature = "secp256k1")]
162impl GroupElement<Secp256k1Scalar> for Secp256k1Point {
163    type AlphaLen = hopr_types::primitive::typenum::U33;
164
165    fn to_alpha(&self) -> Alpha<hopr_types::primitive::typenum::U33> {
166        debug_assert!(self.is_valid(), "to_alpha called on identity point");
167        let mut ret = Alpha::<hopr_types::primitive::typenum::U33>::default();
168        // Copy only if the point is not the identity, we do not care here about constant-time here.
169        if !bool::from(self.0.is_identity()) {
170            ret.copy_from_slice(self.0.to_affine().to_sec1_point(true).as_ref());
171        }
172        ret
173    }
174
175    fn from_alpha(alpha: Alpha<hopr_types::primitive::typenum::U33>) -> Result<Self> {
176        Sec1Point::<Secp256k1>::from_bytes(alpha)
177            .map_err(|_| CryptoError::InvalidInputValue("alpha"))
178            .and_then(|ep| {
179                <Secp256k1 as elliptic_curve::CurveArithmetic>::AffinePoint::from_sec1_point(&ep)
180                    .into_option()
181                    .ok_or(CryptoError::InvalidInputValue("alpha"))
182            })
183            .map(|ap| Secp256k1Point(elliptic_curve::ProjectivePoint::<Secp256k1>::from(ap)))
184    }
185
186    fn generate(scalar: &Secp256k1Scalar) -> Self {
187        Secp256k1Point(elliptic_curve::ProjectivePoint::<Secp256k1>::mul_by_generator(
188            &scalar.0,
189        ))
190    }
191
192    fn is_valid(&self) -> bool {
193        !bool::from(self.0.is_identity())
194    }
195}
196
197// TODO: invert this, so that each SphinxSuite takes this as a type argument
198/// Default packet block size for the Sphinx protocol.
199///
200/// Currently, 3248 bytes.
201pub type DefaultSphinxPacketSize = hopr_types::primitive::hybrid_array::sizes::U3248;
202
203pub use hopr_types::primitive::typenum::Unsigned;
204
205/// Represents an instantiation of the Sphinx protocol using secp256k1 elliptic curve and `ChainKeypair`
206#[cfg(feature = "secp256k1")]
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub struct Secp256k1Suite;
210
211#[cfg(feature = "secp256k1")]
212impl SphinxSuite for Secp256k1Suite {
213    type E = Secp256k1Scalar;
214    // `ChainKeypair::Public` already carries the curve point, so expansion is the identity.
215    type ExpandedPublic = PublicKey;
216    type G = Secp256k1Point;
217    type P = hopr_types::crypto::keypairs::ChainKeypair;
218    type PRP = hopr_types::crypto::lioness::LionessBlake3ChaCha20<DefaultSphinxPacketSize>;
219
220    fn expand_public(public_key: &PublicKey) -> Result<Self::ExpandedPublic> {
221        Ok(*public_key)
222    }
223}
224
225/// Represents an instantiation of the Sphinx protocol using the ed25519 curve and `OffchainKeypair`
226#[cfg(feature = "ed25519")]
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229pub struct Ed25519Suite;
230
231#[cfg(feature = "ed25519")]
232impl SphinxSuite for Ed25519Suite {
233    type E = Curve25519Scalar;
234    type ExpandedPublic = ExpandedOffchainPublicKey;
235    type G = Curve25519Point;
236    type P = hopr_types::crypto::keypairs::OffchainKeypair;
237    type PRP = hopr_types::crypto::lioness::LionessBlake3ChaCha20<DefaultSphinxPacketSize>;
238
239    fn expand_public(public_key: &OffchainPublicKey) -> Result<Self::ExpandedPublic> {
240        Ok(ExpandedOffchainPublicKey::try_from(public_key)?)
241    }
242}
243
244/// Represents an instantiation of the Sphinx protocol using the Curve25519 curve and `OffchainKeypair`
245#[cfg(feature = "x25519")]
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
248pub struct X25519Suite;
249
250#[cfg(feature = "x25519")]
251impl SphinxSuite for X25519Suite {
252    type E = Curve25519Scalar;
253    // Same expansion as `Ed25519Suite`, viewed as a Montgomery point instead.
254    type ExpandedPublic = ExpandedOffchainPublicKey;
255    type G = Curve25519MontgomeryPoint;
256    type P = hopr_types::crypto::keypairs::OffchainKeypair;
257    type PRP = hopr_types::crypto::lioness::LionessBlake3ChaCha20<DefaultSphinxPacketSize>;
258
259    fn expand_public(public_key: &OffchainPublicKey) -> Result<Self::ExpandedPublic> {
260        Ok(ExpandedOffchainPublicKey::try_from(public_key)?)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use parameterized::parameterized;
267
268    use super::super::shared_keys::tests::generic_sphinx_suite_test;
269
270    #[cfg(feature = "secp256k1")]
271    #[test]
272    fn test_extract_key_from_group_element() {
273        use hopr_types::crypto::{crypto_traits::elliptic_curve, prelude::Secp256k1};
274
275        use super::{super::shared_keys::GroupElement, Secp256k1Point};
276
277        let salt = [0xde, 0xad, 0xbe, 0xef];
278        let pt = Secp256k1Point(elliptic_curve::ProjectivePoint::<Secp256k1>::GENERATOR);
279
280        let key = pt.extract_key("test", &salt);
281        assert_eq!(
282            "08112a22609819a4c698d6c92f404628ca925f3d731d53594126ffdf19ef6fa9",
283            const_hex::encode(key)
284        );
285    }
286
287    #[cfg(feature = "secp256k1")]
288    #[test]
289    fn test_expand_key_from_group_element() {
290        use hopr_types::crypto::{crypto_traits::elliptic_curve, prelude::Secp256k1};
291
292        use super::{super::shared_keys::GroupElement, Secp256k1Point};
293
294        let salt = [0xde, 0xad, 0xbe, 0xef];
295        let pt = Secp256k1Point(elliptic_curve::ProjectivePoint::<Secp256k1>::GENERATOR);
296
297        let key = pt.extract_key("test", &salt);
298
299        assert_eq!(
300            "08112a22609819a4c698d6c92f404628ca925f3d731d53594126ffdf19ef6fa9",
301            const_hex::encode(key)
302        );
303    }
304
305    #[cfg(feature = "secp256k1")]
306    #[parameterized(nodes = {4, 3, 2, 1})]
307    fn test_secp256k1_suite(nodes: usize) {
308        generic_sphinx_suite_test::<super::Secp256k1Suite>(nodes)
309    }
310
311    #[cfg(feature = "ed25519")]
312    #[parameterized(nodes = {4, 3, 2, 1})]
313    fn test_ed25519_shared_keys(nodes: usize) {
314        generic_sphinx_suite_test::<crate::Ed25519Suite>(nodes)
315    }
316
317    #[cfg(feature = "x25519")]
318    #[parameterized(nodes = {4, 3, 2, 1})]
319    fn test_montgomery_shared_keys(nodes: usize) {
320        generic_sphinx_suite_test::<super::X25519Suite>(nodes)
321    }
322}