diff --git a/crates/relicario-core/src/device.rs b/crates/relicario-core/src/device.rs index 4d779fc..f2ce780 100644 --- a/crates/relicario-core/src/device.rs +++ b/crates/relicario-core/src/device.rs @@ -106,6 +106,16 @@ pub fn verify(public_key_openssh: &str, data: &[u8], signature_b64: &str) -> Res Ok(verifying_key.verify(data, &signature).is_ok()) } +/// Compute the OpenSSH SHA-256 fingerprint of a public key. +/// Output format matches `ssh-keygen -lf` and `git verify-commit --raw`: +/// `SHA256:<43-char base64 without padding>`. +pub fn fingerprint(public_key_openssh: &str) -> Result { + use ssh_key::HashAlg; + let public = PublicKey::from_openssh(public_key_openssh) + .map_err(|e| RelicarioError::DeviceKey(format!("parse public key: {e}")))?; + Ok(public.fingerprint(HashAlg::Sha256).to_string()) +} + #[cfg(test)] mod tests { use super::*; @@ -132,4 +142,27 @@ mod tests { let sig = sign(&private, b"hello").unwrap(); assert!(!verify(&other_public, b"hello", &sig).unwrap()); } + + #[test] + fn fingerprint_matches_ssh_keygen_format() { + let (_, public) = generate_keypair().unwrap(); + let fp = fingerprint(&public).unwrap(); + assert!(fp.starts_with("SHA256:"), "fingerprint should start with SHA256: prefix, got {fp}"); + let body = fp.strip_prefix("SHA256:").unwrap(); + assert_eq!(body.len(), 43, "SHA-256 fingerprint body is 43 base64 chars (no padding)"); + assert!(body.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/')); + } + + #[test] + fn fingerprint_is_deterministic() { + let (_, public) = generate_keypair().unwrap(); + assert_eq!(fingerprint(&public).unwrap(), fingerprint(&public).unwrap()); + } + + #[test] + fn fingerprint_differs_per_key() { + let (_, p1) = generate_keypair().unwrap(); + let (_, p2) = generate_keypair().unwrap(); + assert_ne!(fingerprint(&p1).unwrap(), fingerprint(&p2).unwrap()); + } } diff --git a/crates/relicario-core/src/lib.rs b/crates/relicario-core/src/lib.rs index 4ae324e..c687383 100644 --- a/crates/relicario-core/src/lib.rs +++ b/crates/relicario-core/src/lib.rs @@ -85,4 +85,4 @@ pub mod import_lastpass; pub use import_lastpass::{parse_lastpass_csv, ImportWarning}; pub mod device; -pub use device::{DeviceEntry, RevokedEntry, generate_keypair, sign, verify}; +pub use device::{fingerprint, DeviceEntry, RevokedEntry, generate_keypair, sign, verify};