From a286b5e41881b268a0f679e08901a472b98a15a8 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Mon, 20 May 2024 09:08:36 -0400 Subject: [PATCH] feat: improve client certificate authentication --- .vscode/settings.json | 1 + README.md | 3 +- apps/gpclient/src/connect.rs | 12 ++- crates/gpapi/src/gp_params.rs | 14 +--- crates/gpapi/src/utils/request.rs | 80 +++++++++++++++---- .../files/badssl.com-client-unencrypted.pem | 62 ++++++++++++++ 6 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 crates/gpapi/tests/files/badssl.com-client-unencrypted.pem diff --git a/.vscode/settings.json b/.vscode/settings.json index bff33a21..0cb6f063 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "authcookie", + "badssl", "bincode", "chacha", "clientos", diff --git a/README.md b/README.md index dcd5f4e1..6c3c4311 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati - [x] Support both SSO and non-SSO authentication - [x] Support the FIDO2 authentication (e.g., YubiKey) - [x] Support authentication using default browser +- [x] Support client certificate authentication - [x] Support multiple portals - [x] Support gateway selection - [x] Support connect gateway directly @@ -74,7 +75,7 @@ sudo apt-get install globalprotect-openconnect > > For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`. -#### **Ubuntu 24.04** +#### **Ubuntu 24.04 and later** The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually: diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index b7af3eec..1e83feab 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -42,9 +42,13 @@ pub(crate) struct ConnectArgs { )] hip: bool, - #[arg(short, long, help = "Use SSL client certificate file (.pem or .p12)")] + #[arg( + short, + long, + help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format" + )] certificate: Option, - #[arg(short = 'k', long, help = "Use SSL private key file (.pem)")] + #[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")] sslkey: Option, #[arg(short = 'p', long, help = "The key passphrase of the private key")] key_password: Option, @@ -122,7 +126,7 @@ impl<'a> ConnectHandler<'a> { loop { let Err(err) = self.handle_impl().await else { - return Ok(()) + return Ok(()); }; let Some(root_cause) = err.root_cause().downcast_ref::() else { @@ -133,7 +137,7 @@ impl<'a> ConnectHandler<'a> { RequestIdentityError::NoKey => { eprintln!("ERROR: No private key found in the certificate file"); eprintln!("ERROR: Please provide the private key file using the `-k` option"); - return Ok(()) + return Ok(()); } RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => { // Decrypt the private key error, ask for the key password diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs index 85af3e06..f72eba3b 100644 --- a/crates/gpapi/src/gp_params.rs +++ b/crates/gpapi/src/gp_params.rs @@ -1,13 +1,11 @@ use std::collections::HashMap; +use log::info; use reqwest::Client; use serde::{Deserialize, Serialize}; use specta::Type; -use crate::{ - utils::request::{create_identity_from_pem, create_identity_from_pkcs12}, - GP_USER_AGENT, -}; +use crate::{utils::request::create_identity, GP_USER_AGENT}; #[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] pub enum ClientOs { @@ -255,12 +253,8 @@ impl TryFrom<&GpParams> for Client { .user_agent(&value.user_agent); if let Some(cert) = value.certificate.as_deref() { - // .p12 or .pfx file - let identity = if cert.ends_with(".p12") || cert.ends_with(".pfx") { - create_identity_from_pkcs12(cert, value.key_password.as_deref())? - } else { - create_identity_from_pem(cert, value.sslkey.as_deref(), value.key_password.as_deref())? - }; + info!("Using client certificate authentication..."); + let identity = create_identity(cert, value.sslkey.as_deref(), value.key_password.as_deref())?; builder = builder.identity(identity); } diff --git a/crates/gpapi/src/utils/request.rs b/crates/gpapi/src/utils/request.rs index f164c2d5..e992f2a8 100644 --- a/crates/gpapi/src/utils/request.rs +++ b/crates/gpapi/src/utils/request.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{borrow::Cow, fs}; use anyhow::bail; use log::warn; @@ -17,24 +17,31 @@ pub enum RequestIdentityError { } /// Create an identity object from a certificate and key -pub fn create_identity_from_pem(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result { +/// The file is expected to be the PKCS#8 PEM or PKCS#12 format +/// When using a PKCS#12 file, the key is NOT required, but a passphrase is required +pub fn create_identity(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result { + if cert.ends_with(".p12") || cert.ends_with(".pfx") { + create_identity_from_pkcs12(cert, passphrase) + } else { + create_identity_from_pem(cert, key, passphrase) + } +} + +fn create_identity_from_pem(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result { let cert_pem = fs::read(cert).map_err(|err| anyhow::anyhow!("Failed to read certificate file: {}", err))?; - // Get the private key pem - let key_pem = match key { - Some(key) => { - let pem_file = fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?; - pem::parse(pem_file)? - } - None => { - // If key is not provided, find the private key in the cert pem - parse_many(&cert_pem)? - .into_iter() - .find(|pem| pem.tag().ends_with("PRIVATE KEY")) - .ok_or(RequestIdentityError::NoKey)? - } + // Use the certificate as the key if no key is provided + let key_pem_file = match key { + Some(key) => Cow::Owned(fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?), + None => Cow::Borrowed(&cert_pem), }; + // Find the private key in the pem file + let key_pem = parse_many(key_pem_file.as_ref())? + .into_iter() + .find(|pem| pem.tag().ends_with("PRIVATE KEY")) + .ok_or(RequestIdentityError::NoKey)?; + // The key pem could be encrypted, so we need to decrypt it let decrypted_key_pem = if key_pem.tag().ends_with("ENCRYPTED PRIVATE KEY") { let passphrase = passphrase.ok_or_else(|| { @@ -56,7 +63,7 @@ pub fn create_identity_from_pem(cert: &str, key: Option<&str>, passphrase: Optio Ok(identity) } -pub fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result { +fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result { let pkcs12 = fs::read(pkcs12)?; let Some(passphrase) = passphrase else { @@ -89,4 +96,45 @@ mod tests { assert!(identity.is_ok()); } + + #[test] + fn create_identity_from_pem_unencrypted_key() { + let cert = "tests/files/badssl.com-client-unencrypted.pem"; + let identity = create_identity_from_pem(cert, None, None); + println!("{:?}", identity); + + assert!(identity.is_ok()); + } + + #[test] + fn create_identity_from_pem_cert_and_encrypted_key() { + let cert = "tests/files/badssl.com-client.pem"; + let key = "tests/files/badssl.com-client.pem"; + let passphrase = "badssl.com"; + + let identity = create_identity_from_pem(cert, Some(key), Some(passphrase)); + + assert!(identity.is_ok()); + } + + #[test] + fn create_identity_from_pem_cert_and_encrypted_key_no_passphrase() { + let cert = "tests/files/badssl.com-client.pem"; + let key = "tests/files/badssl.com-client.pem"; + + let identity = create_identity_from_pem(cert, Some(key), None); + + assert!(identity.is_err()); + assert!(identity.unwrap_err().to_string().contains("No passphrase provided")); + } + + #[test] + fn create_identity_from_pem_cert_and_unencrypted_key() { + let cert = "tests/files/badssl.com-client.pem"; + let key = "tests/files/badssl.com-client-unencrypted.pem"; + + let identity = create_identity_from_pem(cert, Some(key), None); + + assert!(identity.is_ok()); + } } diff --git a/crates/gpapi/tests/files/badssl.com-client-unencrypted.pem b/crates/gpapi/tests/files/badssl.com-client-unencrypted.pem new file mode 100644 index 00000000..d914cff7 --- /dev/null +++ b/crates/gpapi/tests/files/badssl.com-client-unencrypted.pem @@ -0,0 +1,62 @@ +Bag Attributes + localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B +subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate +issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority +-----BEGIN CERTIFICATE----- +MIIEnTCCAoWgAwIBAgIJAPfJjkenM2ooMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjQwNTE3MTc1OTMyWhcNMjYwNTE3 +MTc1OTMyWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG +A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC +YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08 +utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR +WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi +DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8 +w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/ +s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG +CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB +AE6iDW5Lv5I0bJY6TGxJUoB4rcsbbtEP4O4MT14GP7j7I48V09VBG9yjskYze0Ls +Xb9mQpEpPyQLTDJIWu/ic/y5SMnelCjUxmfl37cfNLJajQZxc4FDEUSemrPKpEkB +UzHNkxw9LSzqsyxnQmMIGoN+ZNCFoV7s5pekzPfgZj5+s7a+oiF/AzhOWZzF7vaM +aclX7KCeENQV+q0giDjsGIHI6BevUHYkglocEqff+rIDHjjLxHLPooflV50M+ifc +4uJdHgG8hwKxd1uf3LImUsquiBrW5CO6KCgwLrtQNe11pQHpY0urZxK/tnAj7QtD +v/O1ryd/3+b0Gx14TyulMtcaLHsE94ppwjcxpYGNcyH+M39OMihuR2aqmkrqcZd/ +VWop1cNwZgPtCNVvfivRpX52NLI5I0eMfs6jeTMr719hdAby3akoiNLN3YNKrdrp +pyRz/sUFGO8AHHECXA15KTeMBNfZnO32ZAZ4jHyyDBO1A5f9iDbErhXfIpeRCrCO +gM9MLuO4YEMG1Skp+qaw7SIaG+oi2t4lbVRr3LOv0Hfkjjb7bVjfWSwLBPH/gv0E +ZL6G0p7PjeoCh4obS3Y1yxfNlPR6RQwWl1wve+Nkmf5sDCmgr3P0512ZuvqkbKkB +/syiAWDsYzFuq2Ntv2ljTYPEPwXEIQcpsagDRL6WzoLR +-----END CERTIFICATE----- +Bag Attributes + localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHN18R6x5Oz+u6 +SOXLoxIscz5GHR6cDcCLgyPax2XfXHdJs+h6fTy61WGM+aXEhR2SIwbj5997s34m +0MsbvkJrFmn0LHK1fuTLCihEEmxGdCGZA9xrwxFYAkEjP7D8v7cAWRMipYF/JP7V +U7xNUo+QSkZ0sOi9k6bNkABKL3+yP6PqAzsBoKIN5lN/YRLrppsDmk6nrRDo4R3C +D+8JQl9quEoOmL22Pc/qpOjL1jgOIFSE5y3gwbzDlfCYoAL5V+by1vu0yJShTTK8 +oo5wvphcFfEHaQ9w5jFg2htdq99UER3BKuNDuL+zejqGQZCWb0Xsk8S5WBuX8l3B +rrg5giqNAgMBAAECggEAVRB/t9b9igmeTlzyQpHPIMvUu3uTpm742JmWpcSe61FA +XmhDzInNdLnIfbnb3p44kj4Coy5PbzKlm01sbNxA4BkiBPE1yen1J/2eU/LJ6QuN +jRjo9drFfR75UWPQ3xu9uJhQY2rocLILXmvy69FlG+ebThh8SPbTMtNaTFMb47An +pk2FrW9+rzPswbklOxls/SDt78usRvfAjslm73IdBTOrbceF+GmYs3/SXz1gu05p +LxY2rhC8piBlqnD/QbXBahZbhjb9SkDFn2typMFZKkJIIKDJaOI2E9tIlZ97/0nZ +txqchMty8IuU9YYAfLXCmj2IEfnvLtL7thLfKLuWAQKBgQDyXBpEgKFzfy2a1AI0 ++1qL/u5UN14l7S6/wmyDTgVMXwoxhwPRXWD5PutQ8D6tMfC/y4AYt3OXg1blCvLD +XysNj5SK+dpmQR0SyeWjd9zwxJAXvx0McJefCYd86YGcGhJsuX5bkHIeQlEc6df7 +yoqr1480VQx/+Fk1i6Zr0EIUFQKBgQDSbalUOfXZh2EVRQEgf3VoPlxAiwGGQcVT +i+pbjMG3pOwmkVyJZusGtN5HN4Oi7n1oiyfMYGsszKQ5j4TDBGS70pNUzhTv3Vn8 +0Vsfz0arJRqJxviiv4FfDmsYXwObNKwOjR+LEn1NUPkOYOLdz1lDuWOu11LE90Dy +Q6hg8WwCmQKBgQDTy5lI9AAjpqh7/XpQQrhGT2qHPjuQeU25Vnbt6GjI7OVDkvHL +LQdpyYprGQgs4s+5TGWNNARYC/cMAh1Ujv5Yw3jUWrR5V73IhZeg20bBQYWKuwDv +thVKblFw377cZAxl51R9QCX6O4oW8mRFLiMxORd0bD6YNrf/CyNMZJraYQKBgAE7 +o0JbFJWxtV/qh5cpKAb0VpYKOngO6pkSuMzQhlINJVUUhPZJJBdl9+dy69KIkzOJ +nTIVXotkp5GuxZhe7jgrg7F7g6PkKCLTFzWYgVF/ZihoggxyEs/7xaTe6aZ/KILt +UMH/2bwaPVtYNfwWuu8qpurfWBzPVhIVU2c+AuQBAoGAXMbw10vyiznlhyMFw5kx +SzlBMqJBLJkzQBtpvXuT0lqqxTSNC3N4WxgVOLCHa6HqXiB0790YL8/RWunsXTk2 +c7ugThP6iMPNVAycWkIF4vvHTwZ9RCSmEQabRaqGGLz/bhLL3fi3lPGCR+iW2Dxq +GTH3fhaM/pZZGdIC75x/69Y= +-----END PRIVATE KEY-----