unftp_auth_jsonfile/
lib.rs

1//! This crate implements a [libunftp](https://docs.rs/libunftp/latest/libunftp) `Authenticator`
2//! that authenticates against credentials in a JSON format.
3//!
4//! It supports both plaintext as well as [PBKDF2](https://tools.ietf.org/html/rfc2898#section-5.2)
5//! encoded passwords.
6//!
7//! # Plaintext example
8//!
9//! ```json
10//! [
11//!   {
12//!     "username": "alice",
13//!     "password": "I am in Wonderland!"
14//!   }
15//! ]
16//! ```
17//!
18//! # PBKDF2 encoded Example
19//!
20//! Both the salt and key need to be base64 encoded.
21//! Currently only HMAC_SHA256 is supported by libunftp (more will be supported later).
22//!
23//! There are various tools that can be used to generate the key.
24//!
25//! In this example we show two ways to generate the PBKDF2. First we show how to use the common tool [nettle-pbkdf2](http://www.lysator.liu.se/~nisse/nettle/) directly.
26//!
27//! Generate a secure salt:
28//! ```sh
29//! salt=$(dd if=/dev/random bs=1 count=8)
30//! ```
31//!
32//! Generate the base64 encoded PBKDF2 key, to be copied into the `pbkdf2_key` field of the JSON structure.
33//!
34//! When using `nettle` directly, make sure not to exceed the output length of the digest algorithm (256 bit, 32 bytes in our case):
35//! ```sh
36//! echo -n "mypassword" | nettle-pbkdf2 -i 500000 -l 32 --hex-salt $(echo -n $salt | xxd -p -c 80) --raw |openssl base64 -A
37//! ```
38//!
39//! Convert the salt into base64 to be copied into the `pbkdf2_salt` field of the JSON structure:
40//! ```sh
41//! echo -n $salt | openssl base64 -A
42//! ```
43//!
44//! Alternatively to using `nettle` directly, you may use our convenient docker image: bolcom/unftp-key-generator
45//!
46//! ```sh
47//! docker run -ti bolcom/unftp-key-generator -h
48//! ```
49//!
50//! Running it without options, will generate a PBKDF2 key and a random salt from a given password.
51//! If no password is entered, a secure password will be generated with default settings for the password complexity and number of iterations.
52//!
53//! Now write these to the JSON file, as seen below.
54//! If you use our unftp-key-generator, you can use the `-u` switch, to generate the JSON output directly.
55//! Otherwise, make sure that `pbkdf2_iter` in the example below, matches the iterations (`-i`) used with `nettle-pbkdf2`.
56//!
57//! ```json
58//! [
59//!   {
60//!     "username": "bob",
61//!     "pbkdf2_salt": "<<BASE_64_RANDOM_SALT>>",
62//!     "pbkdf2_key": "<<BASE_64_KEY>>",
63//!     "pbkdf2_iter": 500000
64//!   },
65//! ]
66//! ```
67//!
68//! # Mixed example
69//!
70//! It is possible to mix plaintext and pbkdf2 encoded type passwords.
71//!
72//! ```json
73//! [
74//!   {
75//!     "username": "alice",
76//!     "pbkdf2_salt": "<<BASE_64_RANDOM_SALT>>",
77//!     "pbkdf2_key": "<<BASE_64_KEY>>",
78//!     "pbkdf2_iter": 500000
79//!   },
80//!   {
81//!     "username": "bob",
82//!     "password": "This password is a joke"
83//!   }
84//! ]
85//! ```
86//!
87//! # Using it with libunftp
88//!
89//! Use [JsonFileAuthenticator::from_file](crate::JsonFileAuthenticator::from_file) to load the JSON structure directly from a file.
90//! See the example `examples/jsonfile_auth.rs`.
91//!
92//! Alternatively use another source for your JSON credentials, and use [JsonFileAuthenticator::from_json](crate::JsonFileAuthenticator::from_json) instead.
93//!
94//! # Preventing unauthorized access with allow lists
95//!
96//! ```json
97//! [
98//!   {
99//!     "username": "bob",
100//!     "password": "it is me",
101//!     "allowed_ip_ranges": ["192.168.178.0/24", "127.0.0.0/8"]
102//!   },
103//! ]
104//! ```
105//!
106//! # Per user certificate validation
107//!
108//! The JSON authenticator can also check that the CN of a client certificate matches a certain
109//! string or substring. Furthermore, password-less; certificate only; authentication can be configured
110//! per user when libunftp is configured to use TLS and specifically also configured to request or
111//! require a client certificate through the [Server.ftps_client_auth](https://docs.rs/libunftp/0.17.4/libunftp/struct.Server.html#method.ftps_client_auth)
112//! method. For this to work correctly a trust store with the root certificate also needs to be configured
113//! with [Server.ftps_trust_store](https://docs.rs/libunftp/0.17.4/libunftp/struct.Server.html#method.ftps_trust_store).
114//!
115//! Given this example configuration:
116//!
117//! ```json
118//! [
119//!   {
120//!    "username": "eve",
121//!    "pbkdf2_salt": "dGhpc2lzYWJhZHNhbHR0b28=",
122//!    "pbkdf2_key": "C2kkRTybDzhkBGUkTn5Ys1LKPl8XINI46x74H4c9w8s=",
123//!    "pbkdf2_iter": 500000,
124//!    "client_cert": {
125//!      "allowed_cn": "i.am.trusted"
126//!    }
127//!  },
128//!  {
129//!    "username": "freddie",
130//!    "client_cert": {}
131//!  },
132//!  {
133//!    "username": "santa",
134//!    "password": "clara",
135//!    "client_cert": {}
136//!  }
137//! ]
138//! ```
139//!
140//! we can see that Eve needs to present a valid client certificate with a CN matching "i.am.trusted"
141//! and then also needs to provide the correct password. Freddie just needs to present a valid
142//! certificate that is signed by a certificate in the trust store. No password is required for
143//! him when logging in. Santa needs to provide a valid certificate and password but the CN can
144//! be anything.
145//!
146
147use async_trait::async_trait;
148use base64::Engine;
149use bytes::Bytes;
150use flate2::read::GzDecoder;
151use ipnet::Ipv4Net;
152use iprange::IpRange;
153use libunftp::auth::{AuthenticationError, Authenticator, DefaultUser};
154use ring::{
155    digest::SHA256_OUTPUT_LEN,
156    pbkdf2::{PBKDF2_HMAC_SHA256, verify},
157};
158use serde::Deserialize;
159use std::io::prelude::*;
160use std::{collections::HashMap, fs, num::NonZeroU32, path::Path, time::Duration};
161use tokio::time::sleep;
162use valid::{Validate, constraint::Length};
163
164#[derive(Deserialize, Clone, Debug)]
165struct ClientCertCredential {
166    allowed_cn: Option<String>,
167}
168
169#[derive(Deserialize, Clone, Debug)]
170#[serde(untagged)]
171enum Credentials {
172    Pbkdf2 {
173        username: String,
174        pbkdf2_salt: String,
175        pbkdf2_key: String,
176        pbkdf2_iter: NonZeroU32,
177        client_cert: Option<ClientCertCredential>,
178        allowed_ip_ranges: Option<Vec<String>>,
179    },
180    Plaintext {
181        username: String,
182        password: Option<String>,
183        client_cert: Option<ClientCertCredential>,
184        allowed_ip_ranges: Option<Vec<String>>,
185    },
186}
187
188/// This structure implements the libunftp `Authenticator` trait
189#[derive(Clone, Debug)]
190pub struct JsonFileAuthenticator {
191    credentials_map: HashMap<String, UserCreds>,
192}
193
194#[derive(Clone, Debug)]
195enum Password {
196    PlainPassword {
197        password: Option<String>,
198    },
199    Pbkdf2Password {
200        pbkdf2_salt: Bytes,
201        pbkdf2_key: Bytes,
202        pbkdf2_iter: NonZeroU32,
203    },
204}
205
206#[derive(Clone, Debug)]
207struct UserCreds {
208    pub password: Password,
209    pub client_cert: Option<ClientCertCredential>,
210    pub allowed_ip_ranges: Option<IpRange<Ipv4Net>>,
211}
212
213impl JsonFileAuthenticator {
214    /// Initialize a new [`JsonFileAuthenticator`] from file.
215    pub fn from_file<P: AsRef<Path>>(filename: P) -> Result<Self, Box<dyn std::error::Error>> {
216        let mut f = fs::File::open(&filename)?;
217
218        // The credentials file can be plaintext, gzipped, or gzipped+base64-encoded
219        // The gzip-base64 format is useful for overcoming configmap size limits in Kubernetes
220        let mut magic: [u8; 4] = [0; 4];
221        let n = f.read(&mut magic[..])?;
222        let is_gz = n > 2 && magic[0] == 0x1F && magic[1] == 0x8B && magic[2] == 0x8;
223        // the 3 magic bytes translate to "H4sI" in base64
224        let is_base64gz = n > 3 && magic[0] == b'H' && magic[1] == b'4' && magic[2] == b's' && magic[3] == b'I';
225
226        f.rewind()?;
227        let json: String = if is_gz | is_base64gz {
228            let mut gzdata: Vec<u8> = Vec::new();
229            if is_base64gz {
230                let mut b = Vec::new();
231                f.read_to_end(&mut b)?;
232                b.retain(|&x| x != b'\n' && x != b'\r');
233                gzdata = base64::engine::general_purpose::STANDARD.decode(b)?;
234            } else {
235                f.read_to_end(&mut gzdata)?;
236            }
237            let mut d = GzDecoder::new(&gzdata[..]);
238            let mut s = String::new();
239            d.read_to_string(&mut s)?;
240            s
241        } else {
242            let mut s = String::new();
243            f.read_to_string(&mut s)?;
244            s
245        };
246
247        Self::from_json(json)
248    }
249
250    /// Initialize a new [`JsonFileAuthenticator`] from json string.
251    pub fn from_json<T: Into<String>>(json: T) -> Result<Self, Box<dyn std::error::Error>> {
252        let credentials_list: Vec<Credentials> = serde_json::from_str::<Vec<Credentials>>(&json.into())?;
253        let map: Result<HashMap<String, UserCreds>, _> = credentials_list.into_iter().map(Self::list_entry_to_map_entry).collect();
254        Ok(JsonFileAuthenticator { credentials_map: map? })
255    }
256
257    fn list_entry_to_map_entry(user_info: Credentials) -> Result<(String, UserCreds), Box<dyn std::error::Error>> {
258        let map_entry = match user_info {
259            Credentials::Plaintext {
260                username,
261                password,
262                client_cert,
263                allowed_ip_ranges: ip_ranges,
264            } => (
265                username.clone(),
266                UserCreds {
267                    password: Password::PlainPassword { password },
268                    client_cert,
269                    allowed_ip_ranges: Self::parse_ip_range(username, ip_ranges)?,
270                },
271            ),
272            Credentials::Pbkdf2 {
273                username,
274                pbkdf2_salt,
275                pbkdf2_key,
276                pbkdf2_iter,
277                client_cert,
278                allowed_ip_ranges: ip_ranges,
279            } => (
280                username.clone(),
281                UserCreds {
282                    password: Password::Pbkdf2Password {
283                        pbkdf2_salt: base64::engine::general_purpose::STANDARD
284                            .decode(pbkdf2_salt)
285                            .map_err(|_| "Could not base64 decode the salt")?
286                            .into(),
287                        pbkdf2_key: base64::engine::general_purpose::STANDARD
288                            .decode(pbkdf2_key)
289                            .map_err(|_| "Could not decode base64")?
290                            .validate("pbkdf2_key", &Length::Max(SHA256_OUTPUT_LEN))
291                            .result()
292                            .map_err(|_| format!("Key of user \"{}\" is too long", username))?
293                            .unwrap() // Safe to use given Validated's API
294                            .into(),
295                        pbkdf2_iter,
296                    },
297                    client_cert,
298                    allowed_ip_ranges: Self::parse_ip_range(username, ip_ranges)?,
299                },
300            ),
301        };
302        Ok(map_entry)
303    }
304
305    fn parse_ip_range(username: String, ip_ranges: Option<Vec<String>>) -> Result<Option<IpRange<Ipv4Net>>, String> {
306        ip_ranges
307            .map(|v| {
308                let range: Result<IpRange<Ipv4Net>, _> = v
309                    .iter()
310                    .map(|s| s.parse::<Ipv4Net>().map_err(|_| format!("could not parse IP ranges for user {}", username)))
311                    .collect();
312                range
313            })
314            .transpose()
315    }
316
317    fn check_password(given_password: &str, actual_password: &Password) -> Result<(), ()> {
318        match actual_password {
319            Password::PlainPassword { password } => {
320                if let Some(pwd) = password {
321                    if pwd == given_password { Ok(()) } else { Err(()) }
322                } else {
323                    Err(())
324                }
325            }
326            Password::Pbkdf2Password {
327                pbkdf2_iter,
328                pbkdf2_salt,
329                pbkdf2_key,
330            } => verify(PBKDF2_HMAC_SHA256, *pbkdf2_iter, pbkdf2_salt, given_password.as_bytes(), pbkdf2_key).map_err(|_| ()),
331        }
332    }
333
334    fn ip_ok(creds: &libunftp::auth::Credentials, actual_creds: &UserCreds) -> bool {
335        match &actual_creds.allowed_ip_ranges {
336            Some(allowed) => match creds.source_ip {
337                std::net::IpAddr::V4(ref ip) => allowed.contains(ip),
338                _ => false,
339            },
340            None => true,
341        }
342    }
343}
344
345#[async_trait]
346impl Authenticator<DefaultUser> for JsonFileAuthenticator {
347    #[tracing_attributes::instrument]
348    async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<DefaultUser, AuthenticationError> {
349        let res = if let Some(actual_creds) = self.credentials_map.get(username) {
350            let client_cert = &actual_creds.client_cert;
351            let certificate = &creds.certificate_chain.as_ref().and_then(|x| x.first());
352
353            let ip_check_result = if !Self::ip_ok(creds, actual_creds) {
354                Err(AuthenticationError::IpDisallowed)
355            } else {
356                Ok(DefaultUser {})
357            };
358
359            let cn_check_result = match (&client_cert, certificate) {
360                // If client_cert is Some, it has an allowed_cn
361                // Option, if it is set, the client cert is checked,
362                // otherwise any trusted client cert will be accepted
363                (Some(client_cert), Some(cert)) => match (&client_cert.allowed_cn, cert) {
364                    (Some(cn), cert) => match cert.verify_cn(cn) {
365                        Ok(is_authorized) => {
366                            if is_authorized {
367                                Some(Ok(DefaultUser {}))
368                            } else {
369                                Some(Err(AuthenticationError::CnDisallowed))
370                            }
371                        }
372                        Err(e) => Some(Err(AuthenticationError::with_source("verify_cn", e))),
373                    },
374                    (None, _) => Some(Ok(DefaultUser {})),
375                },
376                (Some(_), None) => Some(Err(AuthenticationError::CnDisallowed)),
377                _ => None,
378            };
379
380            let pass_check_result = match &creds.password {
381                Some(given_password) => {
382                    if Self::check_password(given_password, &actual_creds.password).is_ok() {
383                        Some(Ok(DefaultUser {}))
384                    } else {
385                        Some(Err(AuthenticationError::BadPassword))
386                    }
387                }
388                None => None,
389            };
390
391            // the ip_check_result is returned at the end if all the other credentials are good.
392            // because from unauthorized sources, we want to know whether they posses valid credentials somehow
393            // but for logging purposes it would be better if we simply logged all of the results instead
394            match (pass_check_result, cn_check_result, ip_check_result) {
395                (None, None, _) => Err(AuthenticationError::BadPassword), // At least a password or client cert check is required
396                (Some(pass_res), None, ip_res) => {
397                    if pass_res.is_ok() {
398                        ip_res
399                    } else {
400                        pass_res
401                    }
402                }
403                (None, Some(cn_res), ip_res) => {
404                    if cn_res.is_ok() {
405                        ip_res
406                    } else {
407                        cn_res
408                    }
409                }
410                (Some(pass_res), Some(cn_res), ip_res) => match (pass_res, cn_res) {
411                    (Ok(_), Ok(_)) => ip_res,
412                    (Ok(_), Err(e)) => Err(e),
413                    (Err(e), Ok(_)) => Err(e),
414                    (Err(e), Err(_)) => Err(e), // AuthenticationError::BadPassword returned also if both password and CN are wrong
415                },
416            }
417        } else {
418            Err(AuthenticationError::BadUser)
419        };
420
421        if res.is_err() {
422            sleep(Duration::from_millis(1500)).await;
423        }
424
425        res
426    }
427
428    /// Tells whether its OK to not ask for a password when a valid client cert
429    /// was presented.
430    ///
431    /// For this JSON authenticator, if a certificate object is given
432    /// (optionally matched against client certificate of a specific
433    /// user during authentication), the user can omit the password as
434    /// a way to indicate that the user + client cert is sufficient
435    /// for authentication. If the password is given, then both are
436    /// required.
437    async fn cert_auth_sufficient(&self, username: &str) -> bool {
438        if let Some(actual_creds) = self.credentials_map.get(username) {
439            if let Password::PlainPassword { password: None } = &actual_creds.password {
440                return actual_creds.client_cert.is_some();
441            }
442        }
443        false
444    }
445
446    fn name(&self) -> &str {
447        std::any::type_name::<Self>()
448    }
449}
450
451mod test {
452    #[allow(unused_imports)]
453    use libunftp::auth::ClientCert;
454
455    #[tokio::test]
456    async fn test_json_auth() {
457        use super::*;
458
459        let json: &str = r#"[
460  {
461    "username": "alice",
462    "pbkdf2_salt": "dGhpc2lzYWJhZHNhbHQ=",
463    "pbkdf2_key": "jZZ20ehafJPQPhUKsAAMjXS4wx9FSbzUgMn7HJqx4Hg=",
464    "pbkdf2_iter": 500000
465  },
466  {
467    "username": "bella",
468    "pbkdf2_salt": "dGhpc2lzYWJhZHNhbHR0b28=",
469    "pbkdf2_key": "C2kkRTybDzhkBGUkTn5Ys1LKPl8XINI46x74H4c9w8s=",
470    "pbkdf2_iter": 500000
471  },
472  {
473    "username": "carol",
474    "password": "not so secure"
475  },
476  {
477    "username": "dan",
478    "password": "",
479    "allowed_ip_ranges": ["127.0.0.1/8"]
480  }
481]"#;
482        let json_authenticator = JsonFileAuthenticator::from_json(json).unwrap();
483        assert_eq!(
484            json_authenticator
485                .authenticate("alice", &"this is the correct password for alice".into())
486                .await
487                .unwrap(),
488            DefaultUser
489        );
490        assert_eq!(
491            json_authenticator
492                .authenticate("bella", &"this is the correct password for bella".into())
493                .await
494                .unwrap(),
495            DefaultUser
496        );
497        assert_eq!(json_authenticator.authenticate("carol", &"not so secure".into()).await.unwrap(), DefaultUser);
498        assert_eq!(json_authenticator.authenticate("dan", &"".into()).await.unwrap(), DefaultUser);
499        assert!(matches!(
500            json_authenticator.authenticate("carol", &"this is the wrong password".into()).await,
501            Err(AuthenticationError::BadPassword)
502        ));
503        assert!(matches!(
504            json_authenticator.authenticate("bella", &"this is the wrong password".into()).await,
505            Err(AuthenticationError::BadPassword)
506        ));
507        assert!(matches!(
508            json_authenticator.authenticate("chuck", &"12345678".into()).await,
509            Err(AuthenticationError::BadUser)
510        ));
511
512        assert_eq!(
513            json_authenticator
514                .authenticate(
515                    "dan",
516                    &libunftp::auth::Credentials {
517                        certificate_chain: None,
518                        password: Some("".into()),
519                        source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
520                    },
521                )
522                .await
523                .unwrap(),
524            DefaultUser
525        );
526
527        match json_authenticator
528            .authenticate(
529                "dan",
530                &libunftp::auth::Credentials {
531                    certificate_chain: None,
532                    password: Some("".into()),
533                    source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(128, 0, 0, 1)),
534                },
535            )
536            .await
537        {
538            Err(AuthenticationError::IpDisallowed) => (),
539            _ => panic!(),
540        }
541    }
542
543    #[tokio::test]
544    async fn test_json_cert_sufficient() {
545        use super::*;
546
547        let json: &str = r#"[
548  {
549    "username": "alice",
550    "password": "has a password"
551  },
552  {
553    "username": "bob",
554    "client_cert": {
555      "allowed_cn": "my.cert.is.everything"
556    }
557  },
558  {
559    "username": "carol",
560    "password": "This is ultimate security.",
561    "client_cert": {
562      "allowed_cn": "i.am.trusted"
563    }
564  },
565  {
566    "username": "dan",
567    "pbkdf2_salt": "dGhpc2lzYWJhZHNhbHQ=",
568    "pbkdf2_key": "jZZ20ehafJPQPhUKsAAMjXS4wx9FSbzUgMn7HJqx4Hg=",
569    "pbkdf2_iter": 500000
570  },
571  {
572    "username": "eve",
573    "pbkdf2_salt": "dGhpc2lzYWJhZHNhbHR0b28=",
574    "pbkdf2_key": "C2kkRTybDzhkBGUkTn5Ys1LKPl8XINI46x74H4c9w8s=",
575    "pbkdf2_iter": 500000,
576    "client_cert": {
577      "allowed_cn": "i.am.trusted"
578    }
579  },
580  {
581    "username": "freddie",
582    "client_cert": {}
583  },
584  {
585    "username": "santa",
586    "password": "clara",
587    "client_cert": {}
588  }  
589]"#;
590        let json_authenticator = JsonFileAuthenticator::from_json(json).unwrap();
591        assert!(!json_authenticator.cert_auth_sufficient("alice").await);
592        assert!(json_authenticator.cert_auth_sufficient("bob").await);
593        assert!(!json_authenticator.cert_auth_sufficient("carol").await);
594        assert!(!json_authenticator.cert_auth_sufficient("dan").await);
595        assert!(!json_authenticator.cert_auth_sufficient("eve").await);
596        assert!(json_authenticator.cert_auth_sufficient("freddie").await);
597        assert!(!json_authenticator.cert_auth_sufficient("santa").await);
598    }
599
600    #[tokio::test]
601    async fn test_json_cert_authenticate() {
602        use super::*;
603
604        // DER formatted certificate: subject= /CN=unftp-client.mysite.com/O=mysite.com/C=NL
605        let cert: &[u8] = &[
606            0x30, 0x82, 0x03, 0x1f, 0x30, 0x82, 0x02, 0x07, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, 0xc3, 0x3d, 0x48, 0x52, 0x68, 0x7e, 0x06, 0x83,
607            0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x40, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55,
608            0x04, 0x03, 0x0c, 0x13, 0x75, 0x6e, 0x66, 0x74, 0x70, 0x2d, 0x63, 0x61, 0x2e, 0x6d, 0x79, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x31,
609            0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0a, 0x6d, 0x79, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x0b, 0x30, 0x09,
610            0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x4e, 0x4c, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x31, 0x30, 0x36, 0x32, 0x35, 0x31, 0x32, 0x30, 0x38, 0x30,
611            0x38, 0x5a, 0x17, 0x0d, 0x32, 0x34, 0x30, 0x34, 0x31, 0x34, 0x31, 0x32, 0x30, 0x38, 0x30, 0x38, 0x5a, 0x30, 0x44, 0x31, 0x20, 0x30, 0x1e, 0x06,
612            0x03, 0x55, 0x04, 0x03, 0x0c, 0x17, 0x75, 0x6e, 0x66, 0x74, 0x70, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x6d, 0x79, 0x73, 0x69, 0x74,
613            0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0a, 0x6d, 0x79, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x63,
614            0x6f, 0x6d, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x4e, 0x4c, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a,
615            0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00,
616            0xec, 0xdf, 0x85, 0x48, 0xf4, 0x20, 0xdd, 0x52, 0x0b, 0x9c, 0x08, 0x6a, 0x78, 0x0f, 0x16, 0x16, 0x8b, 0x11, 0x79, 0xef, 0x32, 0xb6, 0x55, 0x90,
617            0x50, 0x31, 0x09, 0xf6, 0x1a, 0x99, 0xff, 0xa2, 0x51, 0x0f, 0x74, 0x2b, 0x80, 0xeb, 0x69, 0x8e, 0x42, 0x53, 0x54, 0x7d, 0xf0, 0x13, 0x92, 0x2d,
618            0x86, 0xda, 0x3b, 0x7d, 0x2b, 0x19, 0x15, 0x3a, 0xeb, 0xb0, 0xd8, 0x33, 0xb4, 0x4c, 0xb0, 0x4e, 0x63, 0x32, 0x35, 0x8e, 0x30, 0xc9, 0xfe, 0xaf,
619            0xcc, 0xc7, 0xa6, 0xdc, 0xbf, 0x83, 0x16, 0x6f, 0xdc, 0xc5, 0xdf, 0x10, 0x24, 0x45, 0xb0, 0x7c, 0x5b, 0x36, 0xc7, 0xcd, 0xf7, 0x5b, 0x1e, 0x9f,
620            0xae, 0x80, 0xd8, 0x0e, 0x27, 0x0f, 0xb6, 0x04, 0x16, 0xa5, 0x4b, 0x58, 0x4c, 0xd5, 0x25, 0x1b, 0x99, 0x48, 0xd4, 0x02, 0x85, 0x25, 0x54, 0x31,
621            0x2b, 0x77, 0x4d, 0xe9, 0x81, 0xbe, 0x81, 0x32, 0xee, 0x16, 0x59, 0x21, 0x82, 0x8c, 0x7d, 0x9f, 0xca, 0x93, 0xe4, 0x93, 0xb8, 0x2f, 0x0f, 0x16,
622            0xa6, 0x43, 0x3e, 0xa6, 0x4f, 0xe0, 0xbd, 0xd5, 0x30, 0x05, 0x8e, 0xe1, 0x85, 0x12, 0xee, 0xbe, 0xa0, 0x1a, 0xa0, 0x63, 0x16, 0x3c, 0xf7, 0x73,
623            0xe1, 0xe6, 0x76, 0xe5, 0x98, 0x82, 0x59, 0x88, 0xe4, 0xa4, 0xe2, 0xf9, 0xc7, 0xb8, 0x21, 0x4c, 0x3f, 0x9f, 0xeb, 0x06, 0x13, 0xf8, 0x67, 0x45,
624            0x4e, 0xf0, 0xf8, 0x07, 0x59, 0x1f, 0x9d, 0x52, 0xb9, 0x19, 0xdb, 0x0e, 0x36, 0x92, 0x39, 0x85, 0xa5, 0x18, 0x30, 0x9f, 0x6b, 0x39, 0x9c, 0xba,
625            0x09, 0xf0, 0xc5, 0xfc, 0x21, 0xf0, 0x27, 0xf9, 0x97, 0x45, 0x96, 0x38, 0x25, 0x56, 0x59, 0x18, 0x9c, 0x99, 0x75, 0x0a, 0x86, 0xb8, 0xc1, 0xb6,
626            0x2c, 0xbe, 0x53, 0x4a, 0xe8, 0xd2, 0x8a, 0xf8, 0x47, 0xc3, 0x71, 0x60, 0x28, 0x88, 0xe1, 0x13, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x18, 0x30,
627            0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x0d, 0x30, 0x0b, 0x82, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x30,
628            0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0xb4, 0x57, 0x68, 0x3e, 0x4b,
629            0x9b, 0x89, 0xbd, 0x60, 0x2b, 0xa7, 0x02, 0x04, 0xbf, 0xff, 0x56, 0xd5, 0xce, 0x1c, 0x20, 0x7f, 0x92, 0xa9, 0xd3, 0xf3, 0x8b, 0xfd, 0x8c, 0x41,
630            0x19, 0xb5, 0xe5, 0x01, 0x0a, 0x5f, 0x2f, 0x86, 0x0b, 0x26, 0x71, 0x89, 0x7b, 0x0f, 0x2c, 0x1b, 0x54, 0xc9, 0x3a, 0xf4, 0x37, 0xdf, 0x52, 0x7d,
631            0x87, 0x30, 0x49, 0xbf, 0x7c, 0x84, 0x46, 0x3c, 0x21, 0xbe, 0x99, 0x8f, 0x69, 0x56, 0x8c, 0x5f, 0x7c, 0xb0, 0xe9, 0xdc, 0xbd, 0xfa, 0xbe, 0x26,
632            0xb6, 0xfa, 0xa5, 0xdd, 0x9b, 0x41, 0xe9, 0x2c, 0xd2, 0x21, 0x42, 0xe7, 0x67, 0xcc, 0x01, 0xda, 0x7a, 0xb7, 0x84, 0xa7, 0x83, 0x91, 0x37, 0x43,
633            0x04, 0x3e, 0xde, 0x41, 0xba, 0x7d, 0xa3, 0x5c, 0xc0, 0x6f, 0x8c, 0x2c, 0x1c, 0xa8, 0x86, 0xa7, 0x38, 0xa4, 0x1f, 0x58, 0x7d, 0xb2, 0xf7, 0xc8,
634            0xe2, 0x3c, 0x10, 0xd9, 0x69, 0x4b, 0xef, 0x3d, 0x47, 0x39, 0xf8, 0x3e, 0x87, 0x67, 0x7e, 0xfc, 0x43, 0xbb, 0x01, 0x7c, 0xa2, 0x26, 0xb9, 0xb1,
635            0x3c, 0x1d, 0xd4, 0xbe, 0xa0, 0x02, 0x0d, 0x10, 0x62, 0xd9, 0xe3, 0x7f, 0x90, 0x30, 0x89, 0x64, 0x37, 0x90, 0xcd, 0x34, 0xd4, 0x03, 0x9f, 0x96,
636            0x80, 0xb1, 0xaa, 0x93, 0x59, 0x23, 0xd7, 0xad, 0x3e, 0x13, 0x76, 0x02, 0x1f, 0xd2, 0xa6, 0x8b, 0x44, 0x26, 0x8f, 0x1d, 0xf8, 0x60, 0xba, 0xc5,
637            0x52, 0x31, 0x26, 0x64, 0xca, 0x7e, 0x3f, 0xe9, 0xba, 0x72, 0xdc, 0x80, 0xfd, 0x4b, 0x10, 0x66, 0x5d, 0x85, 0xd3, 0xa3, 0x2b, 0xe6, 0x73, 0x4a,
638            0xcf, 0xba, 0xe0, 0x48, 0x4f, 0x00, 0xed, 0xaa, 0xb3, 0x75, 0xe8, 0xbc, 0xf3, 0xba, 0xb7, 0x4d, 0x59, 0x17, 0xde, 0xb5, 0x2c, 0x8d, 0x9a, 0x88,
639            0x34, 0x02, 0x19, 0x9c, 0x22, 0x56, 0x26, 0x3f, 0x3a, 0x6f, 0x0f,
640        ];
641
642        let json: &str = r#"[
643  {
644    "username": "alice",
645    "password": "has a password",
646    "client_cert": {
647      "allowed_cn": "unftp-client.mysite.com"
648    }
649  },
650  {
651    "username": "bob",
652    "client_cert": {
653      "allowed_cn": "unftp-client.mysite.com"
654    }
655  },
656  {
657    "username": "carol",
658    "client_cert": {
659      "allowed_cn": "unftp-other-client.mysite.com"
660    }
661  },
662  {
663    "username": "dean",
664    "client_cert": {}
665  }
666]"#;
667        let json_authenticator = JsonFileAuthenticator::from_json(json).unwrap();
668        let client_cert: Vec<u8> = cert.to_vec();
669
670        // correct certificate and password combo authenticates successfully
671        assert_eq!(
672            json_authenticator
673                .authenticate(
674                    "alice",
675                    &libunftp::auth::Credentials {
676                        certificate_chain: Some(vec![ClientCert(client_cert.clone())]),
677                        password: Some("has a password".into()),
678                        source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
679                    },
680                )
681                .await
682                .unwrap(),
683            DefaultUser
684        );
685
686        // correct password but missing certificate fails
687        match json_authenticator
688            .authenticate(
689                "alice",
690                &libunftp::auth::Credentials {
691                    certificate_chain: None,
692                    password: Some("has a password".into()),
693                    source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
694                },
695            )
696            .await
697        {
698            Err(AuthenticationError::CnDisallowed) => (),
699            _ => panic!(),
700        }
701
702        // correct certificate and no password needed according to json file authenticates successfully
703        assert_eq!(
704            json_authenticator
705                .authenticate(
706                    "bob",
707                    &libunftp::auth::Credentials {
708                        certificate_chain: Some(vec![ClientCert(client_cert.clone())]),
709                        password: None,
710                        source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
711                    },
712                )
713                .await
714                .unwrap(),
715            DefaultUser
716        );
717
718        // certificate with incorrect CN and no password needed according to json file fails to authenticate
719        match json_authenticator
720            .authenticate(
721                "carol",
722                &libunftp::auth::Credentials {
723                    certificate_chain: Some(vec![ClientCert(client_cert.clone())]),
724                    password: None,
725                    source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
726                },
727            )
728            .await
729        {
730            Err(AuthenticationError::CnDisallowed) => (),
731            _ => panic!(),
732        }
733
734        // any trusted certificate without password according to json file authenticates successfully
735        assert_eq!(
736            json_authenticator
737                .authenticate(
738                    "dean",
739                    &libunftp::auth::Credentials {
740                        certificate_chain: Some(vec![ClientCert(client_cert.clone())]),
741                        password: None,
742                        source_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
743                    },
744                )
745                .await
746                .unwrap(),
747            DefaultUser
748        );
749    }
750}