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:
glm-5.3-flash committed 2026-10-28 06:27:49 +00:00
1 parent d85299df38
commit 635e82b54b
9 files changed
+225 -7

No files matched your search

+62
View File
@@ -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));
}
}
+2
View File
@@ -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;