Skip to main content

darkbio_crypto/eddsa/
mod.rs

1// crypto-rs: cryptography primitives and wrappers
2// Copyright 2025 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! EdDSA cryptography wrappers and parametrization.
8//!
9//! <https://datatracker.ietf.org/doc/html/rfc8032>
10//!
11//! Ed25519 on its own, the classical half of the composite scheme in the
12//! `xdsa` module. Prefer that module unless a protocol demands plain Ed25519.
13//!
14//! ```
15//! use darkbio_crypto::eddsa;
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let secret = eddsa::SecretKey::generate();
19//! let signature = secret.sign(b"hello");
20//! secret.public_key().verify(b"hello", &signature)?;
21//! # Ok(())
22//! # }
23//! ```
24
25use crate::pem;
26use base64::Engine;
27use base64::engine::general_purpose::STANDARD as BASE64;
28use der::asn1::OctetStringRef;
29use der::{Decode, Encode};
30use ed25519_dalek::pkcs8::{DecodePublicKey, EncodePublicKey};
31use ed25519_dalek::{Signer, Verifier};
32use pkcs8::PrivateKeyInfoRef;
33use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
34use sha2::Digest;
35use spki::der::AnyRef;
36use spki::der::asn1::BitStringRef;
37use spki::{ObjectIdentifier, SubjectPublicKeyInfo};
38use zeroize::Zeroizing;
39
40/// The ASN.1 object identifier for Ed25519.
41pub const OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
42
43/// Size of the secret key in bytes.
44pub const SECRET_KEY_SIZE: usize = 32;
45
46/// Size of the public key in bytes.
47pub const PUBLIC_KEY_SIZE: usize = 32;
48
49/// Size of a signature in bytes.
50pub const SIGNATURE_SIZE: usize = 64;
51
52/// Size of a fingerprint in bytes.
53pub const FINGERPRINT_SIZE: usize = 32;
54
55/// Failures of the EdDSA operations.
56#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
57pub enum Error {
58    /// The PEM wrapper is malformed. The inner error names the rule it broke.
59    /// Raised by [`SecretKey::from_pem`] and [`PublicKey::from_pem`].
60    #[error("pem: {0}")]
61    Pem(#[from] pem::Error),
62    /// The PEM block type is not the expected one, `PRIVATE KEY` for
63    /// [`SecretKey::from_pem`] and `PUBLIC KEY` for [`PublicKey::from_pem`].
64    /// Carries the type found.
65    #[error("invalid PEM tag {0}")]
66    UnexpectedPemTag(String),
67    /// The DER key names an algorithm other than Ed25519, see [`OID`].
68    /// Raised by [`SecretKey::from_der`]
69    /// and [`PublicKey::from_der`], and through them by the PEM parsers.
70    #[error("not an Ed25519 key")]
71    UnexpectedAlgorithm,
72    /// The key encoding cannot be parsed or its contents are unusable, such
73    /// as a seed or point of the wrong size, unexpected algorithm parameters,
74    /// trailing DER bytes, or an unsupported PKCS#8 version. The message names
75    /// the problem. Raised by the fallible key constructors.
76    #[error("malformed key: {0}")]
77    MalformedKey(String),
78    /// A parsed DER key re-encodes to a different length than the input.
79    /// This is a fallback consistency check in the DER constructors. The DER
80    /// parser rejects appended bytes first and reports [`Error::MalformedKey`]
81    /// instead, including when called through the PEM constructors.
82    #[error("trailing data in key encoding")]
83    TrailingData,
84    /// The signature does not verify under the key for this message. Raised by
85    /// [`PublicKey::verify`].
86    #[error("signature verification failed")]
87    InvalidSignature,
88}
89
90/// An Ed25519 private key usable for signing.
91#[derive(Clone)]
92pub struct SecretKey {
93    inner: ed25519_dalek::SigningKey,
94}
95
96impl SecretKey {
97    /// Creates a new, random private key.
98    pub fn generate() -> SecretKey {
99        let mut seed = Zeroizing::new([0u8; SECRET_KEY_SIZE]);
100        getrandom::fill(seed.as_mut()).unwrap();
101
102        Self::from_bytes(&seed)
103    }
104
105    /// Converts a 32-byte array into a private key.
106    pub fn from_bytes(bin: &[u8; SECRET_KEY_SIZE]) -> Self {
107        let key = Zeroizing::new(ed25519_dalek::SecretKey::from(*bin));
108        let sig = ed25519_dalek::SigningKey::from(&*key);
109        Self { inner: sig }
110    }
111
112    /// Parses a DER buffer into a private key.
113    pub fn from_der(der: &[u8]) -> Result<Self, Error> {
114        let info =
115            PrivateKeyInfoRef::try_from(der).map_err(|err| Error::MalformedKey(err.to_string()))?;
116
117        // Reject trailing data by verifying re-encoded length matches input
118        let length = info
119            .encoded_len()
120            .map_err(|err| Error::MalformedKey(err.to_string()))?;
121        if length.try_into() != Ok(der.len()) {
122            return Err(Error::TrailingData);
123        }
124        // Ensure the algorithm OID matches Ed25519
125        if info.algorithm.oid != OID {
126            return Err(Error::UnexpectedAlgorithm);
127        }
128        // Reject v2 keys. The decoder only ever yields v1 keys without or v2
129        // keys with an embedded public key, all other combinations fail to
130        // parse, so public key presence is an exact v2 marker.
131        if info.public_key.is_some() {
132            return Err(Error::MalformedKey("unsupported PKCS#8 version".into()));
133        }
134        // Ensure no algorithm parameters are present, none are allowed
135        if info.algorithm.parameters.is_some() {
136            return Err(Error::MalformedKey(
137                "unexpected algorithm parameters".into(),
138            ));
139        }
140        // The private key field contains an OCTET STRING wrapping the seed
141        let seed = <&OctetStringRef>::from_der(info.private_key.as_bytes())
142            .map_err(|err| Error::MalformedKey(err.to_string()))?;
143        let seed: Zeroizing<[u8; SECRET_KEY_SIZE]> = Zeroizing::new(
144            seed.as_bytes()
145                .try_into()
146                .map_err(|_| Error::MalformedKey("seed not 32 bytes".into()))?,
147        );
148        Ok(Self::from_bytes(&seed))
149    }
150
151    /// Parses a PEM string into a private key.
152    pub fn from_pem(pem_str: &str) -> Result<Self, Error> {
153        let (kind, data) = pem::decode(pem_str.as_bytes())?;
154        if kind != "PRIVATE KEY" {
155            return Err(Error::UnexpectedPemTag(kind));
156        }
157        Self::from_der(&data)
158    }
159
160    /// Converts a private key into a 32-byte array.
161    pub fn to_bytes(&self) -> Zeroizing<[u8; SECRET_KEY_SIZE]> {
162        Zeroizing::new(self.inner.to_bytes())
163    }
164
165    /// Serializes a private key into a DER buffer.
166    pub fn to_der(&self) -> Zeroizing<Vec<u8>> {
167        // The private key field contains an OCTET STRING wrapping the seed
168        let seed = self.to_bytes();
169        let inner = Zeroizing::new(
170            OctetStringRef::new(seed.as_slice())
171                .unwrap()
172                .to_der()
173                .unwrap(),
174        );
175
176        let alg = pkcs8::AlgorithmIdentifierRef {
177            oid: OID,
178            parameters: None::<AnyRef>,
179        };
180        let info = PrivateKeyInfoRef {
181            algorithm: alg,
182            private_key: OctetStringRef::new(&inner).unwrap(),
183            public_key: None,
184        };
185        Zeroizing::new(info.to_der().unwrap())
186    }
187
188    /// Serializes a private key into a PEM string.
189    pub fn to_pem(&self) -> Zeroizing<String> {
190        Zeroizing::new(pem::encode("PRIVATE KEY", &self.to_der()))
191    }
192
193    /// Retrieves the public counterpart of the secret key.
194    pub fn public_key(&self) -> PublicKey {
195        PublicKey {
196            inner: self.inner.verifying_key(),
197        }
198    }
199
200    /// Returns a 256-bit unique identifier for this key. For Ed25519,
201    /// that is the SHA256 hash of the raw public key.
202    pub fn fingerprint(&self) -> Fingerprint {
203        self.public_key().fingerprint()
204    }
205
206    /// Creates a digital signature of the message.
207    pub fn sign(&self, message: &[u8]) -> Signature {
208        Signature(self.inner.sign(message).to_bytes())
209    }
210}
211
212/// An Ed25519 public key usable for verification.
213#[derive(Debug, Clone)]
214pub struct PublicKey {
215    inner: ed25519_dalek::VerifyingKey,
216}
217
218impl PublicKey {
219    /// Converts a 32-byte array into a public key.
220    pub fn from_bytes(bin: &[u8; PUBLIC_KEY_SIZE]) -> Result<Self, Error> {
221        let inner = ed25519_dalek::VerifyingKey::from_bytes(bin)
222            .map_err(|err| Error::MalformedKey(err.to_string()))?;
223        Ok(Self { inner })
224    }
225
226    /// Parses a DER buffer into a public key.
227    pub fn from_der(der: &[u8]) -> Result<Self, Error> {
228        // Parse with SPKI to check for trailing data
229        let info: SubjectPublicKeyInfo<AnyRef, BitStringRef> = SubjectPublicKeyInfo::from_der(der)
230            .map_err(|err| Error::MalformedKey(err.to_string()))?;
231        let length = info
232            .encoded_len()
233            .map_err(|err| Error::MalformedKey(err.to_string()))?;
234        if length.try_into() != Ok(der.len()) {
235            return Err(Error::TrailingData);
236        }
237        // Ensure the algorithm OID matches Ed25519
238        if info.algorithm.oid != OID {
239            return Err(Error::UnexpectedAlgorithm);
240        }
241        // Ensure no algorithm parameters are present, none are allowed
242        if info.algorithm.parameters.is_some() {
243            return Err(Error::MalformedKey(
244                "unexpected algorithm parameters".into(),
245            ));
246        }
247        let inner = ed25519_dalek::VerifyingKey::from_public_key_der(der)
248            .map_err(|err| Error::MalformedKey(err.to_string()))?;
249        Ok(Self { inner })
250    }
251
252    /// Parses a PEM string into a public key.
253    pub fn from_pem(pem_str: &str) -> Result<Self, Error> {
254        let (kind, data) = pem::decode(pem_str.as_bytes())?;
255        if kind != "PUBLIC KEY" {
256            return Err(Error::UnexpectedPemTag(kind));
257        }
258        Self::from_der(&data)
259    }
260
261    /// Converts a public key into a 32-byte array.
262    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] {
263        self.inner.to_bytes()
264    }
265
266    /// Serializes a public key into a DER buffer.
267    pub fn to_der(&self) -> Vec<u8> {
268        self.inner.to_public_key_der().unwrap().as_bytes().to_vec()
269    }
270
271    /// Serializes a public key into a PEM string.
272    pub fn to_pem(&self) -> String {
273        pem::encode("PUBLIC KEY", &self.to_der())
274    }
275
276    /// Returns a 256-bit unique identifier for this key. For Ed25519,
277    /// that is the SHA256 hash of the raw public key.
278    pub fn fingerprint(&self) -> Fingerprint {
279        let mut hasher = sha2::Sha256::new();
280        hasher.update(self.to_bytes());
281        Fingerprint(hasher.finalize().into())
282    }
283
284    /// Verifies a digital signature.
285    pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), Error> {
286        let sig = ed25519_dalek::Signature::from_bytes(&signature.to_bytes());
287        self.inner
288            .verify(message, &sig)
289            .map_err(|_| Error::InvalidSignature)
290    }
291}
292
293impl Serialize for PublicKey {
294    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
295        serializer.serialize_str(&BASE64.encode(self.to_bytes()))
296    }
297}
298
299impl<'de> Deserialize<'de> for PublicKey {
300    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
301        let s = String::deserialize(deserializer)?;
302        let bytes = BASE64.decode(&s).map_err(de::Error::custom)?;
303        let arr: [u8; PUBLIC_KEY_SIZE] = bytes
304            .try_into()
305            .map_err(|_| de::Error::custom("invalid public key length"))?;
306        PublicKey::from_bytes(&arr).map_err(de::Error::custom)
307    }
308}
309
310#[cfg(feature = "cbor")]
311impl crate::cbor::Encode for PublicKey {
312    fn encode_cbor_to(&self, buf: &mut Vec<u8>) -> Result<(), crate::cbor::Error> {
313        self.to_bytes().encode_cbor_to(buf)
314    }
315}
316
317#[cfg(feature = "cbor")]
318impl crate::cbor::Decode for PublicKey {
319    fn decode_cbor(data: &[u8]) -> Result<Self, crate::cbor::Error> {
320        let bytes = <[u8; PUBLIC_KEY_SIZE]>::decode_cbor(data)?;
321        Self::from_bytes(&bytes).map_err(|e| crate::cbor::Error::DecodeFailed(e.to_string()))
322    }
323
324    fn decode_cbor_notrail(
325        decoder: &mut crate::cbor::Decoder<'_>,
326    ) -> Result<Self, crate::cbor::Error> {
327        let bytes = decoder.decode_bytes_fixed::<PUBLIC_KEY_SIZE>()?;
328        Self::from_bytes(&bytes).map_err(|e| crate::cbor::Error::DecodeFailed(e.to_string()))
329    }
330}
331
332/// An Ed25519 signature.
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub struct Signature([u8; SIGNATURE_SIZE]);
335
336impl Signature {
337    /// Converts a 64-byte array into a signature.
338    pub fn from_bytes(bytes: &[u8; SIGNATURE_SIZE]) -> Self {
339        Self(*bytes)
340    }
341
342    /// Converts a signature into a 64-byte array.
343    pub fn to_bytes(&self) -> [u8; SIGNATURE_SIZE] {
344        self.0
345    }
346}
347
348impl Serialize for Signature {
349    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
350        serializer.serialize_str(&BASE64.encode(self.to_bytes()))
351    }
352}
353
354impl<'de> Deserialize<'de> for Signature {
355    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
356        let s = String::deserialize(deserializer)?;
357        let bytes = BASE64.decode(&s).map_err(de::Error::custom)?;
358        let arr: [u8; SIGNATURE_SIZE] = bytes
359            .try_into()
360            .map_err(|_| de::Error::custom("invalid signature length"))?;
361        Ok(Signature::from_bytes(&arr))
362    }
363}
364
365#[cfg(feature = "cbor")]
366impl crate::cbor::Encode for Signature {
367    fn encode_cbor_to(&self, buf: &mut Vec<u8>) -> Result<(), crate::cbor::Error> {
368        self.to_bytes().encode_cbor_to(buf)
369    }
370}
371
372#[cfg(feature = "cbor")]
373impl crate::cbor::Decode for Signature {
374    fn decode_cbor(data: &[u8]) -> Result<Self, crate::cbor::Error> {
375        let bytes = <[u8; SIGNATURE_SIZE]>::decode_cbor(data)?;
376        Ok(Self::from_bytes(&bytes))
377    }
378
379    fn decode_cbor_notrail(
380        decoder: &mut crate::cbor::Decoder<'_>,
381    ) -> Result<Self, crate::cbor::Error> {
382        let bytes = decoder.decode_bytes_fixed::<SIGNATURE_SIZE>()?;
383        Ok(Self::from_bytes(&bytes))
384    }
385}
386
387/// An Ed25519 key fingerprint.
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub struct Fingerprint([u8; FINGERPRINT_SIZE]);
390
391impl Fingerprint {
392    /// Converts a 32-byte array into a fingerprint.
393    pub fn from_bytes(bytes: &[u8; FINGERPRINT_SIZE]) -> Self {
394        Self(*bytes)
395    }
396
397    /// Converts a fingerprint into a 32-byte array.
398    pub fn to_bytes(&self) -> [u8; FINGERPRINT_SIZE] {
399        self.0
400    }
401}
402
403impl Serialize for Fingerprint {
404    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
405        serializer.serialize_str(&BASE64.encode(self.to_bytes()))
406    }
407}
408
409impl<'de> Deserialize<'de> for Fingerprint {
410    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
411        let s = String::deserialize(deserializer)?;
412        let bytes = BASE64.decode(&s).map_err(de::Error::custom)?;
413        let arr: [u8; FINGERPRINT_SIZE] = bytes
414            .try_into()
415            .map_err(|_| de::Error::custom("invalid fingerprint length"))?;
416        Ok(Fingerprint::from_bytes(&arr))
417    }
418}
419
420#[cfg(feature = "cbor")]
421impl crate::cbor::Encode for Fingerprint {
422    fn encode_cbor_to(&self, buf: &mut Vec<u8>) -> Result<(), crate::cbor::Error> {
423        self.to_bytes().encode_cbor_to(buf)
424    }
425}
426
427#[cfg(feature = "cbor")]
428impl crate::cbor::Decode for Fingerprint {
429    fn decode_cbor(data: &[u8]) -> Result<Self, crate::cbor::Error> {
430        let bytes = <[u8; FINGERPRINT_SIZE]>::decode_cbor(data)?;
431        Ok(Self::from_bytes(&bytes))
432    }
433
434    fn decode_cbor_notrail(
435        decoder: &mut crate::cbor::Decoder<'_>,
436    ) -> Result<Self, crate::cbor::Error> {
437        let bytes = decoder.decode_bytes_fixed::<FINGERPRINT_SIZE>()?;
438        Ok(Self::from_bytes(&bytes))
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    /// Test vectors from RFC 8410 Sections 10.1 and 10.3
447    // https://datatracker.ietf.org/doc/html/rfc8410
448    mod ietf_vectors {
449        pub const SECKEY_SEED: &str =
450            "d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842";
451
452        pub const SECKEY_PEM: &str = "\
453-----BEGIN PRIVATE KEY-----
454MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
455-----END PRIVATE KEY-----";
456
457        pub const SECKEY_V2_PEM: &str = "\
458-----BEGIN PRIVATE KEY-----
459MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
460oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB
461Z9w7lshQhqowtrbLDFw4rXAxZuE=
462-----END PRIVATE KEY-----";
463
464        pub const PUBKEY_PEM: &str = "\
465-----BEGIN PUBLIC KEY-----
466MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
467-----END PUBLIC KEY-----";
468    }
469
470    /// Tests operations with IETF test vectors: the private key must parse into
471    /// the published raw seed, re-encode into the published PEM and DER, and
472    /// derive the RFC's matching public key.
473    #[test]
474    fn test_ietf_vectors() {
475        // Round trip the secret key pem and verify the expected seed
476        let key = SecretKey::from_pem(ietf_vectors::SECKEY_PEM).unwrap();
477        assert_eq!(hex::encode(*key.to_bytes()), ietf_vectors::SECKEY_SEED);
478        assert_eq!(key.to_pem().trim(), ietf_vectors::SECKEY_PEM.trim());
479
480        // Round trip the secret key der
481        let (_, der) = pem::decode(ietf_vectors::SECKEY_PEM.as_bytes()).unwrap();
482        assert_eq!(*key.to_der(), *der);
483
484        // Verify the expected public key
485        assert_eq!(
486            key.public_key().to_pem().trim(),
487            ietf_vectors::PUBKEY_PEM.trim()
488        );
489
490        // Round trip the public key pem
491        let key = PublicKey::from_pem(ietf_vectors::PUBKEY_PEM).unwrap();
492        assert_eq!(key.to_pem().trim(), ietf_vectors::PUBKEY_PEM.trim());
493
494        // Round trip the public key der
495        let (_, der) = pem::decode(ietf_vectors::PUBKEY_PEM.as_bytes()).unwrap();
496        assert_eq!(key.to_der(), *der);
497    }
498
499    /// Tests that the RFC 8410 v2 private key vector carrying an attribute and
500    /// an embedded public key is rejected: only v1 keys without embedded public
501    /// data are supported.
502    #[test]
503    fn test_ietf_v2_rejected() {
504        assert!(SecretKey::from_pem(ietf_vectors::SECKEY_V2_PEM).is_err());
505    }
506
507    /// Tests signing and verifying messages. Note, this test is not meant to test
508    /// cryptography, it is mostly an API sanity check to verify that everything
509    /// seems to work.
510    #[test]
511    fn test_sign_verify() {
512        // Create the keys for Alice
513        let secret = SecretKey::generate();
514        let public = secret.public_key();
515
516        // Run a bunch of different authentication/encryption combinations
517        struct TestCase<'a> {
518            message: &'a [u8],
519        }
520        let tests = [TestCase {
521            message: b"message to authenticate",
522        }];
523
524        for tt in &tests {
525            // Sign the message using the test case data
526            let signature = secret.sign(tt.message);
527
528            // Verify the signature message
529            public
530                .verify(tt.message, &signature)
531                .unwrap_or_else(|e| panic!("failed to verify message: {}", e));
532        }
533    }
534
535    /// Tests that a public key whose algorithm identifier carries parameters is
536    /// rejected.
537    #[test]
538    fn test_publickey_der_rejects_params() {
539        // Rebuild a valid public key with injected NULL algorithm parameters
540        let key = SecretKey::from_bytes(&[7; 32]).public_key();
541        let der = key.to_der();
542
543        let mut info: SubjectPublicKeyInfo<AnyRef, BitStringRef> =
544            SubjectPublicKeyInfo::from_der(&der).unwrap();
545        info.algorithm.parameters = Some(AnyRef::NULL);
546        let der_bad = info.to_der().unwrap();
547
548        let err = PublicKey::from_der(&der_bad);
549        assert!(matches!(err, Err(Error::MalformedKey(_))));
550    }
551
552    /// Tests that a private key whose algorithm identifier carries parameters is
553    /// rejected.
554    #[test]
555    fn test_secretkey_der_rejects_params() {
556        // Rebuild a valid private key with NULL algorithm parameters spliced in
557        let der = SecretKey::from_bytes(&[7; 32]).to_der().to_vec();
558        let long = der[1] == 0x82;
559        let algid_pos = if long { 7 } else { 5 };
560        let algid_len = der[algid_pos + 1] as usize;
561        let oid_pos = algid_pos + 2;
562        let oid_len = 2 + der[oid_pos + 1] as usize;
563        let after_oid = oid_pos + oid_len;
564
565        let mut bad = der.clone();
566        bad.splice(after_oid..after_oid, [0x05, 0x00]);
567        bad[algid_pos + 1] = (algid_len + 2) as u8;
568        if long {
569            let grown = (((der[2] as usize) << 8) | der[3] as usize) + 2;
570            bad[2] = (grown >> 8) as u8;
571            bad[3] = (grown & 0xff) as u8;
572        } else {
573            bad[1] = (der[1] as usize + 2) as u8;
574        }
575
576        let err = SecretKey::from_der(&bad);
577        assert!(matches!(err, Err(Error::MalformedKey(_))));
578    }
579}