// SPDX-FileCopyrightText: 2023 Wiktor Kwapisiewicz // SPDX-License-Identifier: Apache-2.0 OR MIT use std::{ fs::{self, File}, io::Read, }; use rstest::rstest; use sequoia_openpgp::{parse::Parse, Cert}; use ssh_openpgp_auth::{authenticate, Authenticate}; #[rstest] #[case(false, "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF\n")] #[case(true, r#"# Downloaded certificate: D9E95D7F42E87610676C40B47E8432836DA1625E # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E, exporting subkey F5CEDEED08E9EA536034F5823475162385DF08AF example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF "#)] fn authenticate_with_cold_cache( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://example.com/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/example.asc")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![]) } } authenticate( Authenticate { host: "example.com".into(), time: Some("2023-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verbose, ..Default::default() }, &mut MockEffects, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); assert!( !dir_diff::is_different(tmp_dir, "fixtures/single-cert")?, "Two directories should be the same" ); Ok(()) } #[rstest] #[case(false, "")] #[case(true, r#"# Downloaded certificate: F6D6B6308F28DC1B6490DD69F0EE590B9EBB1F35 # Certificate F6D6B6308F28DC1B6490DD69F0EE590B9EBB1F35, skipping subkey AF88615FD6397A6C438AF3DFB13CCA164CE61470 as it is revoked "#)] fn authenticate_with_cold_cache_and_revoked_subkey( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://example.com/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/revoked_subkey_example.asc")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![]) } } authenticate( Authenticate { host: "example.com".into(), time: Some("2023-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verbose, ..Default::default() }, &mut MockEffects, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); Ok(()) } #[rstest] #[case(false, r#"example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF "#)] #[case(true, r#"# Found local cert: D9E95D7F42E87610676C40B47E8432836DA1625E # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E, exporting subkey F5CEDEED08E9EA536034F5823475162385DF08AF example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF "#)] fn authenticate_with_hot_cache( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); fs::remove_dir(&tmp_dir)?; copy_dir::copy_dir("fixtures/single-cert", &tmp_dir)?; let mut v = vec![]; struct MockEffects; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, _url: String) -> Result, ssh_openpgp_auth::Error> { panic!("The cert is still valid: the HTTP request should not occur."); } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![]) } } authenticate( Authenticate { host: "example.com".into(), time: Some("2023-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verbose, ..Default::default() }, &mut MockEffects, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); assert!( !dir_diff::is_different(tmp_dir, "fixtures/single-cert")?, "Two directories should be the same" ); Ok(()) } #[rstest] #[case(false, "")] #[case( true, r#"# Downloaded certificate: D9E95D7F42E87610676C40B47E8432836DA1625E # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E is already expired at 2027-12-20 16:37:00 UTC "# )] fn authenticate_with_hot_cache_but_expired( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); fs::remove_dir(&tmp_dir)?; copy_dir::copy_dir("fixtures/single-cert", &tmp_dir)?; let mut v = vec![]; struct MockEffects { request_made: bool, } impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://example.com/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/example.asc")?; f.read_to_end(&mut v)?; self.request_made = true; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![]) } } let mut fx = MockEffects { request_made: false, }; authenticate( Authenticate { host: "example.com".into(), time: Some("2027-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verbose, ..Default::default() }, &mut fx, &mut v, )?; assert!( fx.request_made, "Key is in cache but it's expired: expecting HTTP request" ); let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); assert!( !dir_diff::is_different(tmp_dir, "fixtures/single-cert")?, "Two directories should be the same" ); Ok(()) } #[rstest] #[case(false, "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF\n")] #[case(true, r#"# Downloaded certificate: D9E95D7F42E87610676C40B47E8432836DA1625E # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E found in the DNS zone # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E, exporting subkey F5CEDEED08E9EA536034F5823475162385DF08AF example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF "#)] fn authenticate_with_cold_cache_and_keyoxide_matching( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects { dns_query_made: bool, } let mut e = MockEffects { dns_query_made: false, }; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://example.com/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/example.asc")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { self.dns_query_made = true; Ok(vec![ "openpgp4fpr:d9e95d7f42e87610676c40b47e8432836da1625e".into() ]) } } authenticate( Authenticate { host: "example.com".into(), time: Some("2023-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verify_dns_proof: true, verbose, ..Default::default() }, &mut e, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); assert!(e.dns_query_made, "Expecting DNS query to occur"); Ok(()) } #[rstest] #[case(false, "")] #[case( true, r#"# Downloaded certificate: D9E95D7F42E87610676C40B47E8432836DA1625E # Certificate D9E95D7F42E87610676C40B47E8432836DA1625E NOT found in the DNS zone "# )] fn authenticate_with_cold_cache_and_keyoxide_non_matching( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects { dns_query_made: bool, } let mut e = MockEffects { dns_query_made: false, }; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://example.com/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/example.asc")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { self.dns_query_made = true; Ok(vec![]) } } authenticate( Authenticate { host: "example.com".into(), time: Some("2023-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verify_dns_proof: true, verbose, ..Default::default() }, &mut e, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); assert!(e.dns_query_made, "Expecting DNS query to occur"); Ok(()) } #[rstest] #[case(false, "metacode.biz ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIySQ5AP9fj3hBjGvEQv7NH3gVtjXIHpr3P6O6NMy+Ryz981vW0Ttcer6beAxtsq7S4N/2d8X86to3f2uiXZe2YWxIZ6BWD233Z5SL3YHKr3VwxnC8fK9xOHAzaFDUjkYKbyBPXsHWe3WpbeMbNL8BmlakmBxhGg7H488xr6pzUjUNCNFBDcpYPEz3oyD73OSX6eqOEECwpLBkLFugl2+k8MWQp8qRNbaiWA+jWTK+nd0zdHQOKSrBmfYJq+MG+BoamCwPc/UhPUv+ufXPIZe6Oj+tRdd3EgOWaUGRfUJWZ0O6N8fTqc1lbgb6NfSuOEsshm28uv92cjXUlyInYbQL 6C32E218FEF4287E8E1C25C94CE0B6FCAA9ABA52\nmetacode.biz ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILG5xlylLX9fysPiPwZzCmgSiazwmZquSlMHAcwOsZko C7D2A55513B710D03A2D7FB69B8D5D94932D8A01\n")] #[case(true, r#"# Downloaded certificate: 0E3BB828432962F4E33C9A74D1F809BB3F02EDE9 # Certificate 0E3BB828432962F4E33C9A74D1F809BB3F02EDE9, exporting subkey 6C32E218FEF4287E8E1C25C94CE0B6FCAA9ABA52 metacode.biz ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIySQ5AP9fj3hBjGvEQv7NH3gVtjXIHpr3P6O6NMy+Ryz981vW0Ttcer6beAxtsq7S4N/2d8X86to3f2uiXZe2YWxIZ6BWD233Z5SL3YHKr3VwxnC8fK9xOHAzaFDUjkYKbyBPXsHWe3WpbeMbNL8BmlakmBxhGg7H488xr6pzUjUNCNFBDcpYPEz3oyD73OSX6eqOEECwpLBkLFugl2+k8MWQp8qRNbaiWA+jWTK+nd0zdHQOKSrBmfYJq+MG+BoamCwPc/UhPUv+ufXPIZe6Oj+tRdd3EgOWaUGRfUJWZ0O6N8fTqc1lbgb6NfSuOEsshm28uv92cjXUlyInYbQL 6C32E218FEF4287E8E1C25C94CE0B6FCAA9ABA52 # Certificate 0E3BB828432962F4E33C9A74D1F809BB3F02EDE9, exporting subkey C7D2A55513B710D03A2D7FB69B8D5D94932D8A01 metacode.biz ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILG5xlylLX9fysPiPwZzCmgSiazwmZquSlMHAcwOsZko C7D2A55513B710D03A2D7FB69B8D5D94932D8A01 "#)] fn authenticate_with_two_subkeys( #[case] verbose: bool, #[case] expected_output: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, url: String) -> Result, ssh_openpgp_auth::Error> { assert_eq!(url, "https://metacode.biz/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth"); let mut v = vec![]; let mut f = File::open("fixtures/metacode.asc")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![]) } } authenticate( Authenticate { host: "metacode.biz".into(), time: Some("2024-12-20T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), verbose, ..Default::default() }, &mut MockEffects, &mut v, )?; let s = String::from_utf8_lossy(&v); assert_eq!(expected_output, s); Ok(()) } #[rstest] #[case(true, false, "dns")] #[case(false, true, "wot")] #[case(true, true, "dns wot")] fn authenticate_and_store( #[case] verify_dns_proof: bool, #[case] verify_wot: bool, #[case] expected_verification: &str, ) -> testresult::TestResult { let tmp_dir = tempfile::tempdir()?.into_path(); std::fs::copy("fixtures/trust-root.pgp", tmp_dir.join("trust-root"))?; std::fs::create_dir(tmp_dir.join("86"))?; std::fs::copy( "fixtures/86-3d6704479ed10029129002ce7063afb14c5d8b.pgp", tmp_dir.join("86/3d6704479ed10029129002ce7063afb14c5d8b"), )?; std::fs::create_dir(tmp_dir.join("87"))?; std::fs::copy( "fixtures/87-a9d901712393bfaef89f102b6433d1c5f8039a.pgp", tmp_dir.join("87/a9d901712393bfaef89f102b6433d1c5f8039a"), )?; eprintln!("Using the following cert-d: {}", tmp_dir.display()); let mut v = vec![]; struct MockEffects; impl ssh_openpgp_auth::Effects for MockEffects { fn get(&mut self, _url: String) -> Result, ssh_openpgp_auth::Error> { let mut v = vec![]; let mut f = File::open("fixtures/metacode-auth.pgp")?; f.read_to_end(&mut v)?; Ok(v) } fn dns_query( &mut self, _nameserver: std::net::SocketAddr, _name: &str, ) -> Result, ssh_openpgp_auth::Error> { Ok(vec![ "openpgp4fpr:0e3bb828432962f4e33c9a74d1f809bb3f02ede9".into() ]) } } authenticate( Authenticate { host: "metacode.biz".into(), time: Some("2024-01-31T16:37:00Z".parse()?), cert_store: Some(tmp_dir.clone()), store_verifications: true, verify_dns_proof, verify_wot, ..Default::default() }, &mut MockEffects, &mut v, )?; assert!(!v.is_empty(), "The cert export to SSH worked"); let cert_file = tmp_dir.join("0e/3bb828432962f4e33c9a74d1f809bb3f02ede9"); assert!(cert_file.exists(), "cert was imported"); let mut values = vec![]; let cert = Cert::from_file(cert_file)?; for uid in cert.userids() { for sig in uid.signatures() { for value in sig.notation("ssh-openpgp-auth-verification@metacode.biz") { values.push(String::from_utf8_lossy(value)); } } } assert_eq!(1, values.len()); assert_eq!(expected_verification, values[0]); Ok(()) }