From 52b6fa6fbdf65ecfbd97cedfcfc18bec8c0fc78a Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Sun, 19 May 2024 18:44:07 +0800 Subject: [PATCH] feat: support client certificate authentication (related #363) --- .vscode/settings.json | 3 + Cargo.lock | 18 ++++ Cargo.toml | 2 + apps/gpclient/src/connect.rs | 56 ++++++++++- apps/gpservice/src/vpn_task.rs | 3 + crates/gpapi/Cargo.toml | 2 + crates/gpapi/src/gateway/hip.rs | 6 +- crates/gpapi/src/gateway/login.rs | 5 +- crates/gpapi/src/gp_params.rs | 56 ++++++++++- crates/gpapi/src/portal/config.rs | 5 +- crates/gpapi/src/portal/prelogin.rs | 5 +- crates/gpapi/src/service/request.rs | 33 +++++++ crates/gpapi/src/utils/mod.rs | 1 + crates/gpapi/src/utils/request.rs | 93 +++++++++++++++++++ .../gpapi/tests/files/badssl.com-client.pem | 64 +++++++++++++ crates/openconnect/src/ffi/mod.rs | 2 + crates/openconnect/src/ffi/vpn.c | 7 ++ crates/openconnect/src/ffi/vpn.h | 2 + crates/openconnect/src/vpn.rs | 33 ++++++- 19 files changed, 374 insertions(+), 22 deletions(-) create mode 100644 crates/gpapi/src/utils/request.rs create mode 100644 crates/gpapi/tests/files/badssl.com-client.pem diff --git a/.vscode/settings.json b/.vscode/settings.json index ab135286..bff33a21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,7 +25,9 @@ "LOGNAME", "oneshot", "openconnect", + "pkcs", "pkexec", + "pkey", "Prelogin", "prelogon", "prelogonuserauthcookie", @@ -35,6 +37,7 @@ "rspc", "servercert", "specta", + "sslkey", "sysinfo", "tanstack", "tauri", diff --git a/Cargo.lock b/Cargo.lock index c5c04251..5597d590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,6 +252,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -1440,6 +1446,8 @@ dependencies = [ "log", "md5", "open", + "openssl", + "pem", "redact-engine", "regex", "reqwest", @@ -2670,6 +2678,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index d43d8293..69b2372e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ is_executable = "1.0" log = "0.4" regex = "1" reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] } +openssl = "0.10" +pem = "3" roxmltree = "0.18" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index f1b1ed9e..b7af3eec 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -1,4 +1,4 @@ -use std::{fs, sync::Arc}; +use std::{cell::RefCell, fs, sync::Arc}; use clap::Args; use common::vpn_utils::find_csd_wrapper; @@ -13,7 +13,7 @@ use gpapi::{ auth_launcher::SamlAuthLauncher, users::{get_non_root_user, get_user_by_name}, }, - utils::shutdown_signal, + utils::{request::RequestIdentityError, shutdown_signal}, GP_USER_AGENT, }; use inquire::{Password, PasswordDisplayMode, Select, Text}; @@ -42,6 +42,13 @@ pub(crate) struct ConnectArgs { )] hip: bool, + #[arg(short, long, help = "Use SSL client certificate file (.pem or .p12)")] + certificate: Option, + #[arg(short = 'k', long, help = "Use SSL private key file (.pem)")] + sslkey: Option, + #[arg(short = 'p', long, help = "The key passphrase of the private key")] + key_password: Option, + #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")] csd_user: Option, @@ -86,11 +93,16 @@ impl ConnectArgs { pub(crate) struct ConnectHandler<'a> { args: &'a ConnectArgs, shared_args: &'a SharedArgs, + latest_key_password: RefCell>, } impl<'a> ConnectHandler<'a> { 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 { @@ -99,10 +111,45 @@ impl<'a> ConnectHandler<'a> { .client_os(ClientOs::from(&self.args.os)) .os_version(self.args.os_version()) .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() } 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::() 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 as_gateway = self.args.as_gateway; @@ -217,6 +264,9 @@ impl<'a> ConnectHandler<'a> { let vpn = Vpn::builder(gateway, cookie) .script(self.args.script.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_wrapper(csd_wrapper) .reconnect_timeout(self.args.reconnect_timeout) diff --git a/apps/gpservice/src/vpn_task.rs b/apps/gpservice/src/vpn_task.rs index a90e365d..fa4bb557 100644 --- a/apps/gpservice/src/vpn_task.rs +++ b/apps/gpservice/src/vpn_task.rs @@ -39,6 +39,9 @@ impl VpnTaskContext { .script(args.vpnc_script()) .user_agent(args.user_agent()) .os(args.openconnect_os()) + .certificate(args.certificate()) + .sslkey(args.sslkey()) + .key_password(args.key_password()) .csd_uid(args.csd_uid()) .csd_wrapper(args.csd_wrapper()) .reconnect_timeout(args.reconnect_timeout()) diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml index af91e7e5..58e93373 100644 --- a/crates/gpapi/Cargo.toml +++ b/crates/gpapi/Cargo.toml @@ -9,6 +9,8 @@ anyhow.workspace = true base64.workspace = true log.workspace = true reqwest.workspace = true +openssl.workspace = true +pem.workspace = true roxmltree.workspace = true serde.workspace = true specta.workspace = true diff --git a/crates/gpapi/src/gateway/hip.rs b/crates/gpapi/src/gateway/hip.rs index 7cc7548c..16200027 100644 --- a/crates/gpapi/src/gateway/hip.rs +++ b/crates/gpapi/src/gateway/hip.rs @@ -156,11 +156,7 @@ fn build_csd_token(cookie: &str) -> anyhow::Result { } pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> { - let client = Client::builder() - .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) - .user_agent(gp_params.user_agent()) - .build()?; - + let client = Client::try_from(gp_params)?; let md5 = build_csd_token(cookie)?; info!("Submit HIP report md5: {}", md5); diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs index 7a999f4a..8c4cc684 100644 --- a/crates/gpapi/src/gateway/login.rs +++ b/crates/gpapi/src/gateway/login.rs @@ -21,10 +21,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam let gateway = remove_url_scheme(&url); let login_url = format!("{}/ssl-vpn/login.esp", url); - let client = Client::builder() - .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) - .user_agent(gp_params.user_agent()) - .build()?; + let client = Client::try_from(gp_params)?; let mut params = cred.to_params(); let extra_params = gp_params.to_params(); diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs index 3184b18d..85af3e06 100644 --- a/crates/gpapi/src/gp_params.rs +++ b/crates/gpapi/src/gp_params.rs @@ -1,9 +1,13 @@ use std::collections::HashMap; +use reqwest::Client; use serde::{Deserialize, Serialize}; 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)] pub enum ClientOs { @@ -51,6 +55,9 @@ pub struct GpParams { client_version: Option, computer: String, ignore_tls_errors: bool, + certificate: Option, + sslkey: Option, + key_password: Option, // Used for MFA input_str: Option, otp: Option, @@ -142,6 +149,9 @@ pub struct GpParamsBuilder { client_version: Option, computer: String, ignore_tls_errors: bool, + certificate: Option, + sslkey: Option, + key_password: Option, } impl GpParamsBuilder { @@ -156,6 +166,9 @@ impl GpParamsBuilder { client_version: Default::default(), computer, ignore_tls_errors: false, + certificate: Default::default(), + sslkey: Default::default(), + key_password: Default::default(), } } @@ -194,6 +207,21 @@ impl GpParamsBuilder { self } + pub fn certificate>>(&mut self, certificate: T) -> &mut Self { + self.certificate = certificate.into(); + self + } + + pub fn sslkey>>(&mut self, sslkey: T) -> &mut Self { + self.sslkey = sslkey.into(); + self + } + + pub fn key_password>>(&mut self, password: T) -> &mut Self { + self.key_password = password.into(); + self + } + pub fn build(&self) -> GpParams { GpParams { is_gateway: self.is_gateway, @@ -203,6 +231,9 @@ impl GpParamsBuilder { client_version: self.client_version.clone(), computer: self.computer.clone(), 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(), otp: Default::default(), } @@ -214,3 +245,26 @@ impl Default for GpParamsBuilder { Self::new() } } + +impl TryFrom<&GpParams> for Client { + type Error = anyhow::Error; + + fn try_from(value: &GpParams) -> Result { + 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) + } +} diff --git a/crates/gpapi/src/portal/config.rs b/crates/gpapi/src/portal/config.rs index 704bbb89..bd89f863 100644 --- a/crates/gpapi/src/portal/config.rs +++ b/crates/gpapi/src/portal/config.rs @@ -88,10 +88,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara let server = remove_url_scheme(&portal); let url = format!("{}/global-protect/getconfig.esp", portal); - let client = Client::builder() - .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) - .user_agent(gp_params.user_agent()) - .build()?; + let client = Client::try_from(gp_params)?; let mut params = cred.to_params(); let extra_params = gp_params.to_params(); diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs index a7a0980b..d1c45737 100644 --- a/crates/gpapi/src/portal/prelogin.rs +++ b/crates/gpapi/src/portal/prelogin.rs @@ -114,10 +114,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result, user_agent: Option, os: Option, + certificate: Option, + sslkey: Option, + key_password: Option, csd_uid: u32, csd_wrapper: Option, reconnect_timeout: u32, @@ -47,6 +50,9 @@ impl ConnectArgs { vpnc_script: None, user_agent: None, os: None, + certificate: None, + sslkey: None, + key_password: None, csd_uid: 0, csd_wrapper: None, reconnect_timeout: 300, @@ -71,6 +77,18 @@ impl ConnectArgs { self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) } + pub fn certificate(&self) -> Option { + self.certificate.clone() + } + + pub fn sslkey(&self) -> Option { + self.sslkey.clone() + } + + pub fn key_password(&self) -> Option { + self.key_password.clone() + } + pub fn csd_uid(&self) -> u32 { self.csd_uid } @@ -131,6 +149,21 @@ impl ConnectRequest { self } + pub fn with_certificate>>(mut self, certificate: T) -> Self { + self.args.certificate = certificate.into(); + self + } + + pub fn with_sslkey>>(mut self, sslkey: T) -> Self { + self.args.sslkey = sslkey.into(); + self + } + + pub fn with_key_password>>(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 { self.args.reconnect_timeout = reconnect_timeout; self diff --git a/crates/gpapi/src/utils/mod.rs b/crates/gpapi/src/utils/mod.rs index 55da9b44..e2b43af8 100644 --- a/crates/gpapi/src/utils/mod.rs +++ b/crates/gpapi/src/utils/mod.rs @@ -8,6 +8,7 @@ pub mod env_file; pub mod lock_file; pub mod openssl; pub mod redact; +pub mod request; #[cfg(feature = "tauri")] pub mod window; diff --git a/crates/gpapi/src/utils/request.rs b/crates/gpapi/src/utils/request.rs new file mode 100644 index 00000000..2a3dceb1 --- /dev/null +++ b/crates/gpapi/src/utils/request.rs @@ -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 { + 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 { + 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()); + } +} diff --git a/crates/gpapi/tests/files/badssl.com-client.pem b/crates/gpapi/tests/files/badssl.com-client.pem new file mode 100644 index 00000000..d1cdae89 --- /dev/null +++ b/crates/gpapi/tests/files/badssl.com-client.pem @@ -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: +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIET6L0Ht/lYgCAggA +MBQGCCqGSIb3DQMHBAi1Xo+JdQ6XvwSCBMgX20Fk3/GzptJ0zjl7ZqX2G3J4LIkM +E5qJ4yv2WUkCCOWqz5DjlSrRz4kdCYHqnM1/qyrLa1UWWJlNQ9lBHTE+yp0vtAC/ +ajQfKt3RFyGxblp6nEKJI7kvhmQHDbITilmVEpcZPbci3gi7asQI3bRSLaHwGtbH +DY+8hJ8lZQMRjYGDyGb99qEdYnMMRMW+b44lIRASe6W3EUfrvJlp+OUqRA7hJzn2 +yha9Zo8KWo9fA9UZDFFKNlXakg76+1HymB+uqvZl14xHHfwhlKPaqzmCb8MUtt7e +YJDB9I3y8aHKExXPbRk04bbY5G9o6WdWslDUY4axOZuhUXyn0h6cTZn//qmsjcBH +499+j55vW6W7vkMfurt/pmLIBWC9kDWPZVLizbfXxWiWvRmQvKPfzO5TU8oObYyJ +qUVjb3Vpa/WPrF5APUVd/DDofurgzdOkmDGomONPvSHxahHSyEZsxpnl52GD6uU/ +i3oa5qLE9uA1QjyX6wyN9SU5wE2FZKTJwwRJwW4+s4T/2eJjhuJez5q1xhSCes4A +A2pufAAY/ctQSmCCKCTW+EkrXtcezx66fkgPpNK/m6bz5KGJkA4QXjl8A05PDAFE +Z68VOX/T0IGfXc2BbPgP0u+WpCvvO2cW/pU4sjcwOMxFuT1Bn3TwmDLTZ+zba1rE +zFRMMCz/8SKq3I+VkzQ66ureEz0RLwk07JVzE9AJUEm+zCFUdoIaz09OMGVqtf4a +V+UgupH0QlffmRNJKQtXPuj6Wjfa43GLaCnN/cpXXq8+2o81dLTsCbEsYu+8DRjC +B0iyjzdqgjBBYurIEwEc4iGtPt4Y+4rgAJcpEUgwvWii37xyutOC9V7ansvd6zg3 +WXiX5Ktj/qS0EzM33WtZfx7jygJIf1MvxrJU+D+HgGii1mHaZ6bHxMX3QGpRsEvh +IzBx16XvoHcXARZJG91bC+K1sJ6e05L1PevS7gj4heJTEhtmvABUrn9O1n5fZWPj +Q81zRDgplMO7r8aBW/pE+sj4VSTMg0Xu0nlqqvQoWxr9YFcJm0+I9fHQPxewnRus +sBZoiTqnWqbTr+uRATRUAp+hU03S4jGZwbzH4ylL2hr/TshGVJk/olBsULAfIiHa +dA5H258IEwAoFO6zgI9AvqmTFo3Mnpqb/AS/HuDmmS/3Ud1EF8hFsMLPcV0JdSTY +Dl4xgZ6j6jOUlTN5Yt6To2Zg3Q9Bm6qytFaffEP66Jl5aWhksI31Fz/ihzn5wfx9 +xh91U8+kGVNrpYHlo5y3FR/ywSXynLkJffCbfUciEaTDv9i0JppoIVXyFqcMofHe +GUsWTCozAW3O8MwpLaJxcNcfRq0DWziIdiDgbF2tPoCqnNxXtLYSPpdt3jNDcPcx +U0Z6ep6FnAXiujtQRSRSP3Ssq23098BxDSM9+eashFOmSbSClAEEn/THRxTp/gMh +zmD8kpX1zN1Cm/lerTGjrGjnkXcQ7LY76/+C1uT+tQbw5LjmCfFEYTFtnFyYFlF1 +GiXFokh9SdLaCzW4vmZok85Fe+7VZ7BAchBTfTIMKlXKmeouf3YVYJ8glPsinrjb +cB2pKv3tVrdQwo3moYDwSsDgkd7BNKKHDVdY2O6NgX4/Fyd6pZt7ZAphyC1giEqg +pPo= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/crates/openconnect/src/ffi/mod.rs b/crates/openconnect/src/ffi/mod.rs index 5469c18f..5c031ba2 100644 --- a/crates/openconnect/src/ffi/mod.rs +++ b/crates/openconnect/src/ffi/mod.rs @@ -14,6 +14,8 @@ pub(crate) struct ConnectOptions { pub script: *const c_char, pub os: *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 csd_uid: u32, diff --git a/crates/openconnect/src/ffi/vpn.c b/crates/openconnect/src/ffi/vpn.c index 6aec00e4..97369dde 100644 --- a/crates/openconnect/src/ffi/vpn.c +++ b/crates/openconnect/src/ffi/vpn.c @@ -63,6 +63,8 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) INFO("OS: %s", options->os); INFO("CSD_USER: %d", options->csd_uid); 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("MTU: %d", options->mtu); 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_hostname(vpninfo, options->server); 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) { openconnect_set_reported_os(vpninfo, options->os); diff --git a/crates/openconnect/src/ffi/vpn.h b/crates/openconnect/src/ffi/vpn.h index f301cc08..8cb767c3 100644 --- a/crates/openconnect/src/ffi/vpn.h +++ b/crates/openconnect/src/ffi/vpn.h @@ -15,6 +15,8 @@ typedef struct vpn_options const char *script; const char *os; const char *certificate; + const char *sslkey; + const char *key_password; const char *servercert; const uid_t csd_uid; diff --git a/crates/openconnect/src/vpn.rs b/crates/openconnect/src/vpn.rs index 74fb40ed..c79e4ab4 100644 --- a/crates/openconnect/src/vpn.rs +++ b/crates/openconnect/src/vpn.rs @@ -18,6 +18,8 @@ pub struct Vpn { script: CString, os: CString, certificate: Option, + sslkey: Option, + key_password: Option, servercert: Option, csd_uid: u32, @@ -63,7 +65,10 @@ impl Vpn { user_agent: self.user_agent.as_ptr(), script: self.script.as_ptr(), os: self.os.as_ptr(), + 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), csd_uid: self.csd_uid, @@ -110,6 +115,10 @@ pub struct VpnBuilder { user_agent: Option, os: Option, + certificate: Option, + sslkey: Option, + key_password: Option, + csd_uid: u32, csd_wrapper: Option, @@ -128,6 +137,10 @@ impl VpnBuilder { user_agent: None, os: None, + certificate: None, + sslkey: None, + key_password: None, + csd_uid: 0, csd_wrapper: None, @@ -152,6 +165,21 @@ impl VpnBuilder { self } + pub fn certificate>>(mut self, certificate: T) -> Self { + self.certificate = certificate.into(); + self + } + + pub fn sslkey>>(mut self, sslkey: T) -> Self { + self.sslkey = sslkey.into(); + self + } + + pub fn key_password>>(mut self, key_password: T) -> Self { + self.key_password = key_password.into(); + self + } + pub fn csd_uid(mut self, csd_uid: u32) -> Self { self.csd_uid = csd_uid; self @@ -199,7 +227,10 @@ impl VpnBuilder { user_agent: Self::to_cstring(&user_agent), script: Self::to_cstring(&script), 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, csd_uid: self.csd_uid,