Skip to main content

pgp/crypto/
ed25519.rs

1//! EdDSA for OpenPGP.
2//!
3//! OpenPGP RFC 9580 specifies use of Ed25519 and Ed448.
4//!
5//! Use of Ed25519 is defined with two different framings (using different key types) in RFC 9580:
6//! - The new key format is called `Ed25519`. It can be used both with v4 and v6 keys.
7//! - The old key format has been renamed `EdDSALegacy`. It may only be used with v4 keys.
8//!
9//! Note: The two variants `Ed25519` and `EdDSALegacy` use the same cryptographic mechanism,
10//! and are interchangeable in terms of the low-level cryptographic primitives.
11//! However, at the OpenPGP layer their representation in the key material differs.
12//! This implicitly yields differing OpenPGP fingerprints, so the two OpenPGP key variants cannot
13//! be used interchangeably.
14
15use std::ops::Deref;
16
17use rand::{CryptoRng, Rng};
18use signature::{Signer as _, Verifier};
19use zeroize::{ZeroizeOnDrop, Zeroizing};
20
21use crate::{
22    crypto::{hash::HashAlgorithm, Signer},
23    errors::{bail, ensure, ensure_eq, Result},
24    ser::Serialize,
25    types::{Ed25519PublicParams, EddsaLegacyPublicParams, Mpi, SignatureBytes},
26};
27
28const MIN_HASH_LEN_BITS: usize = 256;
29
30/// Size in bytes of the raw ED25519 secret key.
31pub const KEY_LEN: usize = 32;
32
33/// Specifies which OpenPGP framing (e.g. `Ed25519` vs. `EdDSALegacy`) is used, and also chooses
34/// between curve Ed25519 and Ed448 (TODO: not yet implemented)
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
37pub enum Mode {
38    /// EdDSALegacy (with curve Ed25519). May only be used with v4 keys.
39    ///
40    /// Ref <https://www.rfc-editor.org/rfc/rfc9580.html#key-eddsa-legacy>
41    EdDSALegacy,
42
43    /// Ed25519 as defined in RFC 9580
44    ///
45    /// Ref <https://www.rfc-editor.org/rfc/rfc9580.html#name-algorithm-specific-part-for-ed2>
46    Ed25519,
47}
48
49/// Secret key for EdDSA with Curve25519, the only combination we currently support.
50#[derive(Clone, PartialEq, Eq, ZeroizeOnDrop, derive_more::Debug)]
51#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
52pub struct SecretKey {
53    /// The secret point.
54    #[debug("..")]
55    #[cfg_attr(test, proptest(strategy = "tests::key_gen()"))]
56    secret: ed25519_dalek::SigningKey,
57    #[zeroize(skip)]
58    pub(crate) mode: Mode,
59}
60
61impl From<&SecretKey> for Ed25519PublicParams {
62    fn from(value: &SecretKey) -> Self {
63        debug_assert_eq!(value.mode, Mode::Ed25519);
64        Self {
65            key: value.secret.verifying_key(),
66        }
67    }
68}
69
70impl From<&SecretKey> for EddsaLegacyPublicParams {
71    fn from(value: &SecretKey) -> Self {
72        debug_assert_eq!(value.mode, Mode::EdDSALegacy);
73        Self::Ed25519 {
74            key: value.secret.verifying_key(),
75        }
76    }
77}
78
79impl Deref for SecretKey {
80    type Target = ed25519_dalek::SigningKey;
81
82    fn deref(&self) -> &Self::Target {
83        &self.secret
84    }
85}
86
87impl SecretKey {
88    /// Generate an EdDSA `SecretKey`.
89    ///
90    /// This SecretKey type can be used to form either a `EddsaLegacyPublicParams` or a
91    /// `Ed25519PublicParams`.
92    pub fn generate<R: Rng + CryptoRng>(mut rng: R, mode: Mode) -> Self {
93        let mut bytes = Zeroizing::new([0u8; ed25519_dalek::SECRET_KEY_LENGTH]);
94        rng.fill_bytes(&mut *bytes);
95        let secret = ed25519_dalek::SigningKey::from_bytes(&bytes);
96
97        SecretKey { secret, mode }
98    }
99
100    pub fn try_from_bytes(raw_secret: [u8; KEY_LEN], mode: Mode) -> Result<Self> {
101        let secret = ed25519_dalek::SigningKey::from(raw_secret);
102        Ok(Self { secret, mode })
103    }
104
105    /// Returns the raw key
106    pub fn as_bytes(&self) -> &[u8; KEY_LEN] {
107        self.secret.as_bytes()
108    }
109
110    /// Returns the mode of this key.
111    pub fn mode(&self) -> Mode {
112        self.mode
113    }
114}
115
116impl Signer for SecretKey {
117    fn sign(&self, hash: HashAlgorithm, digest: &[u8]) -> Result<SignatureBytes> {
118        let Some(digest_size) = hash.digest_size() else {
119            bail!("EdDSA signature: invalid hash algorithm: {:?}", hash);
120        };
121        ensure_eq!(
122            digest.len(),
123            digest_size,
124            "Unexpected digest length {} for hash algorithm {:?}",
125            digest.len(),
126            hash,
127        );
128        ensure!(
129            digest_size * 8 >= MIN_HASH_LEN_BITS,
130            "EdDSA signature: hash algorithm {:?} is too weak for Ed25519",
131            hash,
132        );
133
134        let signature = self.secret.sign(digest);
135        let bytes = signature.to_bytes();
136
137        let sig = match self.mode {
138            Mode::EdDSALegacy => {
139                let r = Mpi::from_slice(&bytes[..32]);
140                let s = Mpi::from_slice(&bytes[32..]);
141                SignatureBytes::Mpis(vec![r, s])
142            }
143            Mode::Ed25519 => SignatureBytes::Native(bytes.to_vec().into()),
144        };
145        Ok(sig)
146    }
147}
148
149impl Serialize for SecretKey {
150    fn to_writer<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
151        match self.mode {
152            Mode::EdDSALegacy => {
153                Mpi::from_slice(&self.secret.as_bytes()[..]).to_writer(writer)?;
154            }
155            Mode::Ed25519 => {
156                let x = self.as_bytes();
157                writer.write_all(x)?;
158            }
159        }
160
161        Ok(())
162    }
163
164    fn write_len(&self) -> usize {
165        match self.mode {
166            Mode::EdDSALegacy => Mpi::from_slice(self.secret.as_bytes()).write_len(),
167            Mode::Ed25519 => KEY_LEN,
168        }
169    }
170}
171
172/// Verify an EdDSA signature.
173pub fn verify(
174    key: &ed25519_dalek::VerifyingKey,
175    hash: HashAlgorithm,
176    hashed: &[u8],
177    sig_bytes: &[u8],
178) -> Result<()> {
179    let Some(digest_size) = hash.digest_size() else {
180        bail!("EdDSA signature: invalid hash algorithm: {:?}", hash);
181    };
182    ensure!(
183        digest_size * 8 >= MIN_HASH_LEN_BITS,
184        "EdDSA signature: hash algorithm {:?} is too weak for Ed25519",
185        hash,
186    );
187
188    let sig = sig_bytes.try_into()?;
189
190    Ok(key.verify(hashed, &sig)?)
191}
192
193#[cfg(test)]
194mod tests {
195    use proptest::prelude::*;
196
197    prop_compose! {
198        pub fn key_gen()(bytes: [u8; 32]) -> ed25519_dalek::SigningKey {
199            ed25519_dalek::SigningKey::from_bytes(&bytes)
200        }
201    }
202}