Skip to content

Commit

Permalink
feat: support client certificate authentication (related #363)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuezk committed May 19, 2024
1 parent 3bb115b commit 52b6fa6
Show file tree
Hide file tree
Showing 19 changed files with 374 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"LOGNAME",
"oneshot",
"openconnect",
"pkcs",
"pkexec",
"pkey",
"Prelogin",
"prelogon",
"prelogonuserauthcookie",
Expand All @@ -35,6 +37,7 @@
"rspc",
"servercert",
"specta",
"sslkey",
"sysinfo",
"tanstack",
"tauri",
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
56 changes: 53 additions & 3 deletions apps/gpclient/src/connect.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -42,6 +42,13 @@ pub(crate) struct ConnectArgs {
)]
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")]
csd_user: Option<String>,

Expand Down Expand Up @@ -86,11 +93,16 @@ impl ConnectArgs {
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
shared_args: &'a SharedArgs,
latest_key_password: RefCell<Option<String>>,
}

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 {
Expand All @@ -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::<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 as_gateway = self.args.as_gateway;

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions apps/gpservice/src/vpn_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 2 additions & 0 deletions crates/gpapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions crates/gpapi/src/gateway/hip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
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);
Expand Down
5 changes: 1 addition & 4 deletions crates/gpapi/src/gateway/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
56 changes: 55 additions & 1 deletion crates/gpapi/src/gp_params.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -51,6 +55,9 @@ pub struct GpParams {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
// Used for MFA
input_str: Option<String>,
otp: Option<String>,
Expand Down Expand Up @@ -142,6 +149,9 @@ pub struct GpParamsBuilder {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
}

impl GpParamsBuilder {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -194,6 +207,21 @@ impl GpParamsBuilder {
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 {
GpParams {
is_gateway: self.is_gateway,
Expand All @@ -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(),
}
Expand All @@ -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<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)
}
}
5 changes: 1 addition & 4 deletions crates/gpapi/src/portal/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 1 addition & 4 deletions crates/gpapi/src/portal/prelogin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(user_agent)
.build()?;
let client = Client::try_from(gp_params)?;

let res = client
.post(&prelogin_url)
Expand Down
Loading

0 comments on commit 52b6fa6

Please sign in to comment.