feat(core): add device::fingerprint helper for SSH SHA256 fingerprints

Wraps ssh-key's PublicKey::fingerprint(HashAlg::Sha256). Output format
matches ssh-keygen -lf and git verify-commit --raw stderr
(SHA256:<43-char base64>). Used by the upcoming relicario-server
verify-commit rewrite (audit S1).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-02 16:23:10 -04:00
parent c3d8778042
commit 8a72b5e192
2 changed files with 34 additions and 1 deletions

View File

@@ -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()) 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<String> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -132,4 +142,27 @@ mod tests {
let sig = sign(&private, b"hello").unwrap(); let sig = sign(&private, b"hello").unwrap();
assert!(!verify(&other_public, b"hello", &sig).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());
}
} }

View File

@@ -85,4 +85,4 @@ pub mod import_lastpass;
pub use import_lastpass::{parse_lastpass_csv, ImportWarning}; pub use import_lastpass::{parse_lastpass_csv, ImportWarning};
pub mod device; pub mod device;
pub use device::{DeviceEntry, RevokedEntry, generate_keypair, sign, verify}; pub use device::{fingerprint, DeviceEntry, RevokedEntry, generate_keypair, sign, verify};