feat(auth): land the ADR-004 dispositional auth bridge (w2-auth-bridge)
- auth_publickey: ed25519-only gate → single fingerprint path → resolve_from_fingerprint → accept-with-identity (stored per- connection, read by wave 3) / reject fail-closed; the gate is explicit-over-inherited (ADR-002 decision 4) and precedes resolution - auth_publickey_offered: explicit accept-and-delegate (counting stays honest, POC-1) - auth_none / auth_password / auth_keyboard_interactive / auth_openssh_certificate: four explicit family-contract declines - username carried by no hook — resolution is fingerprint-only (OQ-03) - SessionHandler constructible via #[doc(hidden)] re-export (the w2-server-mount note's widening); resolve_bridge structured out of the #[non_exhaustive]-arg hooks so contract tests drive the bridge core directly - tests/auth_bridge.rs: 21 rows — wire rows over the real adapter mount (accept, fail-closed reject, username independence, revoke between attempts, none/password/certificate declines) + direct rows over the wave-1 doubles - cert fixture (throwaway ca + alpha-cert.pub) recorded in fingerprints.txt; MemHostKeySource double in tests/common Verification: cargo test (76 across the suite), --all-features, clippy --all-targets (both feature sets, -D warnings), fmt --check, doc --no-deps, taskgraph validate — all green
This commit is contained in:
1 parent
b90e89b600
commit
2202149fc0
9 files changed
+826
-34
No files matched your search
@@ -0,0 +1,474 @@
|
||||
//! The wave-2 auth-bridge contract tests (`w2-auth-bridge`) — the
|
||||
//! ADR-004 dispositions at the handler level and over the wire.
|
||||
//!
|
||||
//! Two vehicles, each for a structural reason:
|
||||
//!
|
||||
//! - **Wire rows** (the adapter's tokio-duplex mount + russh's own
|
||||
//! client, the `w2-server-mount` wire-smoke precedent) drive the full
|
||||
//! `auth_publickey` hook end to end — the hook's
|
||||
//! `VerifiedPublicKey`/`PublicKeyOffer` argument shapes are
|
||||
//! `#[non_exhaustive]` (only russh constructs them, post-verification),
|
||||
//! so wire traffic is the only way to observe the full hook — exactly
|
||||
//! where POC-1 observed it. These rows are the thin in-process
|
||||
//! precursor of the dedicated `w2-loopback-harness` vehicle (which
|
||||
//! records the transport choice and the helpers' vocabulary).
|
||||
//! - **Direct-hook rows** (the wave-1 doubles against
|
||||
//! [`alkssh::SessionHandler`] constructed directly) drive every hook
|
||||
//! whose arguments are constructible outside russh — the four
|
||||
//! non-publickey declines, and the bridge core
|
||||
//! ([`alkssh::SessionHandler::resolve_bridge`], structured out of the
|
||||
//! `#[non_exhaustive]`-arg hook on purpose) with fixture keys +
|
||||
//! `MemIdentityProvider`. The non-constructible-arg hooks
|
||||
//! (`auth_publickey`, `auth_publickey_offered`) are the wire rows'
|
||||
//! business.
|
||||
//!
|
||||
//! The rows are contract stamps: each names the pinning ADR § (the
|
||||
//! greppable `Contract stamp:` inventory applies).
|
||||
|
||||
mod common;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use alkcall::core::auth::IdentityProvider;
|
||||
use common::{FIXTURE_KEYS, MemHostKeySource, MemIdentityProvider, fixture, identity};
|
||||
use russh::client::AuthResult;
|
||||
use russh::keys::{Algorithm, Certificate, PrivateKeyWithHashAlg};
|
||||
use russh::server::{Auth, Handler};
|
||||
|
||||
use alkssh::{AdapterError, Limits, ServeConfig, SessionHandler, SshAdapter, fingerprint};
|
||||
use alkssh::{HostKeySource, KeySourceError};
|
||||
|
||||
/// The client-side handler double for the wire rows — accept every
|
||||
/// host key (the test choice; the crate never embeds this posture).
|
||||
struct AcceptAllHostKeys;
|
||||
|
||||
impl russh::client::Handler for AcceptAllHostKeys {
|
||||
type Error = russh::Error;
|
||||
|
||||
async fn check_server_key(
|
||||
&mut self,
|
||||
_server_public_key: &russh::keys::PublicKeyOrCertificate,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mounts the real adapter stack over an in-process duplex pair and
|
||||
/// connects a russh client to it — the wire rows' vehicle.
|
||||
async fn mount(
|
||||
provider: Arc<MemIdentityProvider>,
|
||||
limits: Limits,
|
||||
) -> (
|
||||
russh::client::Handle<AcceptAllHostKeys>,
|
||||
tokio::task::JoinHandle<Result<(), AdapterError>>,
|
||||
) {
|
||||
let mut serve_config = ServeConfig::new(
|
||||
Arc::clone(&provider) as Arc<dyn IdentityProvider>,
|
||||
Arc::new(MemHostKeySource::from_fixtures(&[fixture("bravo")])),
|
||||
);
|
||||
serve_config.limits = limits;
|
||||
|
||||
let adapter = SshAdapter::new(serve_config)
|
||||
.await
|
||||
.expect("adapter constructs (host keys loaded)");
|
||||
let (client_io, server_io) = tokio::io::duplex(65_536);
|
||||
let server_task = tokio::spawn(async move { adapter.handle(server_io).await });
|
||||
|
||||
let client_config = Arc::new(russh::client::Config {
|
||||
inactivity_timeout: Some(Duration::from_secs(30)),
|
||||
..Default::default()
|
||||
});
|
||||
let client = russh::client::connect_stream(client_config, client_io, AcceptAllHostKeys)
|
||||
.await
|
||||
.expect("client mounts against the server stack");
|
||||
(client, server_task)
|
||||
}
|
||||
|
||||
/// Ends one wire row cleanly: disconnect, then the server task's end.
|
||||
async fn unmount(
|
||||
client: russh::client::Handle<AcceptAllHostKeys>,
|
||||
server_task: tokio::task::JoinHandle<Result<(), AdapterError>>,
|
||||
) {
|
||||
client
|
||||
.disconnect(russh::Disconnect::ByApplication, "row done", "en")
|
||||
.await
|
||||
.expect("client disconnects cleanly");
|
||||
server_task
|
||||
.await
|
||||
.expect("server task ends")
|
||||
.expect("clean session end");
|
||||
}
|
||||
|
||||
fn registered_provider(fingerprints: &[&str]) -> Arc<MemIdentityProvider> {
|
||||
let provider = Arc::new(MemIdentityProvider::new());
|
||||
for (index, registered) in fingerprints.iter().enumerate() {
|
||||
provider.register(registered, identity(&format!("user-{index}"), &["ssh"]));
|
||||
}
|
||||
provider
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint →
|
||||
/// `resolve_from_fingerprint` → `Some` → `Auth::Accept` — the bridge
|
||||
/// accepts the identity's key with the arbitrary username attached).
|
||||
#[tokio::test]
|
||||
async fn known_fingerprint_accepts_over_the_wire() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
|
||||
let result = client
|
||||
.authenticate_publickey(
|
||||
"anyone",
|
||||
PrivateKeyWithHashAlg::new(Arc::new(fixture("alpha").private_key()), None),
|
||||
)
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
"registered fingerprint must accept: {result:?}"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 5 + OQ-03 (the username is not
|
||||
/// identity and carries no semantics — keys resolve independently of
|
||||
/// the username string, POC-1's `ssh anything@host` observation).
|
||||
#[tokio::test]
|
||||
async fn username_is_never_an_identity_input() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
|
||||
for username in ["anyone", "git", "root", "alkforge"] {
|
||||
let (mut client, server_task) = mount(Arc::clone(&provider), Limits::default()).await;
|
||||
let result = client
|
||||
.authenticate_publickey(
|
||||
username,
|
||||
PrivateKeyWithHashAlg::new(Arc::new(fixture("alpha").private_key()), None),
|
||||
)
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
assert!(
|
||||
result.success(),
|
||||
"every username authenticates identically — {username}: {result:?}"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject — the
|
||||
/// fail-closed path never creates a weakly authorized session) and
|
||||
/// the advertisement honesty (ADR-004 decision 4 — exactly
|
||||
/// `{publickey}` remains advertised on the decline).
|
||||
#[tokio::test]
|
||||
async fn unknown_fingerprint_rejects_fail_closed() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
|
||||
let result = client
|
||||
.authenticate_publickey(
|
||||
"anyone",
|
||||
PrivateKeyWithHashAlg::new(Arc::new(fixture("delta").private_key()), None),
|
||||
)
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
|
||||
let remaining = match &result {
|
||||
AuthResult::Failure {
|
||||
remaining_methods, ..
|
||||
} => remaining_methods.clone(),
|
||||
other => panic!("unregistered fingerprint must reject, got {other:?}"),
|
||||
};
|
||||
assert!(!result.success());
|
||||
assert_eq!(
|
||||
remaining,
|
||||
russh::MethodSet::from(&[russh::MethodKind::PublicKey][..]),
|
||||
"the publickey-only advertisement survives the decline"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (resolution happens at auth
|
||||
/// time — a post-resolution revocation rejects the *next* attempt;
|
||||
/// nothing is cached into a session credential).
|
||||
#[tokio::test]
|
||||
async fn resolved_fingerprint_can_be_revoked_between_attempts() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(Arc::clone(&provider), Limits::default()).await;
|
||||
|
||||
let first = client
|
||||
.authenticate_publickey(
|
||||
"anyone",
|
||||
PrivateKeyWithHashAlg::new(Arc::new(fixture("alpha").private_key()), None),
|
||||
)
|
||||
.await
|
||||
.expect("auth exchange completes");
|
||||
assert!(first.success(), "registered fingerprint accepts: {first:?}");
|
||||
unmount(client, server_task).await;
|
||||
|
||||
provider.revoke(fixture("alpha").fingerprint);
|
||||
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
let second = client
|
||||
.authenticate_publickey(
|
||||
"anyone",
|
||||
PrivateKeyWithHashAlg::new(Arc::new(fixture("alpha").private_key()), None),
|
||||
)
|
||||
.await
|
||||
.expect("auth exchange completes");
|
||||
assert!(!second.success(), "revoked fingerprint rejects: {second:?}");
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 3 (`auth_none` is an explicit
|
||||
/// family-contract decline — non-advertised methods are never
|
||||
/// *advertised*, but the disposition is every attempted method's own
|
||||
/// hook, not the advertisement's).
|
||||
#[tokio::test]
|
||||
async fn auth_none_rejects_over_the_wire() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
|
||||
let result = client
|
||||
.authenticate_none("anyone")
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
assert!(
|
||||
!result.success(),
|
||||
"the none method must never authenticate: {result:?}"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 3 (`auth_password` explicit
|
||||
/// decline; ADR-005's key custody is the whole client-side story).
|
||||
#[tokio::test]
|
||||
async fn auth_password_rejects_over_the_wire() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
|
||||
let result = client
|
||||
.authenticate_password("anyone", "whatever")
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
assert!(
|
||||
!result.success(),
|
||||
"the password method must never authenticate: {result:?}"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 3 + OQ-04 (the OpenSSH
|
||||
/// certificate decline is the standing disposition — no CA-trust
|
||||
/// story exists in the family identity model).
|
||||
#[tokio::test]
|
||||
async fn auth_openssh_certificate_rejects_over_the_wire() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let (mut client, server_task) = mount(provider, Limits::default()).await;
|
||||
|
||||
let cert =
|
||||
Certificate::from_openssh(include_str!("vectors/alpha-cert.pub")).expect("cert fixture");
|
||||
let result = client
|
||||
.authenticate_openssh_cert("anyone", Arc::new(fixture("alpha").private_key()), cert)
|
||||
.await
|
||||
.expect("auth exchange completes over the wire");
|
||||
assert!(
|
||||
!result.success(),
|
||||
"an OpenSSH certificate must never authenticate: {result:?}"
|
||||
);
|
||||
unmount(client, server_task).await;
|
||||
}
|
||||
|
||||
fn handler_with(provider: Arc<MemIdentityProvider>) -> SessionHandler {
|
||||
SessionHandler::new(Arc::new(ServeConfig::new(
|
||||
provider as Arc<dyn IdentityProvider>,
|
||||
Arc::new(MemHostKeySource::absent()),
|
||||
)))
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 3 (each non-publickey method is
|
||||
/// an *implemented* decline, never the inherited default — the
|
||||
/// dispositions are family contract, and each one is its own hook).
|
||||
#[tokio::test]
|
||||
async fn every_non_publickey_hook_rejects_explicitly() {
|
||||
let mut handler = handler_with(registered_provider(&[fixture("alpha").fingerprint]));
|
||||
|
||||
let none = handler.auth_none("anyone").await.expect("hook resolves");
|
||||
assert_eq!(none, Auth::reject(), "auth_none must decline");
|
||||
|
||||
let password = handler
|
||||
.auth_password("anyone", "whatever")
|
||||
.await
|
||||
.expect("hook resolves");
|
||||
assert_eq!(password, Auth::reject(), "auth_password must decline");
|
||||
|
||||
let keyboard = handler
|
||||
.auth_keyboard_interactive("anyone", "", None)
|
||||
.await
|
||||
.expect("hook resolves");
|
||||
assert_eq!(
|
||||
keyboard,
|
||||
Auth::reject(),
|
||||
"auth_keyboard_interactive must decline"
|
||||
);
|
||||
|
||||
let cert = Certificate::from_openssh(include_str!("vectors/alpha-cert.pub"))
|
||||
.expect("cert fixture parses");
|
||||
let certificate = handler
|
||||
.auth_openssh_certificate("anyone", &cert)
|
||||
.await
|
||||
.expect("hook resolves");
|
||||
assert_eq!(
|
||||
certificate,
|
||||
Auth::reject(),
|
||||
"auth_openssh_certificate must decline"
|
||||
);
|
||||
|
||||
assert!(
|
||||
handler.resolved_identity().is_none(),
|
||||
"no decline path may leave identity state behind"
|
||||
);
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint →
|
||||
/// `resolve_from_fingerprint` → `Some`, the single resolution shape,
|
||||
/// with the crate's single fingerprint path computing the handle the
|
||||
/// doubles registered).
|
||||
#[tokio::test]
|
||||
async fn bridge_core_resolves_registered_fingerprints() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let handler = handler_with(provider);
|
||||
let key = fixture("alpha").public_key();
|
||||
|
||||
let resolved = handler
|
||||
.resolve_bridge(&key, &Algorithm::Ed25519)
|
||||
.expect("registered fingerprint resolves");
|
||||
assert_eq!(resolved.id, "user-0", "the identity rides to the handler");
|
||||
assert!(
|
||||
handler.resolved_identity().is_none(),
|
||||
"resolve_bridge only resolves — the hook stores; an unwired bridge leaves no state"
|
||||
);
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject — the
|
||||
/// unknown / revoked fingerprint never resolves; fail-closed, no
|
||||
/// weakly-authorized session, no cached resolution).
|
||||
#[tokio::test]
|
||||
async fn bridge_core_rejects_unknown_and_revoked_fingerprints() {
|
||||
let provider = Arc::new(MemIdentityProvider::new());
|
||||
let handler = handler_with(Arc::clone(&provider));
|
||||
let key = fixture("delta").public_key();
|
||||
|
||||
assert!(
|
||||
handler.resolve_bridge(&key, &Algorithm::Ed25519).is_none(),
|
||||
"an unregistered fingerprint resolves to the reject disposition"
|
||||
);
|
||||
|
||||
provider.register(
|
||||
fixture("delta").fingerprint,
|
||||
identity("user-delta", &["ssh"]),
|
||||
);
|
||||
assert!(
|
||||
handler.resolve_bridge(&key, &Algorithm::Ed25519).is_some(),
|
||||
"the registration lands"
|
||||
);
|
||||
|
||||
provider.revoke(fixture("delta").fingerprint);
|
||||
assert!(
|
||||
handler.resolve_bridge(&key, &Algorithm::Ed25519).is_none(),
|
||||
"the revocation takes — no cached resolution survives"
|
||||
);
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-002 decision 4 (ed25519-only: the
|
||||
/// non-ed25519 decline is explicit-over-inherited and precedes any
|
||||
/// resolution — a registered fingerprint under a non-ed25519
|
||||
/// algorithm still declines). The sk-variant and `Other` stand-ins
|
||||
/// cover the scheme classes the in-tree build cannot even parse
|
||||
/// (rsa/dsa are dropped from the dependency tree and ssh-key ships
|
||||
/// without ecdsa/dsa/rsa features here, so no wire row can drive a
|
||||
/// non-ed25519 key at this pin — recorded in the task notes; the
|
||||
/// gate is the explicit-over-inherited posture for the bump).
|
||||
#[tokio::test]
|
||||
async fn bridge_core_gates_non_ed25519_algorithms_before_resolution() {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let handler = handler_with(provider);
|
||||
let key = fixture("alpha").public_key();
|
||||
|
||||
assert_eq!(key.algorithm(), Algorithm::Ed25519);
|
||||
for non_ed25519 in [
|
||||
Algorithm::SkEd25519,
|
||||
Algorithm::SkEcdsaSha2NistP256,
|
||||
Algorithm::Other(
|
||||
"rsa-class@alkssh.gate-vector"
|
||||
.parse()
|
||||
.expect("algorithm name parses"),
|
||||
),
|
||||
] {
|
||||
let resolved = handler.resolve_bridge(&key, &non_ed25519);
|
||||
assert!(
|
||||
resolved.is_none(),
|
||||
"the ed25519-only gate precedes resolution: {non_ed25519:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
handler.resolve_bridge(&key, &Algorithm::Ed25519).is_some(),
|
||||
"the same registered fingerprint resolves under the ed25519 algorithm"
|
||||
);
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint the bridge
|
||||
/// computes is byte-format identical to the independent-issuer
|
||||
/// registrations — the wave-1 parity vectors pin the string; this row
|
||||
/// pins that the bridge resolves against exactly those values).
|
||||
#[tokio::test]
|
||||
async fn bridge_core_fingerprint_is_the_registered_value() {
|
||||
let provider = Arc::new(MemIdentityProvider::new());
|
||||
provider.register(
|
||||
fixture("bravo").fingerprint,
|
||||
identity("user-bravo", &["ssh"]),
|
||||
);
|
||||
let handler = handler_with(provider);
|
||||
|
||||
let resolved = handler
|
||||
.resolve_bridge(&fixture("bravo").public_key(), &Algorithm::Ed25519)
|
||||
.expect("independent-issuer registration value resolves");
|
||||
assert_eq!(resolved.id, "user-bravo");
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded
|
||||
/// standard-alphabet base64 — the wave-1 record, re-asserted against
|
||||
/// every committed fixture's `ssh-keygen -lf` value).
|
||||
#[test]
|
||||
fn every_fixture_fingerprint_format_stays_the_contract_invariant_shape() {
|
||||
for fixture_key in FIXTURE_KEYS {
|
||||
let computed = fingerprint(&fixture_key.public_key());
|
||||
assert_eq!(
|
||||
computed, fixture_key.fingerprint,
|
||||
"fixture {} must keep its recorded ssh-keygen value",
|
||||
fixture_key.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-005 decision 6 (host-key loading is the
|
||||
/// construction's business, never the auth hooks' — the bridge
|
||||
/// handler builds over an absent host-key source and never loads it,
|
||||
/// and the absent shape still refuses fail-closed if it were).
|
||||
#[tokio::test]
|
||||
async fn handler_constructs_over_an_absent_host_key_source_without_loading_it() {
|
||||
let provider = registered_provider(&[]);
|
||||
let serve_config = ServeConfig::new(
|
||||
provider as Arc<dyn IdentityProvider>,
|
||||
Arc::new(MemHostKeySource::absent()),
|
||||
);
|
||||
let handler = SessionHandler::new(Arc::new(serve_config));
|
||||
|
||||
assert!(handler.resolved_identity().is_none(), "no auth yet");
|
||||
|
||||
let err = MemHostKeySource::absent()
|
||||
.load()
|
||||
.await
|
||||
.expect_err("the absent shape refuses");
|
||||
assert!(matches!(err, KeySourceError::Absent));
|
||||
}
|
||||
+41
-1
@@ -55,7 +55,7 @@ use alkvault::{EncryptedData, VaultServiceHandle};
|
||||
use async_trait::async_trait;
|
||||
use russh::keys::{PrivateKey, PublicKey, decode_secret_key, parse_public_key_base64};
|
||||
|
||||
use alkssh::{HostKeyPolicy, PresentedHostKey};
|
||||
use alkssh::{HostKeyPolicy, HostKeySource, KeySourceError, PresentedHostKey};
|
||||
|
||||
/// One committed ed25519 test-key fixture: the private OpenSSH-PEM
|
||||
/// (`tests/vectors/<name>.key`), the public one-line blob
|
||||
@@ -269,6 +269,46 @@ impl TestVault {
|
||||
}
|
||||
}
|
||||
|
||||
/// The in-memory [`HostKeySource`] double — a static decrypted key set
|
||||
/// (wave-2 addition): the wire-shaped rows construct a real
|
||||
/// `SshAdapter`, whose construction wants a non-empty host-key set, and
|
||||
/// the direct-hook rows want a source that is never loaded (the handler
|
||||
/// construction loads nothing — host keys are the russh `Config`'s
|
||||
/// business, built in [`alkssh::SshAdapter::new`]).
|
||||
///
|
||||
/// `absent()` is the empty-set shape — its load refuses with
|
||||
/// [`KeySourceError::Absent`] (the fail-closed contract), so any row
|
||||
/// that accidentally loads an absent double fails loudly.
|
||||
pub struct MemHostKeySource {
|
||||
keys: Vec<PrivateKey>,
|
||||
}
|
||||
|
||||
impl MemHostKeySource {
|
||||
/// The never-loaded / absent shape.
|
||||
pub fn absent() -> Self {
|
||||
Self { keys: Vec::new() }
|
||||
}
|
||||
|
||||
/// A static decrypted set from the fixture keys — the wire rows'
|
||||
/// host-key set (e.g. `["bravo"]`, the rotation-pair member).
|
||||
pub fn from_fixtures(keys: &[&'static FixtureKey]) -> Self {
|
||||
Self {
|
||||
keys: keys.iter().map(|k| k.private_key()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostKeySource for MemHostKeySource {
|
||||
async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError> {
|
||||
if self.keys.is_empty() {
|
||||
Err(KeySourceError::Absent)
|
||||
} else {
|
||||
Ok(self.keys.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The in-memory [`HostKeyPolicy`] — a configurable verdict double with
|
||||
/// an inspectable decision log, for the wave-4 dial tests (and the
|
||||
/// case-1/case-2 rows of every host-key contract test).
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIOBvZ5e2rz/imrfzoDBHAMh8Y3tLHBqY+s81mRev5zK3AAAAIM28sP5UAl3npJJ8trZgPt2kE+Af6477/Rx6J2VbM14zAAAAAAAAAAEAAAABAAAAFGFsa3NzaC1maXh0dXJlLWFscGhhAAAABwAAAANnaXQAAAAAaVW5AAAAAAB8JF8AAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgC28p05V4i5BevEvFLLiM1+OG/dW5wkXJBiH+virFUXIAAABTAAAAC3NzaC1lZDI1NTE5AAAAQFWDEudf4jc8ERsBpoO97/xHfPuBCwr2VaVjmfj9wfgBKrrF+GtW23hkYuI+JACk3D0omjlXo0hfm+ljfaS57wI= alkssh-w1-fixture-alpha
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACALbynTlXiLkF68S8UsuIzX44b91bnCRckGIf6+KsVRcgAAAKhvsy24b7Mt
|
||||
uAAAAAtzc2gtZWQyNTUxOQAAACALbynTlXiLkF68S8UsuIzX44b91bnCRckGIf6+KsVRcg
|
||||
AAAEDbFLVv01Kg5qURZ8C46WGuY0QSJVTRm4OJyoK+horUQwtvKdOVeIuQXrxLxSy4jNfj
|
||||
hv3VucJFyQYh/r4qxVFyAAAAImFsa3NzaCB0ZXN0IGNhICh0aHJvd2F3YXkgZml4dHVyZS
|
||||
kBAgM=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAtvKdOVeIuQXrxLxSy4jNfjhv3VucJFyQYh/r4qxVFy alkssh test ca (throwaway fixture)
|
||||
@@ -14,3 +14,14 @@ alpha.pub SHA256:YyaiL42Xl8tcRRUhNCiqQBsiiRWVHHDR9hpXC45EQ6A
|
||||
bravo.pub SHA256:yQf/w2wYBoZ9adDlRGbcNh7AHE3mbqWRCqoGHra1ib0
|
||||
charlie.pub SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U
|
||||
delta.pub SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs
|
||||
ca.pub SHA256:mPAq1aRKBuyH3gZs9IcLWtc1X3AhYi50P80mpo7XPSw
|
||||
# alpha-cert.pub: the alpha user certificate (serial 1, key id
|
||||
# `alkssh-fixture-alpha`, principal `git`, validity 2026–2036) — the
|
||||
# ssh-keygen output above the fingerprint line is its OpenSSH text; the
|
||||
# certificate's embedded public half is alpha.pub (the same fingerprint
|
||||
# as alpha.pub; `ssh-keygen -lf alpha-cert.pub` reports it with the
|
||||
# ED25519-CERT type). The cert was signed by the throwaway ca key at
|
||||
# vector-generation time (w2-auth-bridge's auth_openssh_certificate
|
||||
# decline fixture). `ca.key`'s private half is committed on the same
|
||||
# fixture rationale as every other private half here.
|
||||
alpha-cert.pub SHA256:YyaiL42Xl8tcRRUhNCiqQBsiiRWVHHDR9hpXC45EQ6A
|
||||
Reference in new issue
Block a user