mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
feat: improve client certificate authentication
This commit is contained in:
parent
882ab4001d
commit
a286b5e418
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"authcookie",
|
"authcookie",
|
||||||
|
"badssl",
|
||||||
"bincode",
|
"bincode",
|
||||||
"chacha",
|
"chacha",
|
||||||
"clientos",
|
"clientos",
|
||||||
|
@ -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 both SSO and non-SSO authentication
|
||||||
- [x] Support the FIDO2 authentication (e.g., YubiKey)
|
- [x] Support the FIDO2 authentication (e.g., YubiKey)
|
||||||
- [x] Support authentication using default browser
|
- [x] Support authentication using default browser
|
||||||
|
- [x] Support client certificate authentication
|
||||||
- [x] Support multiple portals
|
- [x] Support multiple portals
|
||||||
- [x] Support gateway selection
|
- [x] Support gateway selection
|
||||||
- [x] Support connect gateway directly
|
- [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`.
|
> 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:
|
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:
|
||||||
|
|
||||||
|
@ -42,9 +42,13 @@ pub(crate) struct ConnectArgs {
|
|||||||
)]
|
)]
|
||||||
hip: bool,
|
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<String>,
|
certificate: Option<String>,
|
||||||
#[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<String>,
|
sslkey: Option<String>,
|
||||||
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
|
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
|
||||||
key_password: Option<String>,
|
key_password: Option<String>,
|
||||||
@ -122,7 +126,7 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Err(err) = self.handle_impl().await else {
|
let Err(err) = self.handle_impl().await else {
|
||||||
return Ok(())
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
|
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
|
||||||
@ -133,7 +137,7 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
RequestIdentityError::NoKey => {
|
RequestIdentityError::NoKey => {
|
||||||
eprintln!("ERROR: No private key found in the certificate file");
|
eprintln!("ERROR: No private key found in the certificate file");
|
||||||
eprintln!("ERROR: Please provide the private key file using the `-k` option");
|
eprintln!("ERROR: Please provide the private key file using the `-k` option");
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
|
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
|
||||||
// Decrypt the private key error, ask for the key password
|
// Decrypt the private key error, ask for the key password
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::{
|
use crate::{utils::request::create_identity, GP_USER_AGENT};
|
||||||
utils::request::{create_identity_from_pem, create_identity_from_pkcs12},
|
|
||||||
GP_USER_AGENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||||
pub enum ClientOs {
|
pub enum ClientOs {
|
||||||
@ -255,12 +253,8 @@ impl TryFrom<&GpParams> for Client {
|
|||||||
.user_agent(&value.user_agent);
|
.user_agent(&value.user_agent);
|
||||||
|
|
||||||
if let Some(cert) = value.certificate.as_deref() {
|
if let Some(cert) = value.certificate.as_deref() {
|
||||||
// .p12 or .pfx file
|
info!("Using client certificate authentication...");
|
||||||
let identity = if cert.ends_with(".p12") || cert.ends_with(".pfx") {
|
let identity = create_identity(cert, value.sslkey.as_deref(), value.key_password.as_deref())?;
|
||||||
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())?
|
|
||||||
};
|
|
||||||
builder = builder.identity(identity);
|
builder = builder.identity(identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::fs;
|
use std::{borrow::Cow, fs};
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
@ -17,24 +17,31 @@ pub enum RequestIdentityError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create an identity object from a certificate and key
|
/// 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<Identity> {
|
/// 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<Identity> {
|
||||||
|
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<Identity> {
|
||||||
let cert_pem = fs::read(cert).map_err(|err| anyhow::anyhow!("Failed to read certificate file: {}", err))?;
|
let cert_pem = fs::read(cert).map_err(|err| anyhow::anyhow!("Failed to read certificate file: {}", err))?;
|
||||||
|
|
||||||
// Get the private key pem
|
// Use the certificate as the key if no key is provided
|
||||||
let key_pem = match key {
|
let key_pem_file = match key {
|
||||||
Some(key) => {
|
Some(key) => Cow::Owned(fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?),
|
||||||
let pem_file = fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?;
|
None => Cow::Borrowed(&cert_pem),
|
||||||
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)?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
// 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 decrypted_key_pem = if key_pem.tag().ends_with("ENCRYPTED PRIVATE KEY") {
|
||||||
let passphrase = passphrase.ok_or_else(|| {
|
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)
|
Ok(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
||||||
let pkcs12 = fs::read(pkcs12)?;
|
let pkcs12 = fs::read(pkcs12)?;
|
||||||
|
|
||||||
let Some(passphrase) = passphrase else {
|
let Some(passphrase) = passphrase else {
|
||||||
@ -89,4 +96,45 @@ mod tests {
|
|||||||
|
|
||||||
assert!(identity.is_ok());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
62
crates/gpapi/tests/files/badssl.com-client-unencrypted.pem
Normal file
62
crates/gpapi/tests/files/badssl.com-client-unencrypted.pem
Normal file
@ -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: <No 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-----
|
Loading…
Reference in New Issue
Block a user