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,
};
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")]
pub struct CertInfo {
pub ca_key_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 from_certificate(cert: &crate::cert::Certificate) -> Result<Self> {
Ok(CertInfo {
ca_key_blob: cert.signature_key_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)]
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;
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")]
pub struct AuthCertCaps {
pub permit_pty: bool,
pub permit_port_forwarding: bool,
pub permit_agent_forwarding: bool,
pub permit_x11_forwarding: bool,
pub force_command: Option<String>,
}
#[cfg(feature = "alloc")]
impl AuthCertCaps {
pub fn from_cert_info(ci: &CertInfo) -> Self {
let force_command = ci
.critical_option("force-command")
.and_then(decode_ssh_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"),
force_command,
}
}
}
#[cfg(feature = "alloc")]
fn decode_ssh_string(data: &[u8]) -> Option<String> {
let mut r = crate::format::Reader::new(data);
let s = r.read_string().ok()?;
if !r.is_empty() {
return None;
}
core::str::from_utf8(s).ok().map(String::from)
}
pub enum ServerStep {
Send(Vec<u8>),
Authenticated {
payload: Vec<u8>,
user: String,
cert_caps: Option<AuthCertCaps>,
},
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>,
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,
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 { .. } => return Some((req.user, "")),
};
Some((req.user, method))
}
pub fn reject_unadvertised(&mut self) -> Result<ServerStep> {
self.emit_failure()
}
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 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.apply_decision(decision, &user)
}
State::Done => Ok(ServerStep::Disconnect("auth: already finished")),
}
}
fn handle_request(&mut self, req: UserauthRequest) -> Result<ServerStep> {
let user = req.user.clone();
match &self.first_user {
Some(prev) if *prev != user => {
return Ok(ServerStep::Disconnect(
"auth: username changed mid-authentication",
));
}
None => self.first_user = Some(user.clone()),
_ => {}
}
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.apply_decision(decision, &user)
}
AuthMethodPayload::PublicKey {
signature_present,
algorithm,
public_blob,
signature,
} => self.handle_publickey(user, signature_present, algorithm, public_blob, signature),
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(),
}
}
fn handle_publickey(
&mut self,
user: String,
signature_present: bool,
algorithm: String,
public_blob: Vec<u8>,
signature: Option<Vec<u8>>,
) -> Result<ServerStep> {
let is_cert = crate::cert::is_cert_name(&algorithm);
if !signature_present {
let cert_info = if is_cert {
match crate::cert::Certificate::parse(&public_blob) {
Ok(c) => Some(CertInfo::from_certificate(&c)?),
Err(_) => 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(),
};
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 self.emit_failure();
}
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()
};
if cert.verify_ca_signature(&ca_algos).is_err() {
return self.emit_failure();
}
let v = match cert.embedded_verifier(&sig) {
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(
&self.session_id,
&user,
self.service,
&algorithm,
&public_blob,
);
if verifier.verify(&signed, &sig).is_err() {
return self.emit_failure();
}
if let Some(ci) = &cert_info {
self.pending_cert_caps = Some(AuthCertCaps::from_cert_info(ci));
}
let decision = self.auth.evaluate(AuthAttempt::PublicKey {
user: user.clone(),
algorithm,
public_blob,
probe_only: false,
verified: true,
cert: cert_info,
});
self.apply_decision(decision, &user)
}
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(),
})
}
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
}
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: Vec<String> = self.accepted_methods.iter().map(|s| (*s).into()).collect();
let failure = UserauthFailure {
continuations: cont,
partial_success: false,
};
Ok(ServerStep::Send(failure.encode()))
}
}
#[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().unwrap(),
ServerStep::Send(_)
));
assert_eq!(sa.failed_attempts(), 1);
}
#[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(_)
));
}
}
}