cdx_core/security/
eddsa.rs1#![allow(clippy::doc_markdown)] use crate::error::invalid_manifest;
6use crate::{DocumentId, Result};
7
8use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo};
9use super::signer::{Signer, Verifier};
10
11#[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 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 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 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 let signature = self.signing_key.sign(document_id.digest());
102
103 let value = base64::engine::general_purpose::STANDARD.encode(signature.to_bytes());
105
106 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#[cfg(feature = "eddsa")]
123pub struct EddsaVerifier {
124 verifying_key: ed25519_dalek::VerifyingKey,
125}
126
127#[cfg(feature = "eddsa")]
128impl EddsaVerifier {
129 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 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 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 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}