Skip to main content

miden_crypto/ecdh/ecgfp5/
mod.rs

1//! EcGFp5 Diffie-Hellman key agreement.
2//!
3//! Public keys use the canonical 40-byte EcGFp5 encoding: five little-endian Goldilocks
4//! coefficients. Secret keys are nonzero integers below the prime group order, encoded as
5//! 40 little-endian bytes. Identity public keys and noncanonical encodings are rejected.
6//!
7//! [`SecretKey::diffie_hellman`] computes a shared secret from a [`PublicKey`].
8//! [`PublicKey::to_commitment`] returns the public-key commitment used by MASM.
9//! [`PublicKey::advise`] prepares private advice for MASM key agreement.
10//!
11//! [`PreparedPublicKey`] expands a recipient key into paired 16-bit multiplication tables for
12//! the generator and recipient. MASM's `agree_preprocessed` authenticates entries supplied through
13//! advice against their Merkle commitment. It assumes the issuer generated the tables correctly.
14//!
15//! Agreement returns raw shared-secret material, not an encryption key. Applications must apply
16//! a context-bound KDF and authenticated encryption.
17//!
18//! Advice generation and VM execution are variable-time and must run in an environment trusted
19//! with the ephemeral secret. Keep the advice private, including its Merkle paths. A public proof
20//! needs zero-knowledge to hide the private execution witness.
21//!
22//! # References
23//!
24//! - [Reference implementation](https://github.com/pornin/ecgfp5/tree/ce059c6d1e1662db437aecbf3db6bb67fe63c716)
25//! - [EcGFp5: a Specialized Elliptic Curve](https://eprint.iacr.org/2022/274)
26
27use alloc::vec::Vec;
28
29mod advice;
30mod direct;
31mod native;
32mod tables;
33
34#[cfg(test)]
35mod tests;
36
37pub use advice::AgreementAdvice;
38use native::{curve::Point, field::GFp5, scalar::Scalar};
39use rand::CryptoRng;
40pub use tables::PreparedPublicKey;
41
42use crate::{
43    Felt, SequentialCommit, Word,
44    hash::eidos::{Eidos, domains::ECGFP5_PUBLIC_KEY},
45    utils::{
46        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
47        read_sensitive_array,
48        zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing},
49    },
50};
51
52/// A canonical, nonzero EcGFp5 scalar. The owned scalar is erased on drop.
53pub struct SecretKey(Zeroizing<Scalar>);
54
55impl SecretKey {
56    /// Samples a scalar uniformly from `1..n` using rejection sampling.
57    pub fn with_rng<R: CryptoRng>(rng: &mut R) -> Self {
58        let mut bytes = Zeroizing::new([0; 40]);
59        loop {
60            rng.fill_bytes(bytes.as_mut());
61            // The group order is just below 2^319.
62            bytes[39] &= 0x7f;
63            if let Ok(key) = Self::from_bytes(&bytes) {
64                return key;
65            }
66        }
67    }
68
69    /// Decodes a 40-byte little-endian scalar, rejecting zero and values at least the group order.
70    /// The caller retains responsibility for erasing the input bytes.
71    pub fn from_bytes(bytes: &[u8; 40]) -> Result<Self, InvalidSecretKey> {
72        let (scalar, valid) = Scalar::decode(bytes);
73        let scalar = Zeroizing::new(scalar);
74        if valid == 0 || scalar.iszero() != 0 {
75            return Err(InvalidSecretKey);
76        }
77        Ok(Self(scalar))
78    }
79
80    /// Encodes the scalar. The returned buffer is erased on drop.
81    pub fn to_bytes(&self) -> Zeroizing<[u8; 40]> {
82        Zeroizing::new(self.0.encode())
83    }
84
85    /// Computes the public key using the conventional EcGFp5 generator.
86    pub fn public_key(&self) -> PublicKey {
87        PublicKey(Point::mulgen(*self.0).encode().encode())
88    }
89
90    /// Computes raw shared-secret material with a validated, nonidentity public key.
91    pub fn diffie_hellman(&self, public_key: &PublicKey) -> SharedSecret {
92        SharedSecret((public_key.point() * *self.0).encode().encode())
93    }
94}
95
96impl ZeroizeOnDrop for SecretKey {}
97
98impl Serializable for SecretKey {
99    fn write_into<W: ByteWriter>(&self, target: &mut W) {
100        target.write_bytes(self.to_bytes().as_ref());
101    }
102}
103
104impl Deserializable for SecretKey {
105    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
106        let bytes = read_sensitive_array::<40, _>(source)?;
107        Self::from_bytes(&bytes)
108            .map_err(|_| DeserializationError::InvalidValue("invalid EcGFp5 secret key".into()))
109    }
110}
111
112/// A canonical, nonidentity EcGFp5 public key.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub struct PublicKey([u8; 40]);
115
116impl PublicKey {
117    /// Returns the domain-separated Eidos commitment to the five-Felt public-key encoding.
118    pub fn to_commitment(&self) -> Word {
119        <Self as SequentialCommit>::to_commitment(self)
120    }
121
122    /// Decodes a canonical point, rejecting invalid field coefficients and the identity.
123    pub fn from_bytes(bytes: [u8; 40]) -> Result<Self, InvalidPublicKey> {
124        let (encoding, canonical) = GFp5::decode(&bytes);
125        let (_, valid) = Point::decode(encoding);
126        if canonical == 0 || valid == 0 || encoding.iszero() != 0 {
127            return Err(InvalidPublicKey);
128        }
129        Ok(Self(bytes))
130    }
131
132    /// Decodes the five coefficients used by the MASM procedure.
133    pub fn from_elements(elements: [Felt; 5]) -> Result<Self, InvalidPublicKey> {
134        let mut bytes = [0; 40];
135        for (chunk, element) in bytes.chunks_exact_mut(8).zip(elements) {
136            chunk.copy_from_slice(&element.as_canonical_u64().to_le_bytes());
137        }
138        Self::from_bytes(bytes)
139    }
140
141    /// Returns the canonical 40-byte encoding.
142    pub fn to_bytes(self) -> [u8; 40] {
143        self.0
144    }
145
146    /// Returns the five coefficients in increasing polynomial degree order.
147    pub fn to_elements(self) -> [Felt; 5] {
148        elements_from_bytes(&self.0)
149    }
150
151    fn point(self) -> Point {
152        let (encoding, _) = GFp5::decode(&self.0);
153        Point::decode(encoding).0
154    }
155}
156
157impl SequentialCommit for PublicKey {
158    type Commitment = Word;
159
160    fn to_commitment(&self) -> Word {
161        Eidos::hash_elements_in_domain(&PublicKey::to_elements(*self), ECGFP5_PUBLIC_KEY)
162    }
163
164    fn to_elements(&self) -> Vec<Felt> {
165        PublicKey::to_elements(*self).to_vec()
166    }
167}
168
169impl Serializable for PublicKey {
170    fn write_into<W: ByteWriter>(&self, target: &mut W) {
171        target.write_bytes(&self.0);
172    }
173}
174
175impl Deserializable for PublicKey {
176    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
177        Self::from_bytes(source.read_array()?)
178            .map_err(|_| DeserializationError::InvalidValue("invalid EcGFp5 public key".into()))
179    }
180}
181
182/// Raw DH output, encoded as a canonical EcGFp5 point. The owned bytes are erased on drop.
183/// This value must pass through a KDF before use as an encryption key.
184pub struct SharedSecret([u8; 40]);
185
186impl SharedSecret {
187    /// Returns the VM representation. The caller must protect and erase this copy.
188    pub fn to_elements(&self) -> [Felt; 5] {
189        elements_from_bytes(&self.0)
190    }
191}
192
193impl AsRef<[u8]> for SharedSecret {
194    fn as_ref(&self) -> &[u8] {
195        &self.0
196    }
197}
198
199impl Zeroize for SharedSecret {
200    fn zeroize(&mut self) {
201        self.0.zeroize();
202    }
203}
204
205impl Drop for SharedSecret {
206    fn drop(&mut self) {
207        self.zeroize();
208    }
209}
210
211impl ZeroizeOnDrop for SharedSecret {}
212
213/// A secret scalar is zero or is not below the group order.
214#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
215#[error("invalid EcGFp5 secret scalar")]
216pub struct InvalidSecretKey;
217
218/// A point encoding is invalid, noncanonical, or the identity.
219#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
220#[error("invalid EcGFp5 public key")]
221pub struct InvalidPublicKey;
222
223fn elements_from_bytes(bytes: &[u8; 40]) -> [Felt; 5] {
224    core::array::from_fn(|i| {
225        Felt::new_unchecked(u64::from_le_bytes(bytes[8 * i..8 * i + 8].try_into().unwrap()))
226    })
227}