mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
feat: support client certificate authentication (related #363)
This commit is contained in:
parent
3bb115bd2d
commit
52b6fa6fbd
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -25,7 +25,9 @@
|
|||||||
"LOGNAME",
|
"LOGNAME",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"openconnect",
|
"openconnect",
|
||||||
|
"pkcs",
|
||||||
"pkexec",
|
"pkexec",
|
||||||
|
"pkey",
|
||||||
"Prelogin",
|
"Prelogin",
|
||||||
"prelogon",
|
"prelogon",
|
||||||
"prelogonuserauthcookie",
|
"prelogonuserauthcookie",
|
||||||
@ -35,6 +37,7 @@
|
|||||||
"rspc",
|
"rspc",
|
||||||
"servercert",
|
"servercert",
|
||||||
"specta",
|
"specta",
|
||||||
|
"sslkey",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tanstack",
|
"tanstack",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -252,6 +252,12 @@ version = "0.21.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -1440,6 +1446,8 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"md5",
|
"md5",
|
||||||
"open",
|
"open",
|
||||||
|
"openssl",
|
||||||
|
"pem",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -2670,6 +2678,16 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -22,6 +22,8 @@ is_executable = "1.0"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
||||||
|
openssl = "0.10"
|
||||||
|
pem = "3"
|
||||||
roxmltree = "0.18"
|
roxmltree = "0.18"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{fs, sync::Arc};
|
use std::{cell::RefCell, fs, sync::Arc};
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use common::vpn_utils::find_csd_wrapper;
|
use common::vpn_utils::find_csd_wrapper;
|
||||||
@ -13,7 +13,7 @@ use gpapi::{
|
|||||||
auth_launcher::SamlAuthLauncher,
|
auth_launcher::SamlAuthLauncher,
|
||||||
users::{get_non_root_user, get_user_by_name},
|
users::{get_non_root_user, get_user_by_name},
|
||||||
},
|
},
|
||||||
utils::shutdown_signal,
|
utils::{request::RequestIdentityError, shutdown_signal},
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||||
@ -42,6 +42,13 @@ pub(crate) struct ConnectArgs {
|
|||||||
)]
|
)]
|
||||||
hip: bool,
|
hip: bool,
|
||||||
|
|
||||||
|
#[arg(short, long, help = "Use SSL client certificate file (.pem or .p12)")]
|
||||||
|
certificate: Option<String>,
|
||||||
|
#[arg(short = 'k', long, help = "Use SSL private key file (.pem)")]
|
||||||
|
sslkey: Option<String>,
|
||||||
|
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
|
||||||
|
key_password: Option<String>,
|
||||||
|
|
||||||
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
|
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
|
||||||
csd_user: Option<String>,
|
csd_user: Option<String>,
|
||||||
|
|
||||||
@ -86,11 +93,16 @@ impl ConnectArgs {
|
|||||||
pub(crate) struct ConnectHandler<'a> {
|
pub(crate) struct ConnectHandler<'a> {
|
||||||
args: &'a ConnectArgs,
|
args: &'a ConnectArgs,
|
||||||
shared_args: &'a SharedArgs,
|
shared_args: &'a SharedArgs,
|
||||||
|
latest_key_password: RefCell<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ConnectHandler<'a> {
|
impl<'a> ConnectHandler<'a> {
|
||||||
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||||
Self { args, shared_args }
|
Self {
|
||||||
|
args,
|
||||||
|
shared_args,
|
||||||
|
latest_key_password: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_gp_params(&self) -> GpParams {
|
fn build_gp_params(&self) -> GpParams {
|
||||||
@ -99,10 +111,45 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
.client_os(ClientOs::from(&self.args.os))
|
.client_os(ClientOs::from(&self.args.os))
|
||||||
.os_version(self.args.os_version())
|
.os_version(self.args.os_version())
|
||||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
|
self.latest_key_password.replace(self.args.key_password.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Err(err) = self.handle_impl().await else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
|
||||||
|
return Err(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
match root_cause {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
|
||||||
|
// Decrypt the private key error, ask for the key password
|
||||||
|
let message = format!("Enter the {} passphrase:", cert_type);
|
||||||
|
let password = Password::new(&message)
|
||||||
|
.without_confirmation()
|
||||||
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
self.latest_key_password.replace(Some(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
|
||||||
let server = self.args.server.as_str();
|
let server = self.args.server.as_str();
|
||||||
let as_gateway = self.args.as_gateway;
|
let as_gateway = self.args.as_gateway;
|
||||||
|
|
||||||
@ -217,6 +264,9 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
let vpn = Vpn::builder(gateway, cookie)
|
let vpn = Vpn::builder(gateway, cookie)
|
||||||
.script(self.args.script.clone())
|
.script(self.args.script.clone())
|
||||||
.user_agent(self.args.user_agent.clone())
|
.user_agent(self.args.user_agent.clone())
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
.csd_uid(csd_uid)
|
.csd_uid(csd_uid)
|
||||||
.csd_wrapper(csd_wrapper)
|
.csd_wrapper(csd_wrapper)
|
||||||
.reconnect_timeout(self.args.reconnect_timeout)
|
.reconnect_timeout(self.args.reconnect_timeout)
|
||||||
|
@ -39,6 +39,9 @@ impl VpnTaskContext {
|
|||||||
.script(args.vpnc_script())
|
.script(args.vpnc_script())
|
||||||
.user_agent(args.user_agent())
|
.user_agent(args.user_agent())
|
||||||
.os(args.openconnect_os())
|
.os(args.openconnect_os())
|
||||||
|
.certificate(args.certificate())
|
||||||
|
.sslkey(args.sslkey())
|
||||||
|
.key_password(args.key_password())
|
||||||
.csd_uid(args.csd_uid())
|
.csd_uid(args.csd_uid())
|
||||||
.csd_wrapper(args.csd_wrapper())
|
.csd_wrapper(args.csd_wrapper())
|
||||||
.reconnect_timeout(args.reconnect_timeout())
|
.reconnect_timeout(args.reconnect_timeout())
|
||||||
|
@ -9,6 +9,8 @@ anyhow.workspace = true
|
|||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
openssl.workspace = true
|
||||||
|
pem.workspace = true
|
||||||
roxmltree.workspace = true
|
roxmltree.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
specta.workspace = true
|
specta.workspace = true
|
||||||
|
@ -156,11 +156,7 @@ fn build_csd_token(cookie: &str) -> anyhow::Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
|
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let md5 = build_csd_token(cookie)?;
|
let md5 = build_csd_token(cookie)?;
|
||||||
|
|
||||||
info!("Submit HIP report md5: {}", md5);
|
info!("Submit HIP report md5: {}", md5);
|
||||||
|
@ -21,10 +21,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
|
|||||||
let gateway = remove_url_scheme(&url);
|
let gateway = remove_url_scheme(&url);
|
||||||
|
|
||||||
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut params = cred.to_params();
|
let mut params = cred.to_params();
|
||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::GP_USER_AGENT;
|
use crate::{
|
||||||
|
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 {
|
||||||
@ -51,6 +55,9 @@ pub struct GpParams {
|
|||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
// Used for MFA
|
// Used for MFA
|
||||||
input_str: Option<String>,
|
input_str: Option<String>,
|
||||||
otp: Option<String>,
|
otp: Option<String>,
|
||||||
@ -142,6 +149,9 @@ pub struct GpParamsBuilder {
|
|||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParamsBuilder {
|
impl GpParamsBuilder {
|
||||||
@ -156,6 +166,9 @@ impl GpParamsBuilder {
|
|||||||
client_version: Default::default(),
|
client_version: Default::default(),
|
||||||
computer,
|
computer,
|
||||||
ignore_tls_errors: false,
|
ignore_tls_errors: false,
|
||||||
|
certificate: Default::default(),
|
||||||
|
sslkey: Default::default(),
|
||||||
|
key_password: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,6 +207,21 @@ impl GpParamsBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate<T: Into<Option<String>>>(&mut self, certificate: T) -> &mut Self {
|
||||||
|
self.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey<T: Into<Option<String>>>(&mut self, sslkey: T) -> &mut Self {
|
||||||
|
self.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password<T: Into<Option<String>>>(&mut self, password: T) -> &mut Self {
|
||||||
|
self.key_password = password.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(&self) -> GpParams {
|
pub fn build(&self) -> GpParams {
|
||||||
GpParams {
|
GpParams {
|
||||||
is_gateway: self.is_gateway,
|
is_gateway: self.is_gateway,
|
||||||
@ -203,6 +231,9 @@ impl GpParamsBuilder {
|
|||||||
client_version: self.client_version.clone(),
|
client_version: self.client_version.clone(),
|
||||||
computer: self.computer.clone(),
|
computer: self.computer.clone(),
|
||||||
ignore_tls_errors: self.ignore_tls_errors,
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
certificate: self.certificate.clone(),
|
||||||
|
sslkey: self.sslkey.clone(),
|
||||||
|
key_password: self.key_password.clone(),
|
||||||
input_str: Default::default(),
|
input_str: Default::default(),
|
||||||
otp: Default::default(),
|
otp: Default::default(),
|
||||||
}
|
}
|
||||||
@ -214,3 +245,26 @@ impl Default for GpParamsBuilder {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&GpParams> for Client {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &GpParams) -> Result<Self, Self::Error> {
|
||||||
|
let mut builder = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(value.ignore_tls_errors)
|
||||||
|
.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())?
|
||||||
|
};
|
||||||
|
builder = builder.identity(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = builder.build()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -88,10 +88,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
|||||||
let server = remove_url_scheme(&portal);
|
let server = remove_url_scheme(&portal);
|
||||||
|
|
||||||
let url = format!("{}/global-protect/getconfig.esp", portal);
|
let url = format!("{}/global-protect/getconfig.esp", portal);
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut params = cred.to_params();
|
let mut params = cred.to_params();
|
||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
|
@ -114,10 +114,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
|
|
||||||
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
|
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
|
||||||
|
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(user_agent)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let res = client
|
let res = client
|
||||||
.post(&prelogin_url)
|
.post(&prelogin_url)
|
||||||
|
@ -33,6 +33,9 @@ pub struct ConnectArgs {
|
|||||||
vpnc_script: Option<String>,
|
vpnc_script: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
os: Option<ClientOs>,
|
os: Option<ClientOs>,
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
csd_wrapper: Option<String>,
|
csd_wrapper: Option<String>,
|
||||||
reconnect_timeout: u32,
|
reconnect_timeout: u32,
|
||||||
@ -47,6 +50,9 @@ impl ConnectArgs {
|
|||||||
vpnc_script: None,
|
vpnc_script: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
certificate: None,
|
||||||
|
sslkey: None,
|
||||||
|
key_password: None,
|
||||||
csd_uid: 0,
|
csd_uid: 0,
|
||||||
csd_wrapper: None,
|
csd_wrapper: None,
|
||||||
reconnect_timeout: 300,
|
reconnect_timeout: 300,
|
||||||
@ -71,6 +77,18 @@ impl ConnectArgs {
|
|||||||
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate(&self) -> Option<String> {
|
||||||
|
self.certificate.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey(&self) -> Option<String> {
|
||||||
|
self.sslkey.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password(&self) -> Option<String> {
|
||||||
|
self.key_password.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn csd_uid(&self) -> u32 {
|
pub fn csd_uid(&self) -> u32 {
|
||||||
self.csd_uid
|
self.csd_uid
|
||||||
}
|
}
|
||||||
@ -131,6 +149,21 @@ impl ConnectRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
|
||||||
|
self.args.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
|
||||||
|
self.args.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
|
||||||
|
self.args.key_password = key_password.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
|
pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
|
||||||
self.args.reconnect_timeout = reconnect_timeout;
|
self.args.reconnect_timeout = reconnect_timeout;
|
||||||
self
|
self
|
||||||
|
@ -8,6 +8,7 @@ pub mod env_file;
|
|||||||
pub mod lock_file;
|
pub mod lock_file;
|
||||||
pub mod openssl;
|
pub mod openssl;
|
||||||
pub mod redact;
|
pub mod redact;
|
||||||
|
pub mod request;
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
|
93
crates/gpapi/src/utils/request.rs
Normal file
93
crates/gpapi/src/utils/request.rs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use log::warn;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use pem::parse_many;
|
||||||
|
use reqwest::Identity;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RequestIdentityError {
|
||||||
|
#[error("Failed to find the private key")]
|
||||||
|
NoKey,
|
||||||
|
#[error("No passphrase provided")]
|
||||||
|
NoPassphrase(&'static str),
|
||||||
|
#[error("Failed to decrypt private key")]
|
||||||
|
DecryptError(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an identity object from a certificate and key
|
||||||
|
pub(crate) fn create_identity_from_pem(
|
||||||
|
cert: &str,
|
||||||
|
key: Option<&str>,
|
||||||
|
passphrase: Option<&str>,
|
||||||
|
) -> anyhow::Result<Identity> {
|
||||||
|
let cert_pem = fs::read(cert)?;
|
||||||
|
|
||||||
|
// Get the private key pem
|
||||||
|
let key_pem = match key {
|
||||||
|
Some(key) => pem::parse(fs::read(key)?)?,
|
||||||
|
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)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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(|| {
|
||||||
|
warn!("Key is encrypted but no passphrase provided");
|
||||||
|
RequestIdentityError::NoPassphrase("PEM")
|
||||||
|
})?;
|
||||||
|
let pem_content = pem::encode(&key_pem);
|
||||||
|
let key = PKey::private_key_from_pem_passphrase(pem_content.as_bytes(), passphrase.as_bytes()).map_err(|err| {
|
||||||
|
warn!("Failed to decrypt key: {}", err);
|
||||||
|
RequestIdentityError::DecryptError("PEM")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
key.private_key_to_pem_pkcs8()?
|
||||||
|
} else {
|
||||||
|
pem::encode(&key_pem).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let identity = Identity::from_pkcs8_pem(&cert_pem, &decrypted_key_pem)?;
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
||||||
|
let pkcs12 = fs::read(pkcs12)?;
|
||||||
|
|
||||||
|
let Some(passphrase) = passphrase else {
|
||||||
|
bail!(RequestIdentityError::NoPassphrase("PKCS#12"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let identity = Identity::from_pkcs12_der(&pkcs12, passphrase)?;
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_requires_passphrase() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let identity = create_identity_from_pem(cert, None, None);
|
||||||
|
|
||||||
|
assert!(identity.is_err());
|
||||||
|
assert!(identity.unwrap_err().to_string().contains("No passphrase provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_with_passphrase() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let passphrase = "badssl.com";
|
||||||
|
|
||||||
|
let identity = create_identity_from_pem(cert, None, Some(passphrase));
|
||||||
|
|
||||||
|
assert!(identity.is_ok());
|
||||||
|
}
|
||||||
|
}
|
64
crates/gpapi/tests/files/badssl.com-client.pem
Normal file
64
crates/gpapi/tests/files/badssl.com-client.pem
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 ENCRYPTED PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
pPo=
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
@ -14,6 +14,8 @@ pub(crate) struct ConnectOptions {
|
|||||||
pub script: *const c_char,
|
pub script: *const c_char,
|
||||||
pub os: *const c_char,
|
pub os: *const c_char,
|
||||||
pub certificate: *const c_char,
|
pub certificate: *const c_char,
|
||||||
|
pub sslkey: *const c_char,
|
||||||
|
pub key_password: *const c_char,
|
||||||
pub servercert: *const c_char,
|
pub servercert: *const c_char,
|
||||||
|
|
||||||
pub csd_uid: u32,
|
pub csd_uid: u32,
|
||||||
|
@ -63,6 +63,8 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
INFO("OS: %s", options->os);
|
INFO("OS: %s", options->os);
|
||||||
INFO("CSD_USER: %d", options->csd_uid);
|
INFO("CSD_USER: %d", options->csd_uid);
|
||||||
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
||||||
|
INFO("CERTIFICATE: %s", options->certificate);
|
||||||
|
INFO("SSLKEY: %s", options->sslkey);
|
||||||
INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
|
INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
|
||||||
INFO("MTU: %d", options->mtu);
|
INFO("MTU: %d", options->mtu);
|
||||||
INFO("DISABLE_IPV6: %d", options->disable_ipv6);
|
INFO("DISABLE_IPV6: %d", options->disable_ipv6);
|
||||||
@ -80,6 +82,11 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
openconnect_set_protocol(vpninfo, "gp");
|
openconnect_set_protocol(vpninfo, "gp");
|
||||||
openconnect_set_hostname(vpninfo, options->server);
|
openconnect_set_hostname(vpninfo, options->server);
|
||||||
openconnect_set_cookie(vpninfo, options->cookie);
|
openconnect_set_cookie(vpninfo, options->cookie);
|
||||||
|
openconnect_set_client_cert(vpninfo, options->certificate, options->sslkey);
|
||||||
|
|
||||||
|
if (options->key_password) {
|
||||||
|
openconnect_set_key_password(vpninfo, options->key_password);
|
||||||
|
}
|
||||||
|
|
||||||
if (options->os) {
|
if (options->os) {
|
||||||
openconnect_set_reported_os(vpninfo, options->os);
|
openconnect_set_reported_os(vpninfo, options->os);
|
||||||
|
@ -15,6 +15,8 @@ typedef struct vpn_options
|
|||||||
const char *script;
|
const char *script;
|
||||||
const char *os;
|
const char *os;
|
||||||
const char *certificate;
|
const char *certificate;
|
||||||
|
const char *sslkey;
|
||||||
|
const char *key_password;
|
||||||
const char *servercert;
|
const char *servercert;
|
||||||
|
|
||||||
const uid_t csd_uid;
|
const uid_t csd_uid;
|
||||||
|
@ -18,6 +18,8 @@ pub struct Vpn {
|
|||||||
script: CString,
|
script: CString,
|
||||||
os: CString,
|
os: CString,
|
||||||
certificate: Option<CString>,
|
certificate: Option<CString>,
|
||||||
|
sslkey: Option<CString>,
|
||||||
|
key_password: Option<CString>,
|
||||||
servercert: Option<CString>,
|
servercert: Option<CString>,
|
||||||
|
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
@ -63,7 +65,10 @@ impl Vpn {
|
|||||||
user_agent: self.user_agent.as_ptr(),
|
user_agent: self.user_agent.as_ptr(),
|
||||||
script: self.script.as_ptr(),
|
script: self.script.as_ptr(),
|
||||||
os: self.os.as_ptr(),
|
os: self.os.as_ptr(),
|
||||||
|
|
||||||
certificate: Self::option_to_ptr(&self.certificate),
|
certificate: Self::option_to_ptr(&self.certificate),
|
||||||
|
sslkey: Self::option_to_ptr(&self.sslkey),
|
||||||
|
key_password: Self::option_to_ptr(&self.key_password),
|
||||||
servercert: Self::option_to_ptr(&self.servercert),
|
servercert: Self::option_to_ptr(&self.servercert),
|
||||||
|
|
||||||
csd_uid: self.csd_uid,
|
csd_uid: self.csd_uid,
|
||||||
@ -110,6 +115,10 @@ pub struct VpnBuilder {
|
|||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
os: Option<String>,
|
os: Option<String>,
|
||||||
|
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
|
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
csd_wrapper: Option<String>,
|
csd_wrapper: Option<String>,
|
||||||
|
|
||||||
@ -128,6 +137,10 @@ impl VpnBuilder {
|
|||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
|
||||||
|
certificate: None,
|
||||||
|
sslkey: None,
|
||||||
|
key_password: None,
|
||||||
|
|
||||||
csd_uid: 0,
|
csd_uid: 0,
|
||||||
csd_wrapper: None,
|
csd_wrapper: None,
|
||||||
|
|
||||||
@ -152,6 +165,21 @@ impl VpnBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
|
||||||
|
self.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
|
||||||
|
self.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
|
||||||
|
self.key_password = key_password.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
|
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
|
||||||
self.csd_uid = csd_uid;
|
self.csd_uid = csd_uid;
|
||||||
self
|
self
|
||||||
@ -199,7 +227,10 @@ impl VpnBuilder {
|
|||||||
user_agent: Self::to_cstring(&user_agent),
|
user_agent: Self::to_cstring(&user_agent),
|
||||||
script: Self::to_cstring(&script),
|
script: Self::to_cstring(&script),
|
||||||
os: Self::to_cstring(&os),
|
os: Self::to_cstring(&os),
|
||||||
certificate: None,
|
|
||||||
|
certificate: self.certificate.as_deref().map(Self::to_cstring),
|
||||||
|
sslkey: self.sslkey.as_deref().map(Self::to_cstring),
|
||||||
|
key_password: self.key_password.as_deref().map(Self::to_cstring),
|
||||||
servercert: None,
|
servercert: None,
|
||||||
|
|
||||||
csd_uid: self.csd_uid,
|
csd_uid: self.csd_uid,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user