Skip to main content

actpub_httpsig/key/
ed25519.rs

1//! Ed25519 signing and verification backed by [`aws_lc_rs`].
2
3use std::fmt;
4
5use aws_lc_rs::rand::SystemRandom;
6use aws_lc_rs::signature::{self, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
7
8use crate::error::Error;
9
10/// Length of a raw Ed25519 public key in bytes.
11pub(crate) const ED25519_PUBLIC_KEY_LEN: usize = 32;
12
13/// Length of a raw Ed25519 signature in bytes.
14pub(crate) const ED25519_SIGNATURE_LEN: usize = 64;
15
16/// An Ed25519 key pair capable of producing signatures.
17pub struct Ed25519SigningKey {
18    inner: Ed25519KeyPair,
19    /// Cached PKCS#8 encoding so we can hand back the same bytes via
20    /// [`Self::to_pkcs8_der`] after an in-memory generation.
21    pkcs8_der: Vec<u8>,
22}
23
24impl Ed25519SigningKey {
25    /// Generates a fresh Ed25519 key pair using the system RNG.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`Error::KeyGeneration`] if the underlying RNG fails, which
30    /// effectively only happens on platforms where `aws-lc-rs` cannot
31    /// initialise a secure random source.
32    pub fn generate() -> Result<Self, Error> {
33        let rng = SystemRandom::new();
34        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
35            .map_err(|_| Error::KeyGeneration("Ed25519 PKCS#8 generation failed"))?;
36        let inner = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref())
37            .map_err(|_| Error::KeyGeneration("Ed25519 PKCS#8 parse after generate"))?;
38        Ok(Self {
39            inner,
40            pkcs8_der: pkcs8.as_ref().to_vec(),
41        })
42    }
43
44    /// Loads an Ed25519 key pair from a PKCS#8 DER blob.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`Error::InvalidPkcs8`] if the DER cannot be decoded as an
49    /// Ed25519 `PrivateKeyInfo`.
50    pub fn from_pkcs8_der(der: &[u8]) -> Result<Self, Error> {
51        let inner = Ed25519KeyPair::from_pkcs8(der)
52            .map_err(|e| Error::InvalidPkcs8(format!("Ed25519: {e}")))?;
53        Ok(Self {
54            inner,
55            pkcs8_der: der.to_vec(),
56        })
57    }
58
59    /// Returns the PKCS#8 v2 DER encoding of the private key.
60    #[must_use]
61    pub fn to_pkcs8_der(&self) -> &[u8] {
62        &self.pkcs8_der
63    }
64
65    /// Returns the corresponding public key.
66    ///
67    /// # Panics
68    ///
69    /// Panics only if `aws-lc-rs` produces a public key whose length is
70    /// not exactly 32 bytes — which the Ed25519 algorithm forbids by
71    /// construction. A panic here therefore indicates a bug in
72    /// `aws-lc-rs` and not in our code.
73    #[must_use]
74    pub fn public_key(&self) -> Ed25519PublicKey {
75        #[allow(
76            clippy::expect_used,
77            reason = "aws-lc-rs guarantees an Ed25519 public key is exactly 32 bytes"
78        )]
79        let bytes: [u8; ED25519_PUBLIC_KEY_LEN] = self
80            .inner
81            .public_key()
82            .as_ref()
83            .try_into()
84            .expect("Ed25519 public key is exactly 32 bytes");
85        Ed25519PublicKey { bytes }
86    }
87
88    /// Produces a detached Ed25519 signature over `message`.
89    #[must_use]
90    pub fn sign(&self, message: &[u8]) -> Vec<u8> {
91        self.inner.sign(message).as_ref().to_vec()
92    }
93}
94
95impl fmt::Debug for Ed25519SigningKey {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        // Never print key material.
98        f.debug_struct("Ed25519SigningKey").finish_non_exhaustive()
99    }
100}
101
102/// A verifying half of an Ed25519 key pair.
103#[derive(Clone, Copy, PartialEq, Eq, Hash)]
104pub struct Ed25519PublicKey {
105    bytes: [u8; ED25519_PUBLIC_KEY_LEN],
106}
107
108impl Ed25519PublicKey {
109    /// Wraps a raw 32-byte public key.
110    ///
111    /// # Errors
112    ///
113    /// Returns [`Error::InvalidMultikeyLength`] if the slice is not exactly
114    /// 32 bytes.
115    pub fn from_bytes(raw: &[u8]) -> Result<Self, Error> {
116        let bytes: [u8; ED25519_PUBLIC_KEY_LEN] =
117            raw.try_into().map_err(|_| Error::InvalidMultikeyLength {
118                expected: ED25519_PUBLIC_KEY_LEN,
119                actual: raw.len(),
120            })?;
121        Ok(Self { bytes })
122    }
123
124    /// Returns the raw 32-byte representation.
125    #[must_use]
126    pub const fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_LEN] {
127        &self.bytes
128    }
129
130    /// Verifies a detached signature of `message` against this public key.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`Error::VerificationFailed`] if the signature is invalid,
135    /// malformed, or if `message` has been tampered with.
136    pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Error> {
137        if signature.len() != ED25519_SIGNATURE_LEN {
138            return Err(Error::VerificationFailed);
139        }
140        UnparsedPublicKey::new(&signature::ED25519, self.bytes)
141            .verify(message, signature)
142            .map_err(|_| Error::VerificationFailed)
143    }
144}
145
146impl fmt::Debug for Ed25519PublicKey {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        // Public keys are safe to print but we still use hex for readability.
149        f.debug_tuple("Ed25519PublicKey")
150            .field(&format_args!("{}", hex(&self.bytes)))
151            .finish()
152    }
153}
154
155#[allow(
156    clippy::expect_used,
157    reason = "writing to an owned `String` via `core::fmt::Write` is infallible; the `Result` only exists to satisfy the trait"
158)]
159fn hex(bytes: &[u8]) -> String {
160    use core::fmt::Write as _;
161    let mut out = String::with_capacity(bytes.len() * 2);
162    for b in bytes {
163        write!(out, "{b:02x}").expect("writing to an owned String is infallible");
164    }
165    out
166}
167
168#[cfg(test)]
169mod tests {
170    use pretty_assertions::assert_eq;
171
172    use super::*;
173
174    #[test]
175    fn generate_then_sign_and_verify_roundtrips() {
176        let key = Ed25519SigningKey::generate().expect("rng available");
177        let public = key.public_key();
178        let msg = b"activitypub inbox delivery";
179        let sig = key.sign(msg);
180        assert_eq!(sig.len(), ED25519_SIGNATURE_LEN);
181        public.verify(msg, &sig).expect("signature must verify");
182    }
183
184    #[test]
185    fn tampered_message_fails_verification() {
186        let key = Ed25519SigningKey::generate().expect("rng available");
187        let public = key.public_key();
188        let sig = key.sign(b"original message");
189        let err = public
190            .verify(b"tampered message", &sig)
191            .expect_err("tampered message must not verify");
192        assert!(matches!(err, Error::VerificationFailed));
193    }
194
195    #[test]
196    fn wrong_signature_length_is_rejected() {
197        let key = Ed25519SigningKey::generate().expect("rng available");
198        let public = key.public_key();
199        let err = public
200            .verify(b"msg", &[0u8; 32])
201            .expect_err("short signature must not verify");
202        assert!(matches!(err, Error::VerificationFailed));
203    }
204
205    #[test]
206    fn pkcs8_roundtrip_preserves_key() {
207        let original = Ed25519SigningKey::generate().expect("rng available");
208        let reloaded = Ed25519SigningKey::from_pkcs8_der(original.to_pkcs8_der())
209            .expect("reload must succeed");
210        assert_eq!(original.public_key(), reloaded.public_key());
211    }
212
213    #[test]
214    fn from_bytes_rejects_wrong_length() {
215        let err = Ed25519PublicKey::from_bytes(&[0u8; 16]).expect_err("16 bytes must be rejected");
216        assert!(matches!(
217            err,
218            Error::InvalidMultikeyLength {
219                expected: 32,
220                actual: 16
221            }
222        ));
223    }
224}