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:
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
Reference in New Issue
Block a user