feat(fingerprint): land the single fingerprint path + ssh-keygen parity vectors
- src/fingerprint.rs: alkssh::fingerprint wraps the ssh-key re-export (HashAlg::Sha256) — the crate's only fingerprint computation and formatting site (ADR-004 decision 1's contract invariant) - tests/fingerprint_parity.rs: 4 committed ed25519 fixtures with ssh-keygen -lf-recorded fingerprints (OpenSSH 9.6p1, independent issuer); blobs cross-checked against the vectors manifest - verified: cargo test, clippy (default + all-features, -D warnings), fmt; taskgraph validate clean
This commit is contained in:
1 parent
d85299df38
commit
635e82b54b
9 files changed
+225
-7
No files matched your search
@@ -0,0 +1,62 @@
|
||||
//! The crate's single fingerprint path (ADR-004 decision 1).
|
||||
//!
|
||||
//! A public key's fingerprint is the SSH-shaped identity handle: the
|
||||
//! auth bridge (server) resolves it through alkcall's
|
||||
//! `IdentityProvider::resolve_from_fingerprint`, the host-key policy
|
||||
//! path (client) surfaces it at `check_server_key`, and assembly
|
||||
//! registrations store it as a string. The string format
|
||||
//! (`SSHFP`-style `SHA256:` prefix over the OpenSSH wire-key digest,
|
||||
//! unpadded base64) is a **contract invariant** — registration strings
|
||||
//! from any standard issuer (`ssh-keygen -lf`, alkcall-side
|
||||
//! computation) must compare equal to what this crate computes.
|
||||
//!
|
||||
//! Parity is proven against independent-issuer vectors
|
||||
//! (`ssh-keygen -lf`-recorded, committed under `tests/vectors/`), not
|
||||
//! assumed. There is deliberately no second formatting path in the
|
||||
//! crate: a drift here would break every identity resolution silently.
|
||||
|
||||
use russh::keys::{HashAlg, PublicKey};
|
||||
|
||||
/// Computes a public key's fingerprint — the single fingerprint path
|
||||
/// in the crate (ADR-004 decision 1's contract invariant).
|
||||
///
|
||||
/// The returned string is the OpenSSH fingerprint format: `SHA256:`
|
||||
/// followed by the unpadded base64 of the SHA-256 digest over the
|
||||
/// key's OpenSSH wire encoding, byte-identical to independent issuers
|
||||
/// (`ssh-keygen -lf`) — parity is proven, not assumed. Every
|
||||
/// fingerprint comparison in the crate and every registration value an
|
||||
/// assembly stores is this string; no other formatting path exists.
|
||||
pub fn fingerprint(public_key: &PublicKey) -> String {
|
||||
public_key.fingerprint(HashAlg::Sha256).to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::fingerprint;
|
||||
|
||||
#[test]
|
||||
fn format_shape_is_prefixed_and_unpadded() {
|
||||
let key = russh::keys::parse_public_key_base64(
|
||||
"AAAAC3NzaC1lZDI1NTE5AAAAICS7Ub4A073+G450Nt2DUOwWyniGVU/lrlUoQZAUg1sd",
|
||||
)
|
||||
.expect("fixture blob parses");
|
||||
let fp = fingerprint(&key);
|
||||
assert_eq!(fp.split(':').count(), 1 + 1);
|
||||
let (prefix, body) = fp.split_once(':').expect("single separator");
|
||||
assert_eq!(prefix, "SHA256");
|
||||
assert!(!body.contains('='), "unpadded: {fp}");
|
||||
assert!(
|
||||
!body.contains('-') && !body.contains('_'),
|
||||
"not url-safe: {fp}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determinism_same_key_same_string() {
|
||||
let key = russh::keys::parse_public_key_base64(
|
||||
"AAAAC3NzaC1lZDI1NTE5AAAAICS7Ub4A073+G450Nt2DUOwWyniGVU/lrlUoQZAUg1sd",
|
||||
)
|
||||
.expect("fixture blob parses");
|
||||
assert_eq!(fingerprint(&key), fingerprint(&key));
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,7 @@
|
||||
//! start). See also `docs/research/` and family ADR-003.
|
||||
|
||||
mod error;
|
||||
mod fingerprint;
|
||||
|
||||
pub use error::KeySourceError;
|
||||
pub use fingerprint::fingerprint;
|
||||
Reference in new issue
Block a user