Skip to main content

dcrypt_sign/eddsa/ed25519/
mod.rs

1//! Dcrypt-owned Ed25519 signing and strict verification.
2//!
3//! The implementation follows RFC 8032 and keeps all arithmetic in safe Rust.
4//! Point encodings are canonical, public keys and signature commitments must
5//! be non-identity prime-order points, and signature scalars must be below the
6//! subgroup order.  Secret scalar multiplication uses a fixed 256-iteration
7//! schedule with constant-time point selection.  Compiler- and target-specific
8//! validation is still required for a concrete side-channel claim.
9
10#![forbid(unsafe_code)]
11
12use dcrypt_algorithms::hash::sha2::Sha512;
13use dcrypt_algorithms::hash::HashFunction;
14use dcrypt_api::{
15    error::Error as ApiError, Result as ApiResult, Signature as SignatureTrait, ZeroizingBytes,
16};
17use dcrypt_internal::{
18    try_fill_bytes_zeroing_on_error, zeroizing_bytes_from_slice, ConstantTimeEq, CryptoRng,
19    RngCore, Zeroize, ZeroizeOnDrop, Zeroizing,
20};
21
22use super::constants::{ED25519_PUBLIC_KEY_SIZE, ED25519_SECRET_KEY_SIZE, ED25519_SIGNATURE_SIZE};
23use super::point::EdwardsPoint;
24use super::scalar::Scalar;
25
26/// Ed25519 signature scheme.
27pub struct Ed25519;
28
29/// Canonically encoded, non-identity, prime-order Ed25519 public key.
30#[derive(Clone)]
31pub struct Ed25519PublicKey(pub [u8; ED25519_PUBLIC_KEY_SIZE]);
32
33impl core::fmt::Debug for Ed25519PublicKey {
34    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35        f.debug_struct("Ed25519PublicKey")
36            .field("algorithm", &"Ed25519")
37            .finish()
38    }
39}
40
41/// Ed25519 secret seed, wiped on drop.
42#[derive(Clone)]
43pub struct Ed25519SecretKey {
44    seed: [u8; ED25519_SECRET_KEY_SIZE],
45}
46
47impl Zeroize for Ed25519SecretKey {
48    fn zeroize(&mut self) {
49        self.seed.zeroize();
50    }
51}
52
53impl Drop for Ed25519SecretKey {
54    fn drop(&mut self) {
55        self.zeroize();
56    }
57}
58
59impl ZeroizeOnDrop for Ed25519SecretKey {}
60
61impl core::fmt::Debug for Ed25519SecretKey {
62    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63        f.debug_struct("Ed25519SecretKey")
64            .field("algorithm", &"Ed25519")
65            .finish()
66    }
67}
68
69/// Canonically encoded Ed25519 signature bytes (`R || S`).
70#[derive(Clone)]
71pub struct Ed25519Signature(pub [u8; ED25519_SIGNATURE_SIZE]);
72
73impl core::fmt::Debug for Ed25519Signature {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        f.debug_struct("Ed25519Signature")
76            .field("length", &self.0.len())
77            .finish()
78    }
79}
80
81fn invalid_key(context: &'static str, _message: &'static str) -> ApiError {
82    ApiError::InvalidKey {
83        context,
84        #[cfg(feature = "std")]
85        message: _message.to_string(),
86    }
87}
88
89fn invalid_signature(context: &'static str, _message: &'static str) -> ApiError {
90    ApiError::InvalidSignature {
91        context,
92        #[cfg(feature = "std")]
93        message: _message.to_string(),
94    }
95}
96
97fn decode_prime_order_point(bytes: &[u8; 32], context: &'static str) -> ApiResult<EdwardsPoint> {
98    let point = EdwardsPoint::decompress(bytes)
99        .ok_or_else(|| invalid_key(context, "point encoding is invalid or non-canonical"))?;
100    if !point.is_strict_prime_order() {
101        return Err(invalid_key(
102            context,
103            "point is identity, small-order, or not torsion-free",
104        ));
105    }
106    Ok(point)
107}
108
109fn hash_parts(parts: &[&[u8]]) -> ApiResult<Zeroizing<[u8; 64]>> {
110    let mut hasher = Sha512::new();
111    for part in parts {
112        hasher.update(part).map_err(ApiError::from)?;
113    }
114    let mut digest = hasher.finalize().map_err(ApiError::from)?;
115    let mut output = Zeroizing::new([0u8; 64]);
116    output.copy_from_slice(digest.as_ref());
117    digest.zeroize();
118    Ok(output)
119}
120
121impl Ed25519PublicKey {
122    /// Parse a canonical, non-identity point in the prime-order subgroup.
123    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
124        let key_bytes: [u8; ED25519_PUBLIC_KEY_SIZE] =
125            bytes.try_into().map_err(|_| ApiError::InvalidLength {
126                context: "Ed25519PublicKey::from_bytes",
127                expected: ED25519_PUBLIC_KEY_SIZE,
128                actual: bytes.len(),
129            })?;
130        decode_prime_order_point(&key_bytes, "Ed25519PublicKey::from_bytes")?;
131        Ok(Self(key_bytes))
132    }
133
134    pub fn to_bytes(&self) -> [u8; ED25519_PUBLIC_KEY_SIZE] {
135        self.0
136    }
137
138    fn point(&self) -> ApiResult<EdwardsPoint> {
139        decode_prime_order_point(&self.0, "Ed25519 verify")
140    }
141}
142
143impl Ed25519SecretKey {
144    /// Construct a secret key from the RFC 8032 32-byte seed.
145    pub fn from_seed(seed: &[u8; ED25519_SECRET_KEY_SIZE]) -> ApiResult<Self> {
146        Ok(Self { seed: *seed })
147    }
148
149    pub fn seed(&self) -> &[u8; ED25519_SECRET_KEY_SIZE] {
150        &self.seed
151    }
152
153    pub fn export_seed(&self) -> ZeroizingBytes {
154        zeroizing_bytes_from_slice(&self.seed)
155    }
156
157    pub fn public_key(&self) -> ApiResult<Ed25519PublicKey> {
158        Ed25519::derive_public_from_secret(self)
159    }
160
161    fn expanded(&self) -> ApiResult<Zeroizing<[u8; 64]>> {
162        let mut expanded = hash_parts(&[&self.seed])?;
163        expanded[0] &= 248;
164        expanded[31] &= 127;
165        expanded[31] |= 64;
166        Ok(expanded)
167    }
168}
169
170impl Ed25519Signature {
171    /// Parse a signature with a canonical prime-order `R` and canonical `S`.
172    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
173        let signature: [u8; ED25519_SIGNATURE_SIZE] =
174            bytes.try_into().map_err(|_| ApiError::InvalidLength {
175                context: "Ed25519Signature::from_bytes",
176                expected: ED25519_SIGNATURE_SIZE,
177                actual: bytes.len(),
178            })?;
179        let r_bytes: [u8; 32] = signature[..32]
180            .try_into()
181            .map_err(|_| invalid_signature("Ed25519Signature::from_bytes", "invalid R length"))?;
182        decode_prime_order_point(&r_bytes, "Ed25519Signature::from_bytes")
183            .map_err(|_| invalid_signature("Ed25519Signature::from_bytes", "invalid R point"))?;
184        let s_bytes: [u8; 32] = signature[32..]
185            .try_into()
186            .map_err(|_| invalid_signature("Ed25519Signature::from_bytes", "invalid S length"))?;
187        Scalar::from_canonical_bytes(&s_bytes).ok_or_else(|| {
188            invalid_signature("Ed25519Signature::from_bytes", "S is not below group order")
189        })?;
190        Ok(Self(signature))
191    }
192
193    pub fn to_bytes(&self) -> [u8; ED25519_SIGNATURE_SIZE] {
194        self.0
195    }
196}
197
198impl SignatureTrait for Ed25519 {
199    type PublicKey = Ed25519PublicKey;
200    type SecretKey = Ed25519SecretKey;
201    type SignatureData = Ed25519Signature;
202    type KeyPair = (Self::PublicKey, Self::SecretKey);
203
204    fn name() -> &'static str {
205        "Ed25519"
206    }
207
208    /// Generate a key from caller-provided cryptographic randomness.
209    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
210        let mut seed = Zeroizing::new([0u8; ED25519_SECRET_KEY_SIZE]);
211        try_fill_bytes_zeroing_on_error(rng, &mut *seed).map_err(|_| {
212            ApiError::RandomGenerationError {
213                context: "Ed25519::keypair",
214                #[cfg(feature = "std")]
215                message: "caller-provided randomness source failed".into(),
216            }
217        })?;
218        let secret = Ed25519SecretKey::from_seed(&seed)?;
219        let public = secret.public_key()?;
220        Ok((public, secret))
221    }
222
223    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
224        keypair.0.clone()
225    }
226
227    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
228        keypair.1.clone()
229    }
230
231    fn sign(message: &[u8], secret_key: &Self::SecretKey) -> ApiResult<Self::SignatureData> {
232        let expanded = secret_key.expanded()?;
233        let mut secret_scalar_bytes = Zeroizing::new([0u8; 32]);
234        secret_scalar_bytes.copy_from_slice(&expanded[..32]);
235        let secret_scalar = Scalar::reduce_32(&secret_scalar_bytes);
236
237        let nonce_digest = hash_parts(&[&expanded[32..], message])?;
238        let nonce = Scalar::reduce_64(&nonce_digest);
239        let commitment = EdwardsPoint::basepoint().scalar_mult(&nonce);
240        if bool::from(commitment.is_identity()) {
241            return Err(invalid_signature(
242                "Ed25519 sign",
243                "derived commitment is the identity",
244            ));
245        }
246        let r_bytes = commitment.compress();
247        let public_bytes = EdwardsPoint::basepoint()
248            .scalar_mult(&secret_scalar)
249            .compress();
250
251        let challenge_digest = hash_parts(&[&r_bytes, &public_bytes, message])?;
252        let challenge = Scalar::reduce_64(&challenge_digest);
253        let response = nonce.add(&challenge.mul(&secret_scalar));
254
255        let mut signature = [0u8; ED25519_SIGNATURE_SIZE];
256        signature[..32].copy_from_slice(&r_bytes);
257        let response_bytes = response.to_bytes();
258        // The response becomes public only as part of the completed signature.
259        signature[32..].copy_from_slice(&response_bytes[..]);
260        Ok(Ed25519Signature(signature))
261    }
262
263    fn verify(
264        message: &[u8],
265        signature: &Self::SignatureData,
266        public_key: &Self::PublicKey,
267    ) -> ApiResult<()> {
268        // Revalidate tuple-struct values so direct construction cannot bypass
269        // canonical and subgroup checks.
270        let public_point = public_key.point()?;
271        let r_bytes: [u8; 32] = signature.0[..32]
272            .try_into()
273            .map_err(|_| invalid_signature("Ed25519 verify", "invalid R length"))?;
274        let r_point = decode_prime_order_point(&r_bytes, "Ed25519 signature R")
275            .map_err(|_| invalid_signature("Ed25519 verify", "invalid R point"))?;
276        let s_bytes: [u8; 32] = signature.0[32..]
277            .try_into()
278            .map_err(|_| invalid_signature("Ed25519 verify", "invalid S length"))?;
279        let response = Scalar::from_canonical_bytes(&s_bytes)
280            .ok_or_else(|| invalid_signature("Ed25519 verify", "S is not below group order"))?;
281
282        let challenge_digest = hash_parts(&[&r_bytes, &public_key.0, message])?;
283        let challenge = Scalar::reduce_64(&challenge_digest);
284        let left = EdwardsPoint::basepoint().scalar_mult(&response);
285        let right = r_point.add(&public_point.scalar_mult(&challenge));
286
287        if bool::from(left.ct_eq(&right)) {
288            Ok(())
289        } else {
290            Err(invalid_signature(
291                "Ed25519 verify",
292                "strict signature equation failed",
293            ))
294        }
295    }
296}
297
298impl Ed25519 {
299    pub fn derive_public_from_secret(secret_key: &Ed25519SecretKey) -> ApiResult<Ed25519PublicKey> {
300        let expanded = secret_key.expanded()?;
301        let mut scalar_bytes = Zeroizing::new([0u8; 32]);
302        scalar_bytes.copy_from_slice(&expanded[..32]);
303        let scalar = Scalar::reduce_32(&scalar_bytes);
304        let bytes = EdwardsPoint::basepoint().scalar_mult(&scalar).compress();
305        Ed25519PublicKey::from_bytes(&bytes)
306    }
307}
308
309#[cfg(test)]
310mod tests;