use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::hostkey::{HostKeyVerify, host_key_verify_by_name};
use super::message::{
AuthMethodPayload, SSH_MSG_SERVICE_REQUEST, SSH_MSG_USERAUTH_INFO_RESPONSE,
SSH_MSG_USERAUTH_REQUEST, SecretString, ServiceAccept, ServiceRequest, UserauthFailure,
UserauthInfoRequest, UserauthInfoResponse, UserauthPkOk, UserauthRequest, encode_success,
};
fn account_name(user: &str) -> &str {
user.split_once(':').map_or(user, |(u, _)| u)
}
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum AuthAttempt {
None {
user: String,
},
Password {
user: String,
password: SecretString,
},
PublicKey {
user: String,
algorithm: String,
public_blob: Vec<u8>,
probe_only: bool,
verified: bool,
cert: Option<CertInfo>,
},
KeyboardInteractive {
user: String,
},
}
impl core::fmt::Debug for AuthAttempt {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AuthAttempt::None { user } => f.debug_struct("None").field("user", user).finish(),
AuthAttempt::Password { user, password: _ } => f
.debug_struct("Password")
.field("user", user)
.field("password", &"<redacted>")
.finish(),
AuthAttempt::PublicKey {
user,
algorithm,
public_blob,
probe_only,
verified,
cert,
} => f
.debug_struct("PublicKey")
.field("user", user)
.field("algorithm", algorithm)
.field("public_blob", public_blob)
.field("probe_only", probe_only)
.field("verified", verified)
.field("cert", cert)
.finish(),
AuthAttempt::KeyboardInteractive { user } => f
.debug_struct("KeyboardInteractive")
.field("user", user)
.finish(),
}
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "alloc")]
#[non_exhaustive]
pub struct CertInfo {
pub ca_key_blob: Vec<u8>,
pub embedded_pubkey_blob: Vec<u8>,
pub ca_algorithm: String,
pub key_id: String,
pub serial: u64,
pub valid_principals: Vec<String>,
pub critical_options: Vec<(String, Vec<u8>)>,
pub extensions: Vec<(String, Vec<u8>)>,
pub valid_after: u64,
pub valid_before: u64,
}
#[cfg(feature = "alloc")]
impl CertInfo {
pub fn new(
ca_key_blob: Vec<u8>,
embedded_pubkey_blob: Vec<u8>,
ca_algorithm: impl Into<String>,
key_id: impl Into<String>,
serial: u64,
valid_principals: Vec<String>,
) -> Self {
CertInfo {
ca_key_blob,
embedded_pubkey_blob,
ca_algorithm: ca_algorithm.into(),
key_id: key_id.into(),
serial,
valid_principals,
critical_options: Vec::new(),
extensions: Vec::new(),
valid_after: 0,
valid_before: u64::MAX,
}
}
pub fn from_certificate(cert: &crate::cert::Certificate) -> Result<Self> {
Ok(CertInfo {
ca_key_blob: cert.signature_key_blob.clone(),
embedded_pubkey_blob: cert.embedded_pubkey_blob.clone(),
ca_algorithm: cert.ca_algorithm()?.into(),
key_id: cert.key_id.clone(),
serial: cert.serial,
valid_principals: cert.valid_principals.clone(),
critical_options: cert.critical_options.clone(),
extensions: cert.extensions.clone(),
valid_after: cert.valid_after,
valid_before: cert.valid_before,
})
}
pub fn has_extension(&self, name: &str) -> bool {
self.extensions.iter().any(|(n, _)| n == name)
}
pub fn critical_option(&self, name: &str) -> Option<&[u8]> {
self.critical_options
.iter()
.find(|(n, _)| n == name)
.map(|(_, d)| d.as_slice())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AuthDecision {
Accept,
PartialAccept {
still_required: Vec<String>,
},
Reject,
InteractiveRequest {
name: String,
instruction: String,
prompts: Vec<(String, bool)>,
},
}
pub trait Authenticator: Send {
fn evaluate(&mut self, attempt: AuthAttempt) -> AuthDecision;
#[cfg(feature = "alloc")]
fn key_restrictions(&mut self, user: &str, public_blob: &[u8]) -> Option<AuthCertCaps> {
let _ = (user, public_blob);
None
}
#[cfg(feature = "alloc")]
fn continuable_methods(&self, offered: &[&'static str]) -> Vec<String> {
offered.iter().map(|m| (*m).into()).collect()
}
fn evaluate_interactive(&mut self, user: &str, responses: Vec<String>) -> AuthDecision {
let _ = (user, responses);
AuthDecision::Reject
}
fn on_user_resolved(&mut self, user: &str, methods: &[String]) {
let _ = (user, methods);
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "alloc")]
#[non_exhaustive]
pub struct AuthCertCaps {
pub permit_pty: bool,
pub permit_port_forwarding: bool,
pub permit_agent_forwarding: bool,
pub permit_x11_forwarding: bool,
pub permit_user_rc: bool,
pub environment: Vec<String>,
pub force_command: Option<String>,
pub permit_open: Option<Vec<crate::config::HostPort>>,
pub permit_listen: Option<Vec<crate::config::HostPort>>,
pub force_tun_device: Option<u32>,
}
#[cfg(feature = "alloc")]
impl AuthCertCaps {
pub fn from_cert_info(ci: &CertInfo) -> Self {
let force_command = ci
.critical_option("force-command")
.and_then(crate::cert::decode_option_string);
AuthCertCaps {
permit_pty: ci.has_extension("permit-pty"),
permit_port_forwarding: ci.has_extension("permit-port-forwarding"),
permit_agent_forwarding: ci.has_extension("permit-agent-forwarding"),
permit_x11_forwarding: ci.has_extension("permit-X11-forwarding"),
permit_user_rc: ci.has_extension("permit-user-rc"),
environment: Vec::new(),
force_command,
permit_open: None,
permit_listen: None,
force_tun_device: None,
}
}
pub fn unrestricted() -> Self {
AuthCertCaps {
permit_pty: true,
permit_port_forwarding: true,
permit_agent_forwarding: true,
permit_x11_forwarding: true,
permit_user_rc: true,
environment: Vec::new(),
force_command: None,
permit_open: None,
permit_listen: None,
force_tun_device: None,
}
}
pub fn from_key_options(o: &crate::key::AuthorizedKeyOptions) -> Self {
let list = |v: &[String]| {
(!v.is_empty()).then(|| v.iter().filter_map(|e| parse_permit(e)).collect())
};
AuthCertCaps {
permit_pty: o.allows_pty(),
permit_port_forwarding: o.allows_port_forwarding(),
permit_agent_forwarding: o.allows_agent_forwarding(),
permit_x11_forwarding: o.allows_x11_forwarding(),
permit_user_rc: o.allows_user_rc(),
environment: o.environment.clone(),
force_command: o.command.clone(),
permit_open: list(&o.permit_open),
permit_listen: list(&o.permit_listen),
force_tun_device: o.tunnel.as_deref().and_then(|t| {
if t.eq_ignore_ascii_case("any") {
Some(0x7fff_ffff)
} else {
t.parse().ok()
}
}),
}
}
pub fn intersect(self, other: AuthCertCaps) -> Option<AuthCertCaps> {
let force_command = match (self.force_command, other.force_command) {
(Some(a), Some(b)) if a != b => return None,
(a, b) => a.or(b),
};
let both = |a: Option<Vec<crate::config::HostPort>>, b: Option<Vec<_>>| match (a, b) {
(Some(a), Some(b)) => Some(a.into_iter().filter(|e| b.contains(e)).collect()),
(a, b) => a.or(b),
};
let mut environment = self.environment;
environment.extend(other.environment);
Some(AuthCertCaps {
permit_pty: self.permit_pty && other.permit_pty,
permit_port_forwarding: self.permit_port_forwarding && other.permit_port_forwarding,
permit_agent_forwarding: self.permit_agent_forwarding && other.permit_agent_forwarding,
permit_x11_forwarding: self.permit_x11_forwarding && other.permit_x11_forwarding,
permit_user_rc: self.permit_user_rc && other.permit_user_rc,
environment,
force_command,
permit_open: both(self.permit_open, other.permit_open),
permit_listen: both(self.permit_listen, other.permit_listen),
force_tun_device: self.force_tun_device.or(other.force_tun_device),
})
}
}
#[cfg(feature = "alloc")]
fn merge_caps(a: Option<AuthCertCaps>, b: Option<AuthCertCaps>) -> Option<Option<AuthCertCaps>> {
match (a, b) {
(Some(a), Some(b)) => a.intersect(b).map(Some),
(a, b) => Some(a.or(b)),
}
}
#[cfg(feature = "alloc")]
fn parse_permit(spec: &str) -> Option<crate::config::HostPort> {
let (host, port) = if let Some(rest) = spec.strip_prefix('[') {
let (h, p) = rest.split_once("]:")?;
(String::from(h), p)
} else if let Some((h, p)) = spec.rsplit_once(':') {
(String::from(h), p)
} else {
(String::from("*"), spec)
};
let port = if port == "*" {
None
} else {
Some(port.parse().ok()?)
};
Some(crate::config::HostPort { host, port })
}
#[non_exhaustive]
pub enum ServerStep {
Send(Vec<u8>),
Authenticated {
payload: Vec<u8>,
user: String,
cert_caps: Option<AuthCertCaps>,
auth_info: String,
},
Disconnect(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
AwaitingServiceRequest,
AwaitingRequest,
AwaitingInfoResponse,
Done,
}
pub struct ServerAuth {
service: &'static str,
session_id: Vec<u8>,
accepted_methods: Vec<&'static str>,
auth: Box<dyn Authenticator>,
state: State,
pending_user: Option<String>,
used_keys: Vec<Vec<u8>>,
host_key: Option<Vec<u8>>,
auth_info: String,
pending_prompt_count: Option<usize>,
first_user: Option<String>,
allow_none: bool,
max_auth_tries: Option<u32>,
failed_attempts: u32,
now: u64,
ca_signature_algorithms: Vec<String>,
pending_cert_caps: Option<AuthCertCaps>,
}
impl ServerAuth {
pub fn new(
session_id: Vec<u8>,
methods: Vec<&'static str>,
auth: Box<dyn Authenticator>,
) -> Self {
Self {
service: "ssh-connection",
session_id,
accepted_methods: methods,
auth,
state: State::AwaitingServiceRequest,
pending_user: None,
used_keys: Vec::new(),
host_key: None,
auth_info: String::new(),
pending_prompt_count: None,
first_user: None,
allow_none: false,
max_auth_tries: None,
failed_attempts: 0,
now: 0,
ca_signature_algorithms: Vec::new(),
pending_cert_caps: None,
}
}
pub fn set_now(&mut self, now: u64) -> &mut Self {
self.now = now;
self
}
pub fn set_ca_signature_algorithms(&mut self, algos: Vec<String>) -> &mut Self {
self.ca_signature_algorithms = algos;
self
}
pub fn set_max_auth_tries(&mut self, max: Option<u32>) -> &mut Self {
self.max_auth_tries = max;
self
}
pub fn set_accepted_methods(&mut self, methods: Vec<&'static str>) -> &mut Self {
self.accepted_methods = methods;
self
}
pub fn accepted_methods(&self) -> &[&'static str] {
&self.accepted_methods
}
pub fn notify_user_resolved(&mut self, user: &str, methods: &[String]) {
self.auth.on_user_resolved(user, methods);
}
pub fn peek_request(payload: &[u8]) -> Option<(String, &'static str)> {
let req = UserauthRequest::decode(payload).ok()?;
let method = match req.method {
AuthMethodPayload::None => "none",
AuthMethodPayload::Password { .. } => "password",
AuthMethodPayload::PublicKey { .. } => "publickey",
AuthMethodPayload::KeyboardInteractive { .. } => "keyboard-interactive",
AuthMethodPayload::Other { .. } => "",
};
Some((String::from(account_name(&req.user)), method))
}
pub fn reject_unadvertised(&mut self, user: &str) -> Result<ServerStep> {
if let Some(step) = self.pin_user(user) {
return Ok(step);
}
self.emit_failure()
}
fn pin_user(&mut self, user: &str) -> Option<ServerStep> {
match &self.first_user {
Some(prev) if prev != user => Some(ServerStep::Disconnect(
"auth: username changed mid-authentication",
)),
None => {
self.first_user = Some(user.into());
None
}
_ => None,
}
}
pub fn set_host_key(&mut self, host_key: Vec<u8>) {
self.host_key = Some(host_key);
}
pub fn allow_none(&mut self, allow: bool) -> &mut Self {
self.allow_none = allow;
self
}
pub fn on_packet(&mut self, payload: &[u8]) -> Result<ServerStep> {
if payload.is_empty() {
return Err(Error::Format("auth: empty payload"));
}
let msg_type = payload[0];
match self.state {
State::AwaitingServiceRequest => {
if msg_type != SSH_MSG_SERVICE_REQUEST {
return Err(Error::Protocol("auth: expected SERVICE_REQUEST"));
}
let req = ServiceRequest::decode(payload)?;
if req.service != "ssh-userauth" {
return Err(Error::Protocol("auth: unknown service requested"));
}
self.state = State::AwaitingRequest;
let accept = ServiceAccept {
service: "ssh-userauth".into(),
};
Ok(ServerStep::Send(accept.encode()))
}
State::AwaitingRequest => {
if msg_type != SSH_MSG_USERAUTH_REQUEST {
return Err(Error::Protocol("auth: expected USERAUTH_REQUEST"));
}
let req = UserauthRequest::decode(payload)?;
if let Some(step) = self.pin_user(account_name(&req.user)) {
return Ok(step);
}
if req.service != self.service {
return self.emit_failure();
}
self.handle_request(req)
}
State::AwaitingInfoResponse => {
if msg_type != SSH_MSG_USERAUTH_INFO_RESPONSE {
return Err(Error::Protocol("auth: expected INFO_RESPONSE"));
}
let mut resp = UserauthInfoResponse::decode(payload)?;
let user = self
.pending_user
.take()
.ok_or(Error::Protocol("auth: info response without pending user"))?;
let prompt_count = self.pending_prompt_count.take();
let responses = core::mem::take(&mut resp.responses);
self.state = State::AwaitingRequest;
if prompt_count != Some(responses.len()) {
return self.emit_failure();
}
let decision = self.auth.evaluate_interactive(&user, responses);
self.record_success(&decision, "keyboard-interactive/pam".into());
self.apply_decision(decision, &user)
}
State::Done => Ok(ServerStep::Disconnect("auth: already finished")),
}
}
fn handle_request(&mut self, req: UserauthRequest) -> Result<ServerStep> {
let wire_user = req.user.clone();
let user = String::from(account_name(&req.user));
match req.method {
AuthMethodPayload::None => {
if !self.allow_none {
return self.emit_failure();
}
let decision = self.auth.evaluate(AuthAttempt::None { user: user.clone() });
self.apply_decision(decision, &user)
}
AuthMethodPayload::Password {
password,
new_password: _,
} => {
let decision = self.auth.evaluate(AuthAttempt::Password {
user: user.clone(),
password,
});
self.record_success(&decision, "password".into());
self.apply_decision(decision, &user)
}
AuthMethodPayload::PublicKey {
signature_present,
algorithm,
public_blob,
signature,
host_key,
} => {
if let Some(named) = &host_key
&& self.host_key.as_ref() != Some(named)
{
return self.emit_failure();
}
self.handle_publickey(
user,
&wire_user,
signature_present,
algorithm,
public_blob,
signature,
host_key,
)
}
AuthMethodPayload::KeyboardInteractive {
language_tag: _,
submethods: _,
} => {
let decision = self
.auth
.evaluate(AuthAttempt::KeyboardInteractive { user: user.clone() });
self.apply_decision(decision, &user)
}
AuthMethodPayload::Other { .. } => self.emit_failure(),
}
}
#[allow(clippy::too_many_arguments)]
fn handle_publickey(
&mut self,
user: String,
wire_user: &str,
signature_present: bool,
algorithm: String,
public_blob: Vec<u8>,
signature: Option<Vec<u8>>,
host_key: Option<Vec<u8>>,
) -> Result<ServerStep> {
let is_cert = crate::cert::is_cert_name(&algorithm);
let key_blob = if is_cert {
match crate::cert::Certificate::parse(&public_blob) {
Ok(c) => c.embedded_pubkey_blob().to_vec(),
Err(_) => return self.emit_failure(),
}
} else {
public_blob.clone()
};
if self.used_keys.contains(&key_blob) {
return self.emit_failure();
}
if !signature_present {
let cert_info = if is_cert {
match crate::cert::Certificate::parse(&public_blob) {
Ok(c)
if cert_plain_algorithm(&algorithm, &c).is_some()
&& self.user_cert_valid(&c) =>
{
Some(CertInfo::from_certificate(&c)?)
}
_ => return self.emit_failure(),
}
} else {
None
};
let decision = self.auth.evaluate(AuthAttempt::PublicKey {
user: user.clone(),
algorithm: algorithm.clone(),
public_blob: public_blob.clone(),
probe_only: true,
verified: false,
cert: cert_info,
});
return match decision {
AuthDecision::Accept | AuthDecision::PartialAccept { .. } => {
let pk_ok = UserauthPkOk {
algorithm,
public_blob,
};
Ok(ServerStep::Send(pk_ok.encode()))
}
AuthDecision::Reject => self.emit_failure(),
AuthDecision::InteractiveRequest { .. } => {
Err(Error::Protocol("auth: interactive on publickey probe"))
}
};
}
let sig = match signature {
Some(s) => s,
None => return Err(Error::Format("auth: missing signature")),
};
let (verifier, cert_info): (Box<dyn HostKeyVerify>, Option<CertInfo>) = if is_cert {
let cert = match crate::cert::Certificate::parse(&public_blob) {
Ok(c) => c,
Err(_) => return self.emit_failure(),
};
let plain = match cert_plain_algorithm(&algorithm, &cert) {
Some(p) => p,
None => return self.emit_failure(),
};
if signature_algorithm(&sig) != Some(plain) {
return self.emit_failure();
}
if !self.user_cert_valid(&cert) {
return self.emit_failure();
}
let v = match host_key_verify_by_name(plain, cert.embedded_pubkey_blob()) {
Ok(v) => v,
Err(_) => return self.emit_failure(),
};
(v, Some(CertInfo::from_certificate(&cert)?))
} else {
(host_key_verify_by_name(&algorithm, &public_blob)?, None)
};
let signed = super::message::publickey_signed_data_bound(
&self.session_id,
wire_user,
self.service,
&algorithm,
&public_blob,
host_key.as_deref(),
);
if verifier.verify(&signed, &sig).is_err() {
return self.emit_failure();
}
let cert_caps = cert_info.as_ref().map(AuthCertCaps::from_cert_info);
let decision = self.auth.evaluate(AuthAttempt::PublicKey {
user: user.clone(),
algorithm,
public_blob: public_blob.clone(),
probe_only: false,
verified: true,
cert: cert_info,
});
let decision = match decision {
d @ (AuthDecision::Accept | AuthDecision::PartialAccept { .. }) => {
let key_caps = self.auth.key_restrictions(&user, &public_blob);
let merged = merge_caps(cert_caps, key_caps)
.and_then(|c| merge_caps(self.pending_cert_caps.clone(), c));
match merged {
Some(c) => {
self.pending_cert_caps = c;
d
}
None => AuthDecision::Reject,
}
}
other => other,
};
if matches!(decision, AuthDecision::PartialAccept { .. }) {
self.used_keys.push(key_blob);
}
let key_type = crate::format::Reader::new(&public_blob)
.read_string()
.ok()
.and_then(|t| core::str::from_utf8(t).ok())
.unwrap_or("");
let line = alloc::format!(
"publickey {key_type} {}",
crate::key::base64::encode(&public_blob)
);
self.record_success(&decision, line);
self.apply_decision(decision, &user)
}
fn user_cert_valid(&self, cert: &crate::cert::Certificate) -> bool {
if cert.check_type(crate::cert::CertType::User).is_err()
|| cert.check_validity(self.now).is_err()
|| cert.require_known_critical_options().is_err()
{
return false;
}
let ca_algos: Vec<&str> = if self.ca_signature_algorithms.is_empty() {
crate::config::algos::CA_SIGNATURE_DEFAULTS.to_vec()
} else {
self.ca_signature_algorithms
.iter()
.map(|s| s.as_str())
.collect()
};
cert.verify_ca_signature(&ca_algos).is_ok()
}
fn record_success(&mut self, decision: &AuthDecision, line: String) {
if matches!(
decision,
AuthDecision::Accept | AuthDecision::PartialAccept { .. }
) {
self.auth_info.push_str(&line);
self.auth_info.push('\n');
}
}
fn apply_decision(&mut self, decision: AuthDecision, user: &str) -> Result<ServerStep> {
match decision {
AuthDecision::Accept => {
self.state = State::Done;
Ok(ServerStep::Authenticated {
payload: encode_success(),
user: user.into(),
cert_caps: self.pending_cert_caps.take(),
auth_info: core::mem::take(&mut self.auth_info),
})
}
AuthDecision::PartialAccept { still_required } => {
let failure = UserauthFailure {
continuations: still_required,
partial_success: true,
};
Ok(ServerStep::Send(failure.encode()))
}
AuthDecision::Reject => self.emit_failure(),
AuthDecision::InteractiveRequest {
name,
instruction,
prompts,
} => {
let prompt_count = prompts.len();
let req = UserauthInfoRequest {
name,
instruction,
language: String::new(),
prompts,
};
self.state = State::AwaitingInfoResponse;
self.pending_user = Some(user.into());
self.pending_prompt_count = Some(prompt_count);
Ok(ServerStep::Send(req.encode()))
}
}
}
#[cfg(test)]
fn failed_attempts(&self) -> u32 {
self.failed_attempts
}
#[cfg(test)]
fn pinned_user(&self) -> Option<&str> {
self.first_user.as_deref()
}
fn emit_failure(&mut self) -> Result<ServerStep> {
self.failed_attempts = self.failed_attempts.saturating_add(1);
if let Some(max) = self.max_auth_tries
&& self.failed_attempts > max
{
self.state = State::Done;
return Ok(ServerStep::Disconnect("Too many authentication failures"));
}
let cont = self.auth.continuable_methods(&self.accepted_methods);
let failure = UserauthFailure {
continuations: cont,
partial_success: false,
};
Ok(ServerStep::Send(failure.encode()))
}
}
fn cert_plain_algorithm(algorithm: &str, cert: &crate::cert::Certificate) -> Option<&'static str> {
let plain = crate::cert::cert_name_to_plain(algorithm)?;
let family_ok = match cert.embedded_algorithm() {
"ssh-rsa" => matches!(plain, "rsa-sha2-256" | "rsa-sha2-512" | "ssh-rsa"),
other => plain == other,
};
family_ok.then_some(plain)
}
fn signature_algorithm(sig_blob: &[u8]) -> Option<&str> {
let mut r = crate::format::Reader::new(sig_blob);
core::str::from_utf8(r.read_string().ok()?).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::message::{AuthMethodPayload, SecretString, ServiceRequest, UserauthRequest};
struct RejectAll;
impl Authenticator for RejectAll {
fn evaluate(&mut self, _attempt: AuthAttempt) -> AuthDecision {
AuthDecision::Reject
}
}
fn service_req() -> Vec<u8> {
ServiceRequest {
service: "ssh-userauth".into(),
}
.encode()
}
fn password_req() -> Vec<u8> {
UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::Password {
password: SecretString::from("wrong"),
new_password: None,
},
}
.encode()
}
#[test]
fn max_auth_tries_disconnects() {
let mut sa = ServerAuth::new(vec![1, 2, 3], vec!["password"], Box::new(RejectAll));
sa.set_max_auth_tries(Some(2));
assert!(matches!(
sa.on_packet(&service_req()).unwrap(),
ServerStep::Send(_)
));
assert!(matches!(
sa.on_packet(&password_req()).unwrap(),
ServerStep::Send(_)
));
assert!(matches!(
sa.on_packet(&password_req()).unwrap(),
ServerStep::Send(_)
));
assert_eq!(sa.failed_attempts(), 2);
assert!(matches!(
sa.on_packet(&password_req()).unwrap(),
ServerStep::Disconnect(_)
));
}
#[test]
fn peek_request_extracts_user_and_method() {
let (user, method) = ServerAuth::peek_request(&password_req()).expect("decoded");
assert_eq!(user, "alice");
assert_eq!(method, "password");
assert!(ServerAuth::peek_request(&service_req()).is_none());
}
#[test]
fn reject_unadvertised_counts_and_uses_current_methods() {
let mut sa = ServerAuth::new(vec![1, 2, 3], vec!["publickey"], Box::new(RejectAll));
sa.set_accepted_methods(vec![]);
assert!(sa.accepted_methods().is_empty());
assert!(matches!(
sa.reject_unadvertised("alice").unwrap(),
ServerStep::Send(_)
));
assert_eq!(sa.failed_attempts(), 1);
assert_eq!(sa.pinned_user(), Some("alice"));
assert!(matches!(
sa.reject_unadvertised("bob").unwrap(),
ServerStep::Disconnect(_)
));
assert_eq!(sa.failed_attempts(), 1);
}
#[test]
fn service_mismatch_rejection_still_pins_username() {
let mut sa = ServerAuth::new(vec![1, 2, 3], vec!["password"], Box::new(RejectAll));
sa.on_packet(&service_req()).unwrap();
let bad_service = UserauthRequest {
user: "alice".into(),
service: "not-ssh-connection".into(),
method: AuthMethodPayload::None,
}
.encode();
assert!(matches!(
sa.on_packet(&bad_service).unwrap(),
ServerStep::Send(_)
));
assert_eq!(sa.pinned_user(), Some("alice"));
let bob = UserauthRequest {
user: "bob".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::Password {
password: SecretString::from("x"),
new_password: None,
},
}
.encode();
assert!(matches!(
sa.on_packet(&bob).unwrap(),
ServerStep::Disconnect(_)
));
}
#[test]
fn no_limit_keeps_failing() {
let mut sa = ServerAuth::new(vec![1], vec!["password"], Box::new(RejectAll));
sa.on_packet(&service_req()).unwrap();
for _ in 0..10 {
assert!(matches!(
sa.on_packet(&password_req()).unwrap(),
ServerStep::Send(_)
));
}
}
}