1use std::sync::Arc;
41
42use ed25519_dalek::pkcs8::DecodePrivateKey as Ed25519DecodePrivateKey;
43use secretx_core::{SecretError, SecretUri, SigningAlgorithm, SigningBackend};
44
45const BACKEND: &str = "local-signing";
46use signature::{SignatureEncoding, Signer};
47use zeroize::Zeroizing;
48
49fn read_file_zeroizing(path: &str) -> std::io::Result<Zeroizing<Vec<u8>>> {
52 use std::io::Read;
53 let mut f = std::fs::File::open(path)?;
54 let len = f.metadata().map(|m| m.len() as usize).unwrap_or(0);
55 let mut buf = Zeroizing::new(Vec::with_capacity(len));
56 f.read_to_end(&mut buf)?;
57 Ok(buf)
58}
59
60enum LocalKey {
63 Ed25519(ed25519_dalek::SigningKey),
64 P256(p256::ecdsa::SigningKey),
65 RsaPss2048(std::sync::Arc<rsa::pss::SigningKey<sha2::Sha256>>),
72}
73
74pub struct LocalSigningBackend {
80 inner: LocalKey,
81 algorithm: SigningAlgorithm,
82}
83
84impl std::fmt::Debug for LocalSigningBackend {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("LocalSigningBackend")
87 .field("algorithm", &self.algorithm)
88 .finish_non_exhaustive()
89 }
90}
91
92impl LocalSigningBackend {
93 pub fn from_uri(uri: &str) -> Result<Self, SecretError> {
110 Self::from_parsed_uri(&SecretUri::parse(uri)?)
111 }
112
113 pub fn from_parsed_uri(parsed: &SecretUri) -> Result<Self, SecretError> {
115 if parsed.backend() != BACKEND {
116 return Err(SecretError::InvalidUri(format!(
117 "expected backend `local-signing`, got `{}`",
118 parsed.backend()
119 )));
120 }
121 if parsed.path().is_empty() {
122 return Err(SecretError::InvalidUri(
123 "local-signing URI requires a key path: \
124 secretx:local-signing:<path>?algorithm=<algo>"
125 .into(),
126 ));
127 }
128 let key_path = std::path::PathBuf::from(parsed.path());
130 if key_path
131 .components()
132 .any(|c| c == std::path::Component::ParentDir)
133 {
134 return Err(SecretError::InvalidUri(
135 "local-signing URI path must not contain '..' components".into(),
136 ));
137 }
138 for key in parsed.param_keys() {
140 if key != "algorithm" {
141 return Err(SecretError::InvalidUri(format!(
142 "local-signing URI: unknown query parameter `{key}`; \
143 only `?algorithm=` is supported"
144 )));
145 }
146 }
147
148 let algo_str = parsed.param("algorithm");
149
150 if let Some(a) = algo_str {
153 validate_algorithm(a)?;
154 }
155
156 let key_bytes: Zeroizing<Vec<u8>> = read_file_zeroizing(parsed.path())
160 .map_err(|e| match e.kind() {
161 std::io::ErrorKind::NotFound => SecretError::NotFound,
162 _ => SecretError::Backend {
163 backend: BACKEND,
164 source: e.into(),
165 },
166 })?;
167
168 let (inner, algorithm) = match algo_str {
169 Some(a) => parse_key(a, &key_bytes)?,
170 None => detect_key(&key_bytes)?,
171 };
172 Ok(Self { inner, algorithm })
173 }
174}
175
176fn validate_algorithm(algo_str: &str) -> Result<(), SecretError> {
177 match algo_str {
178 "ed25519" | "p256" | "rsa-pss-2048" => Ok(()),
179 other => Err(SecretError::InvalidUri(format!(
180 "unknown algorithm `{other}`; supported: ed25519, p256, rsa-pss-2048"
181 ))),
182 }
183}
184
185fn parse_key(
186 algo_str: &str,
187 key_bytes: &[u8],
188) -> Result<(LocalKey, SigningAlgorithm), SecretError> {
189 match algo_str {
190 "ed25519" => {
191 let key = ed25519_dalek::SigningKey::from_pkcs8_der(key_bytes).map_err(|e| {
192 SecretError::Backend {
193 backend: BACKEND,
194 source: format!("ed25519 PKCS#8 parse error: {e}").into(),
195 }
196 })?;
197 Ok((LocalKey::Ed25519(key), SigningAlgorithm::Ed25519))
198 }
199 "p256" => {
200 use p256::pkcs8::DecodePrivateKey as _;
201 let key = p256::ecdsa::SigningKey::from_pkcs8_der(key_bytes).map_err(|e| {
202 SecretError::Backend {
203 backend: BACKEND,
204 source: format!("P-256 PKCS#8 parse error: {e}").into(),
205 }
206 })?;
207 Ok((LocalKey::P256(key), SigningAlgorithm::EcdsaP256Sha256))
208 }
209 "rsa-pss-2048" => {
210 use pkcs8::DecodePrivateKey as _;
211 let key = rsa::RsaPrivateKey::from_pkcs8_der(key_bytes).map_err(|e| {
212 SecretError::Backend {
213 backend: BACKEND,
214 source: format!("RSA PKCS#8 parse error: {e}").into(),
215 }
216 })?;
217 let signing_key = std::sync::Arc::new(rsa::pss::SigningKey::<sha2::Sha256>::new(key));
220 Ok((
221 LocalKey::RsaPss2048(signing_key),
222 SigningAlgorithm::RsaPss2048Sha256,
223 ))
224 }
225 other => unreachable!("algorithm `{other}` was already rejected by validate_algorithm"),
228 }
229}
230
231fn detect_key(key_bytes: &[u8]) -> Result<(LocalKey, SigningAlgorithm), SecretError> {
238 if let Ok(key) = ed25519_dalek::SigningKey::from_pkcs8_der(key_bytes) {
240 return Ok((LocalKey::Ed25519(key), SigningAlgorithm::Ed25519));
241 }
242 {
244 use p256::pkcs8::DecodePrivateKey as _;
245 if let Ok(key) = p256::ecdsa::SigningKey::from_pkcs8_der(key_bytes) {
246 return Ok((LocalKey::P256(key), SigningAlgorithm::EcdsaP256Sha256));
247 }
248 }
249 {
251 use pkcs8::DecodePrivateKey as _;
252 if let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_der(key_bytes) {
253 let signing_key =
254 std::sync::Arc::new(rsa::pss::SigningKey::<sha2::Sha256>::new(key));
255 return Ok((
256 LocalKey::RsaPss2048(signing_key),
257 SigningAlgorithm::RsaPss2048Sha256,
258 ));
259 }
260 }
261
262 Err(SecretError::Backend {
263 backend: BACKEND,
264 source: "key file is not valid PKCS#8 DER for any supported algorithm \
265 (ed25519, p256, rsa-pss-2048)"
266 .into(),
267 })
268}
269
270#[async_trait::async_trait]
273impl SigningBackend for LocalSigningBackend {
274 async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SecretError> {
275 match &self.inner {
276 LocalKey::Ed25519(key) => {
277 let sig: ed25519_dalek::Signature = key.sign(message);
278 Ok(sig.to_bytes().to_vec())
279 }
280 LocalKey::P256(key) => {
281 let sig: p256::ecdsa::Signature = key.sign(message);
282 Ok(sig.to_bytes().to_vec())
283 }
284 LocalKey::RsaPss2048(signing_key) => {
285 let key_clone = std::sync::Arc::clone(signing_key);
293 let msg = message.to_vec();
294 tokio::task::spawn_blocking(move || -> Vec<u8> {
295 let sig: rsa::pss::Signature = key_clone.sign(&msg);
296 sig.to_bytes().to_vec()
297 })
298 .await
299 .map_err(|e| SecretError::Backend {
300 backend: BACKEND,
301 source: format!("RSA sign task panicked: {e}").into(),
302 })
303 }
304 }
305 }
306
307 async fn public_key_der(&self) -> Result<Vec<u8>, SecretError> {
308 use pkcs8::EncodePublicKey;
309 match &self.inner {
310 LocalKey::Ed25519(key) => {
311 let vk = key.verifying_key();
312 vk.to_public_key_der()
313 .map(|d| d.to_vec())
314 .map_err(|e| SecretError::Backend {
315 backend: BACKEND,
316 source: format!("Ed25519 public key encode error: {e}").into(),
317 })
318 }
319 LocalKey::P256(key) => {
320 let vk = key.verifying_key();
321 vk.to_public_key_der()
322 .map(|d| d.to_vec())
323 .map_err(|e| SecretError::Backend {
324 backend: BACKEND,
325 source: format!("P-256 public key encode error: {e}").into(),
326 })
327 }
328 LocalKey::RsaPss2048(signing_key) => {
329 use rsa::pkcs8::EncodePublicKey as RsaEncodePublicKey;
330 let sk: &rsa::pss::SigningKey<sha2::Sha256> = signing_key;
335 let priv_key = <rsa::pss::SigningKey<_> as AsRef<rsa::RsaPrivateKey>>::as_ref(sk);
336 let pub_key = rsa::RsaPublicKey::from(priv_key);
337 pub_key
338 .to_public_key_der()
339 .map(|d| d.to_vec())
340 .map_err(|e| SecretError::Backend {
341 backend: BACKEND,
342 source: format!("RSA public key encode error: {e}").into(),
343 })
344 }
345 }
346 }
347
348 fn algorithm(&self) -> Result<SigningAlgorithm, SecretError> {
349 Ok(self.algorithm)
350 }
351}
352
353inventory::submit!(secretx_core::SigningBackendRegistration::new(
354 "local-signing",
355 |uri: &secretx_core::SecretUri| {
356 let b = LocalSigningBackend::from_parsed_uri(uri)?;
357 Ok(Arc::new(b) as Arc<dyn secretx_core::SigningBackend>)
358 },
359));
360
361#[cfg(test)]
364mod tests {
365 use super::*;
366
367 use ed25519_dalek::pkcs8::DecodePublicKey as Ed25519DecodePublicKey;
368 use ed25519_dalek::Verifier as Ed25519Verifier;
369 use p256::ecdsa::{signature::Verifier as P256Verifier, Signature as P256Signature, VerifyingKey as P256VerifyingKey};
370 use p256::pkcs8::DecodePublicKey as P256DecodePublicKey;
371 use rsa::pkcs8::DecodePublicKey as RsaDecodePublicKey;
372 use rsa::pss::VerifyingKey as RsaPssVerifyingKey;
373 use rsa::signature::Verifier as RsaVerifier;
374
375 const ED25519_KEY: &str = "/tmp/secretx-test-keys/ed25519.der";
378 const P256_KEY: &str = "/tmp/secretx-test-keys/p256.der";
379 const RSA_KEY: &str = "/tmp/secretx-test-keys/rsa.der";
380
381 fn ed25519_uri() -> String {
382 format!("secretx:local-signing:{ED25519_KEY}?algorithm=ed25519")
383 }
384 fn p256_uri() -> String {
385 format!("secretx:local-signing:{P256_KEY}?algorithm=p256")
386 }
387 fn rsa_uri() -> String {
388 format!("secretx:local-signing:{RSA_KEY}?algorithm=rsa-pss-2048")
389 }
390
391 macro_rules! load_or_skip {
407 ($uri:expr) => {
408 match LocalSigningBackend::from_uri(&$uri) {
409 Ok(b) => b,
410 Err(SecretError::NotFound) => return, Err(e) => panic!("from_uri failed unexpectedly: {e}"),
412 }
413 };
414 }
415
416 #[test]
419 fn from_uri_wrong_backend() {
420 assert!(matches!(
421 LocalSigningBackend::from_uri("secretx:file:/tmp/key.der?algorithm=ed25519"),
422 Err(SecretError::InvalidUri(_))
423 ));
424 }
425
426 #[test]
427 fn from_uri_missing_path() {
428 assert!(matches!(
429 LocalSigningBackend::from_uri("secretx:local-signing?algorithm=ed25519"),
430 Err(SecretError::InvalidUri(_))
431 ));
432 }
433
434 #[test]
435 fn from_uri_omitted_algorithm_auto_detects() {
436 assert!(matches!(
439 LocalSigningBackend::from_uri("secretx:local-signing:/tmp/nonexistent-key.der"),
440 Err(SecretError::NotFound)
441 ));
442 }
443
444 #[test]
445 fn from_uri_unknown_algorithm() {
446 assert!(matches!(
448 LocalSigningBackend::from_uri("secretx:local-signing:/tmp/key.der?algorithm=elgamal"),
449 Err(SecretError::InvalidUri(_))
450 ));
451 }
452
453 #[test]
454 fn from_uri_path_traversal_rejected() {
455 assert!(matches!(
456 LocalSigningBackend::from_uri(
457 "secretx:local-signing:../../etc/shadow?algorithm=ed25519"
458 ),
459 Err(SecretError::InvalidUri(_))
460 ));
461 }
462
463 #[test]
467 fn from_uri_nonexistent_file_returns_not_found() {
468 let result = LocalSigningBackend::from_uri(
469 "secretx:local-signing:/nonexistent/path/that/will/never/exist.der?algorithm=ed25519",
470 );
471 assert!(
472 matches!(result, Err(SecretError::NotFound)),
473 "missing key file must return NotFound"
474 );
475 }
476
477 #[tokio::test]
480 async fn ed25519_loads_and_signs() {
481 let backend = load_or_skip!(ed25519_uri());
482 assert_eq!(
483 backend.algorithm().expect("algorithm"),
484 SigningAlgorithm::Ed25519
485 );
486
487 let message = b"hello, ed25519";
488 let sig_bytes = backend.sign(message).await.expect("sign failed");
489 assert_eq!(sig_bytes.len(), 64, "Ed25519 signature must be 64 bytes");
490
491 let pub_der = backend
494 .public_key_der()
495 .await
496 .expect("public_key_der failed");
497 assert!(!pub_der.is_empty());
498
499 let vk = <ed25519_dalek::VerifyingKey as Ed25519DecodePublicKey>::from_public_key_der(&pub_der)
501 .expect("VerifyingKey from DER failed");
502 let sig = ed25519_dalek::Signature::from_bytes(
503 sig_bytes.as_slice().try_into().expect("sig length wrong"),
504 );
505 Ed25519Verifier::verify(&vk, message, &sig)
506 .expect("Ed25519 signature verification failed");
507 }
508
509 #[tokio::test]
510 async fn ed25519_different_messages_differ() {
511 let backend = load_or_skip!(ed25519_uri());
512 let s1 = backend.sign(b"message one").await.unwrap();
513 let s2 = backend.sign(b"message two").await.unwrap();
514 assert_ne!(s1, s2);
515 }
516
517 #[tokio::test]
520 async fn p256_loads_and_signs() {
521 let backend = load_or_skip!(p256_uri());
522 assert_eq!(
523 backend.algorithm().expect("algorithm"),
524 SigningAlgorithm::EcdsaP256Sha256
525 );
526
527 let message = b"hello, p256";
528 let sig_bytes = backend.sign(message).await.expect("sign failed");
529 assert!(!sig_bytes.is_empty(), "P-256 signature must not be empty");
530
531 let pub_der = backend
532 .public_key_der()
533 .await
534 .expect("public_key_der failed");
535 assert!(!pub_der.is_empty());
536
537 let vk = <P256VerifyingKey as P256DecodePublicKey>::from_public_key_der(&pub_der)
539 .expect("P-256 VerifyingKey from DER failed");
540 let sig = P256Signature::from_bytes(sig_bytes.as_slice().into())
541 .expect("P-256 Signature decode failed");
542 P256Verifier::verify(&vk, message, &sig)
543 .expect("P-256 signature verification failed");
544 }
545
546 #[tokio::test]
547 async fn p256_different_messages_differ() {
548 let backend = load_or_skip!(p256_uri());
549 let s1 = backend.sign(b"message one").await.unwrap();
550 let s2 = backend.sign(b"message two").await.unwrap();
551 assert_ne!(s1, s2);
552 }
553
554 #[tokio::test]
557 async fn rsa_pss_loads_and_signs() {
558 let backend = load_or_skip!(rsa_uri());
559 assert_eq!(
560 backend.algorithm().expect("algorithm"),
561 SigningAlgorithm::RsaPss2048Sha256
562 );
563
564 let message = b"hello, rsa-pss";
565 let sig_bytes = backend.sign(message).await.expect("sign failed");
566 assert_eq!(
567 sig_bytes.len(),
568 256,
569 "RSA-2048 PSS signature must be 256 bytes"
570 );
571
572 let pub_der = backend
573 .public_key_der()
574 .await
575 .expect("public_key_der failed");
576 assert!(!pub_der.is_empty());
577
578 let pub_key = <rsa::RsaPublicKey as RsaDecodePublicKey>::from_public_key_der(&pub_der)
580 .expect("RSA public key from DER failed");
581 let vk = RsaPssVerifyingKey::<sha2::Sha256>::new(pub_key);
582 let sig = rsa::pss::Signature::try_from(sig_bytes.as_slice())
583 .expect("RSA-PSS Signature decode failed");
584 RsaVerifier::verify(&vk, message, &sig)
585 .expect("RSA-PSS signature verification failed");
586 }
587
588 #[tokio::test]
589 async fn rsa_pss_different_messages_differ() {
590 let backend = load_or_skip!(rsa_uri());
591 let s1 = backend.sign(b"message one").await.unwrap();
592 let s2 = backend.sign(b"message two").await.unwrap();
593 assert_ne!(s1, s2);
594 }
595
596 #[tokio::test]
598 async fn rsa_pss_same_message_randomized() {
599 let backend = load_or_skip!(rsa_uri());
600 let s1 = backend.sign(b"same message").await.unwrap();
601 let s2 = backend.sign(b"same message").await.unwrap();
602 assert_ne!(s1, s2, "RSA-PSS should produce randomized signatures");
604 }
605
606 #[tokio::test]
609 async fn ed25519_public_key_der_stable() {
610 let b = load_or_skip!(ed25519_uri());
611 let d1 = b.public_key_der().await.unwrap();
612 let d2 = b.public_key_der().await.unwrap();
613 assert_eq!(d1, d2);
614 }
615}