Skip to main content

russh/
auth.rs

1// Copyright 2016 Pierre-Étienne Meunier
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15
16use std::future::Future;
17use std::ops::Deref;
18use std::str::FromStr;
19use std::sync::Arc;
20
21use ssh_key::{Certificate, HashAlg, PrivateKey};
22use thiserror::Error;
23use tokio::io::{AsyncRead, AsyncWrite};
24
25use crate::helpers::NameList;
26use crate::keys::PrivateKeyWithHashAlg;
27use crate::keys::agent::AgentIdentity;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum MethodKind {
31    None,
32    Password,
33    PublicKey,
34    HostBased,
35    KeyboardInteractive,
36}
37
38impl From<&MethodKind> for &'static str {
39    fn from(value: &MethodKind) -> Self {
40        match value {
41            MethodKind::None => "none",
42            MethodKind::Password => "password",
43            MethodKind::PublicKey => "publickey",
44            MethodKind::HostBased => "hostbased",
45            MethodKind::KeyboardInteractive => "keyboard-interactive",
46        }
47    }
48}
49
50impl FromStr for MethodKind {
51    fn from_str(b: &str) -> Result<MethodKind, Self::Err> {
52        match b {
53            "none" => Ok(MethodKind::None),
54            "password" => Ok(MethodKind::Password),
55            "publickey" => Ok(MethodKind::PublicKey),
56            "hostbased" => Ok(MethodKind::HostBased),
57            "keyboard-interactive" => Ok(MethodKind::KeyboardInteractive),
58            _ => Err(()),
59        }
60    }
61
62    type Err = ();
63}
64
65impl From<&MethodKind> for String {
66    fn from(value: &MethodKind) -> Self {
67        <&str>::from(value).to_string()
68    }
69}
70
71/// An ordered set of authentication methods.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct MethodSet(Vec<MethodKind>);
74
75impl Deref for MethodSet {
76    type Target = [MethodKind];
77
78    fn deref(&self) -> &Self::Target {
79        &self.0
80    }
81}
82
83impl From<&[MethodKind]> for MethodSet {
84    fn from(value: &[MethodKind]) -> Self {
85        let mut this = Self::empty();
86        for method in value {
87            this.push(*method);
88        }
89        this
90    }
91}
92
93impl From<&MethodSet> for NameList {
94    fn from(value: &MethodSet) -> Self {
95        Self(value.iter().map(|x| x.into()).collect())
96    }
97}
98
99impl From<&NameList> for MethodSet {
100    fn from(value: &NameList) -> Self {
101        Self(
102            value
103                .iter()
104                .filter_map(|x| MethodKind::from_str(x).ok())
105                .collect(),
106        )
107    }
108}
109
110impl MethodSet {
111    pub fn empty() -> Self {
112        Self(Vec::new())
113    }
114
115    pub fn all() -> Self {
116        Self(vec![
117            MethodKind::None,
118            MethodKind::Password,
119            MethodKind::PublicKey,
120            MethodKind::HostBased,
121            MethodKind::KeyboardInteractive,
122        ])
123    }
124
125    pub fn remove(&mut self, method: MethodKind) {
126        self.0.retain(|x| *x != method);
127    }
128
129    /// Push a method to the end of the list.
130    /// If the method is already in the list, it is moved to the end.
131    pub fn push(&mut self, method: MethodKind) {
132        self.remove(method);
133        self.0.push(method);
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum AuthResult {
139    Success,
140    Failure {
141        /// The server suggests to proceed with these auth methods
142        remaining_methods: MethodSet,
143        /// The server says that though auth method has been accepted,
144        /// further authentication is required
145        partial_success: bool,
146    },
147}
148
149impl AuthResult {
150    pub fn success(&self) -> bool {
151        matches!(self, AuthResult::Success)
152    }
153}
154
155#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
156pub trait Signer: Sized {
157    type Error: From<crate::SendError>;
158
159    fn auth_sign(
160        &mut self,
161        key: &AgentIdentity,
162        hash_alg: Option<HashAlg>,
163        to_sign: Vec<u8>,
164    ) -> impl Future<Output = Result<Vec<u8>, Self::Error>> + Send;
165}
166
167#[derive(Debug, Error)]
168pub enum AgentAuthError {
169    #[error(transparent)]
170    Send(#[from] crate::SendError),
171    #[error(transparent)]
172    Key(#[from] crate::keys::Error),
173}
174
175#[cfg_attr(feature = "async-trait", async_trait::async_trait)]
176impl<R: AsyncRead + AsyncWrite + Unpin + Send + 'static> Signer
177    for crate::keys::agent::client::AgentClient<R>
178{
179    type Error = AgentAuthError;
180
181    #[allow(clippy::manual_async_fn)]
182    fn auth_sign(
183        &mut self,
184        key: &AgentIdentity,
185        hash_alg: Option<HashAlg>,
186        to_sign: Vec<u8>,
187    ) -> impl Future<Output = Result<Vec<u8>, Self::Error>> {
188        async move {
189            self.sign_request(key, hash_alg, to_sign)
190                .await
191                .map_err(Into::into)
192        }
193    }
194}
195
196#[derive(Debug)]
197#[allow(clippy::large_enum_variant)]
198pub enum Method {
199    None,
200    Password {
201        password: String,
202    },
203    PublicKey {
204        key: PrivateKeyWithHashAlg,
205    },
206    OpenSshCertificate {
207        key: Arc<PrivateKey>,
208        cert: Certificate,
209    },
210    FuturePublicKey {
211        key: ssh_key::PublicKey,
212        hash_alg: Option<HashAlg>,
213    },
214    /// Certificate-based authentication using an external signer (e.g., SSH agent).
215    /// The certificate is sent to the server, but signing is delegated to the signer.
216    FutureCertificate {
217        cert: Certificate,
218        hash_alg: Option<HashAlg>,
219    },
220    KeyboardInteractive {
221        submethods: String,
222    },
223    // Hostbased,
224}
225
226#[doc(hidden)]
227#[derive(Debug)]
228pub struct AuthRequest {
229    initial_methods: MethodSet,
230    pub methods: MethodSet,
231    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
232    pub partial_success: bool,
233    pub current: Option<CurrentRequest>,
234    pub(crate) principal: Option<AuthPrincipal>,
235    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
236    pub rejection_count: usize,
237}
238
239#[doc(hidden)]
240#[derive(Debug)]
241pub(crate) struct AuthPrincipal {
242    user: String,
243    service: String,
244}
245
246#[doc(hidden)]
247#[derive(Debug)]
248pub enum CurrentRequest {
249    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
250    PublicKey {
251        #[allow(dead_code)]
252        key: Vec<u8>,
253        #[allow(dead_code)]
254        algo: Vec<u8>,
255        sent_pk_ok: bool,
256    },
257    KeyboardInteractive {
258        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
259        submethods: String,
260    },
261}
262
263impl AuthRequest {
264    pub(crate) fn server(methods: MethodSet) -> Self {
265        Self {
266            initial_methods: methods.clone(),
267            methods,
268            partial_success: false,
269            current: None,
270            principal: None,
271            rejection_count: 0,
272        }
273    }
274
275    pub(crate) fn new(method: &Method) -> Self {
276        match method {
277            Method::KeyboardInteractive { submethods } => Self {
278                initial_methods: MethodSet::all(),
279                methods: MethodSet::all(),
280                partial_success: false,
281                current: Some(CurrentRequest::KeyboardInteractive {
282                    submethods: submethods.to_string(),
283                }),
284                principal: None,
285                rejection_count: 0,
286            },
287            _ => Self {
288                initial_methods: MethodSet::all(),
289                methods: MethodSet::all(),
290                partial_success: false,
291                current: None,
292                principal: None,
293                rejection_count: 0,
294            },
295        }
296    }
297
298    pub(crate) fn bind_or_reset_principal(&mut self, user: &str, service: &str) -> bool {
299        match &self.principal {
300            Some(bound) if bound.user == user && bound.service == service => false,
301            _ => {
302                self.principal = Some(AuthPrincipal {
303                    user: user.to_owned(),
304                    service: service.to_owned(),
305                });
306                self.methods = self.initial_methods.clone();
307                self.partial_success = false;
308                self.current = None;
309                true
310            }
311        }
312    }
313}