Skip to main content

cdx_core/security/
eddsa.rs

1#![allow(clippy::doc_markdown)] // EdDSA is a proper algorithm name
2
3//! EdDSA (Ed25519) signature implementation.
4
5use crate::error::invalid_manifest;
6use crate::{DocumentId, Result};
7
8use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo};
9use super::signer::{Signer, Verifier};
10
11/// EdDSA (Ed25519) signer.
12#[cfg(feature = "eddsa")]
13pub struct EddsaSigner {
14    signing_key: ed25519_dalek::SigningKey,
15    signer_info: SignerInfo,
16}
17
18#[cfg(feature = "eddsa")]
19impl EddsaSigner {
20    /// Create a new signer from a PEM-encoded private key.
21    ///
22    /// # Errors
23    ///
24    /// Returns an error if the PEM cannot be parsed.
25    pub fn from_pem(pem: &str, signer_info: SignerInfo) -> Result<Self> {
26        use ed25519_dalek::pkcs8::DecodePrivateKey;
27
28        let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem)
29            .map_err(|e| invalid_manifest(format!("Failed to parse EdDSA private key PEM: {e}")))?;
30
31        Ok(Self {
32            signing_key,
33            signer_info,
34        })
35    }
36
37    /// Generate a new random signing key.
38    ///
39    /// Returns the signer and the public key in PEM format.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if key generation fails.
44    pub fn generate(signer_info: SignerInfo) -> Result<(Self, String)> {
45        use ed25519_dalek::pkcs8::spki::{der::pem::LineEnding, EncodePublicKey};
46
47        let mut key_bytes = [0u8; 32];
48        getrandom::fill(&mut key_bytes)
49            .map_err(|e| invalid_manifest(format!("System RNG failed: {e}")))?;
50        let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
51        let verifying_key = signing_key.verifying_key();
52        let public_key_pem = verifying_key
53            .to_public_key_pem(LineEnding::LF)
54            .map_err(|e| invalid_manifest(format!("Failed to encode EdDSA public key: {e}")))?;
55
56        Ok((
57            Self {
58                signing_key,
59                signer_info,
60            },
61            public_key_pem,
62        ))
63    }
64
65    /// Get the public key in PEM format.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if encoding fails.
70    pub fn public_key_pem(&self) -> Result<String> {
71        use ed25519_dalek::pkcs8::spki::{der::pem::LineEnding, EncodePublicKey};
72
73        self.signing_key
74            .verifying_key()
75            .to_public_key_pem(LineEnding::LF)
76            .map_err(|e| invalid_manifest(format!("Failed to encode EdDSA public key: {e}")))
77    }
78}
79
80#[cfg(feature = "eddsa")]
81impl Signer for EddsaSigner {
82    fn algorithm(&self) -> SignatureAlgorithm {
83        SignatureAlgorithm::EdDSA
84    }
85
86    fn signer_info(&self) -> SignerInfo {
87        self.signer_info.clone()
88    }
89
90    fn sign(&self, document_id: &DocumentId) -> Result<Signature> {
91        use base64::Engine;
92        use ed25519_dalek::Signer as EddsaSignerTrait;
93
94        if document_id.is_pending() {
95            return Err(crate::Error::InvalidManifest {
96                reason: "Cannot sign a pending document ID".to_string(),
97            });
98        }
99
100        // Sign the document ID bytes
101        let signature = self.signing_key.sign(document_id.digest());
102
103        // Encode as base64
104        let value = base64::engine::general_purpose::STANDARD.encode(signature.to_bytes());
105
106        // Generate signature ID
107        let sig_id = format!(
108            "sig-{}",
109            &crate::Hasher::hash(crate::HashAlgorithm::Sha256, value.as_bytes()).hex_digest()[..8]
110        );
111
112        Ok(Signature::new(
113            sig_id,
114            SignatureAlgorithm::EdDSA,
115            self.signer_info.clone(),
116            value,
117        ))
118    }
119}
120
121/// EdDSA (Ed25519) verifier.
122#[cfg(feature = "eddsa")]
123pub struct EddsaVerifier {
124    verifying_key: ed25519_dalek::VerifyingKey,
125}
126
127#[cfg(feature = "eddsa")]
128impl EddsaVerifier {
129    /// Create a new verifier from a PEM-encoded public key.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if the PEM cannot be parsed.
134    pub fn from_pem(pem: &str) -> Result<Self> {
135        use ed25519_dalek::pkcs8::DecodePublicKey;
136
137        let verifying_key = ed25519_dalek::VerifyingKey::from_public_key_pem(pem)
138            .map_err(|e| invalid_manifest(format!("Failed to parse EdDSA public key PEM: {e}")))?;
139
140        Ok(Self { verifying_key })
141    }
142}
143
144#[cfg(feature = "eddsa")]
145impl Verifier for EddsaVerifier {
146    fn verify(
147        &self,
148        document_id: &DocumentId,
149        signature: &Signature,
150    ) -> Result<SignatureVerification> {
151        use base64::Engine;
152        use ed25519_dalek::Verifier as EddsaVerifierTrait;
153
154        if signature.algorithm != SignatureAlgorithm::EdDSA {
155            return Ok(SignatureVerification::invalid(
156                &signature.id,
157                format!(
158                    "Algorithm mismatch: expected EdDSA, got {}",
159                    signature.algorithm
160                ),
161            ));
162        }
163
164        // Decode signature from base64
165        let sig_bytes = base64::engine::general_purpose::STANDARD
166            .decode(&signature.value)
167            .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?;
168
169        // Parse signature
170        let sig_array: [u8; 64] =
171            sig_bytes
172                .try_into()
173                .map_err(|_| crate::Error::InvalidManifest {
174                    reason: "Invalid EdDSA signature length (expected 64 bytes)".to_string(),
175                })?;
176        let eddsa_sig = ed25519_dalek::Signature::from_bytes(&sig_array);
177
178        // Verify
179        match self.verifying_key.verify(document_id.digest(), &eddsa_sig) {
180            Ok(()) => Ok(SignatureVerification::valid(&signature.id)),
181            Err(e) => Ok(SignatureVerification::invalid(
182                &signature.id,
183                format!("EdDSA signature verification failed: {e}"),
184            )),
185        }
186    }
187}
188
189#[cfg(all(test, feature = "eddsa"))]
190mod tests {
191    use super::*;
192    use crate::security::test_helpers;
193
194    fn generate_keypair() -> (EddsaSigner, EddsaVerifier) {
195        let signer_info = SignerInfo::new("Test EdDSA Signer");
196        let (signer, public_key_pem) = EddsaSigner::generate(signer_info).unwrap();
197        let verifier = EddsaVerifier::from_pem(&public_key_pem).unwrap();
198        (signer, verifier)
199    }
200
201    #[test]
202    fn test_generate_and_sign() {
203        let signer_info = SignerInfo::new("Test EdDSA Signer");
204        let (signer, public_key_pem) = EddsaSigner::generate(signer_info).unwrap();
205
206        assert!(!public_key_pem.is_empty());
207        assert!(public_key_pem.contains("BEGIN PUBLIC KEY"));
208
209        test_helpers::assert_sign_produces_valid_signature(&signer, SignatureAlgorithm::EdDSA);
210    }
211
212    #[test]
213    fn test_sign_and_verify() {
214        let (signer, verifier) = generate_keypair();
215        test_helpers::assert_sign_verify_roundtrip(&signer, &verifier);
216    }
217
218    #[test]
219    fn test_verify_wrong_document() {
220        let (signer, verifier) = generate_keypair();
221        test_helpers::assert_verify_wrong_document_fails(&signer, &verifier);
222    }
223
224    #[test]
225    fn test_cannot_sign_pending_id() {
226        let (signer, _) = generate_keypair();
227        test_helpers::assert_cannot_sign_pending_id(&signer);
228    }
229
230    #[test]
231    fn test_algorithm_mismatch() {
232        let (signer, verifier) = generate_keypair();
233        test_helpers::assert_algorithm_mismatch_rejected(
234            &signer,
235            &verifier,
236            SignatureAlgorithm::ES256,
237        );
238    }
239}