diff --git a/Cargo.toml b/Cargo.toml index 9ca6c11e..7ed589ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,13 @@ network_client = ["reqwest", "trust-dns-resolver", "portpicker"] [dependencies] byteorder = "1.2.7" bitflags = "1.0" -rand = "0.6" +rand = "0.8.5" cfg-if = "0.1" chrono = "0.4" md-5 = "0.9" md4 = "0.9" sha2 = "0.9" +sha1 = "0.10" hmac = "0.11" crypto-mac = "0.11" num-derive = "0.2" @@ -36,19 +37,23 @@ serde = "1.0" serde_derive = "1.0" url = "2.2.2" reqwest = { version = "0.11", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], optional = true, default-features = false } -picky-krb = "0.4.0" -picky-asn1 = { version = "0.5.0", features = ["chrono_conversion"] } -picky-asn1-der = "0.3.1" -picky-asn1-x509 = "0.7.0" +picky = { version = "7.0.0-rc.3" } +picky-krb = { version = "0.5.0" } +picky-asn1 = { version = "0.7.0", features = ["chrono_conversion"] } +picky-asn1-der = { version = "0.4.0" } +picky-asn1-x509 = { version = "0.9.0", features = ["pkcs7"] } oid = "0.2.1" +uuid = { version = "1.1", features = ["v4"] } whoami = "0.5" trust-dns-resolver = { version = "0.21.2", optional = true } portpicker = { version = "0.1.1", optional = true } +num-bigint-dig = "0.8.1" [target.'cfg(windows)'.dependencies] winreg = "0.10" winapi = { version = "0.3", features = ["sspi", "rpcdce", "impl-default", "timezoneapi", "wincrypt"] } windows = { version = "0.39.0", features = [ "Win32_Foundation", "Win32_NetworkManagement_Dns"] } +windows-sys = { version = "0.42.0", features = ["Win32_Security_Cryptography", "Win32_Foundation"] } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] async-dnssd = "0.5.0" diff --git a/ffi/src/credentials_attributes.rs b/ffi/src/credentials_attributes.rs index d8cd6ce6..4a60ead7 100644 --- a/ffi/src/credentials_attributes.rs +++ b/ffi/src/credentials_attributes.rs @@ -1,6 +1,7 @@ -use crate::sspi_data_types::{SecChar, SecWChar}; use libc::{c_uint, c_ulong, c_ushort}; +use crate::sspi_data_types::{SecChar, SecWChar}; + pub struct KdcProxySettings { pub proxy_server: String, #[allow(dead_code)] diff --git a/ffi/src/sec_handle.rs b/ffi/src/sec_handle.rs index f8ad2e77..f1338118 100644 --- a/ffi/src/sec_handle.rs +++ b/ffi/src/sec_handle.rs @@ -10,8 +10,8 @@ use sspi::internal::SspiImpl; use sspi::kerberos::config::KerberosConfig; use sspi::kerberos::network_client::reqwest_network_client::ReqwestNetworkClient; use sspi::{ - kerberos, negotiate, ntlm, AuthIdentityBuffers, ClientRequestFlags, DataRepresentation, Error, ErrorKind, Kerberos, - Negotiate, NegotiateConfig, Ntlm, Result, Sspi, + kerberos, negotiate, ntlm, pku2u, AuthIdentityBuffers, ClientRequestFlags, DataRepresentation, Error, ErrorKind, + Kerberos, Negotiate, NegotiateConfig, Ntlm, Pku2u, Pku2uConfig, Result, Sspi, }; #[cfg(windows)] use symbol_rename_macro::rename_symbol; @@ -84,8 +84,9 @@ pub(crate) unsafe fn p_ctxt_handle_to_sspi_context( negotiate::PKG_NAME => { if let Some(kdc_url) = attributes.kdc_url() { let kerberos_config = KerberosConfig::from_kdc_url(&kdc_url, Box::new(ReqwestNetworkClient::new())); - let mut negotiate_config = NegotiateConfig::new_with_kerberos(kerberos_config); - negotiate_config.package_list = attributes.package_list.clone(); + let negotiate_config = + NegotiateConfig::new(Box::new(kerberos_config), attributes.package_list.clone()); + SspiContext::Negotiate(Negotiate::new(negotiate_config)?) } else { let mut negotiate_config = NegotiateConfig::default(); @@ -93,6 +94,15 @@ pub(crate) unsafe fn p_ctxt_handle_to_sspi_context( SspiContext::Negotiate(Negotiate::new(negotiate_config)?) } } + pku2u::PKG_NAME => { + #[cfg(not(target_os = "windows"))] + return Err(Error::new( + ErrorKind::InvalidParameter, + "PKU2U is not supported on non-Windows OS yet".into(), + )); + #[cfg(target_os = "windows")] + SspiContext::Pku2u(Pku2u::new_client_from_config(Pku2uConfig::default_client_config()?)?) + } kerberos::PKG_NAME => { if let Some(kdc_url) = attributes.kdc_url() { SspiContext::Kerberos(Kerberos::new_client_from_config(KerberosConfig::from_kdc_url( diff --git a/src/crypto/rc4.rs b/src/crypto/rc4.rs index 70848f07..cf75ae14 100644 --- a/src/crypto/rc4.rs +++ b/src/crypto/rc4.rs @@ -109,7 +109,7 @@ mod test { fn empty_message() { let key = "key".to_string(); let message = "".to_string(); - let expected = []; + let expected: [u8; 0] = []; assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); } diff --git a/src/dns.rs b/src/dns.rs index 3bab5c99..50fc9efe 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -50,9 +50,9 @@ cfg_if::cfg_if! { if let (Some(namespace), Some(name_server_list)) = (namespace, name_server_list) { let name_servers: Vec = name_server_list.split(';').map(|x| x.to_string()).collect(); rules.push(DnsClientNrptRule { - rule_name: rule_name, - namespace: namespace, - name_servers: name_servers + rule_name, + namespace, + name_servers, }); } } @@ -90,7 +90,7 @@ cfg_if::cfg_if! { } pub fn get_name_servers_for_domain(domain: &str) -> Vec { - let domain_namespace = if domain.starts_with(".") { + let domain_namespace = if domain.starts_with('.') { domain.to_string() } else { format!(".{}", &domain) @@ -102,7 +102,7 @@ cfg_if::cfg_if! { } } - return get_default_name_servers(); + get_default_name_servers() } pub fn detect_kdc_hosts_from_dns_windows(domain: &str) -> Vec { @@ -110,14 +110,14 @@ cfg_if::cfg_if! { let krb_tcp_srv = dns_query_srv_records(krb_tcp_name); if !krb_tcp_srv.is_empty() { - return krb_tcp_srv.iter().map(|x| format!("tcp://{}:88", x).to_owned()).collect() + return krb_tcp_srv.iter().map(|x| format!("tcp://{}:88", x)).collect() } let krb_udp_name = &format!("_kerberos._udp.{}", domain); let krb_udp_srv = dns_query_srv_records(krb_udp_name); if !krb_udp_srv.is_empty() { - return krb_udp_srv.iter().map(|x| format!("udp://{}:88", x).to_owned()).collect() + return krb_udp_srv.iter().map(|x| format!("udp://{}:88", x)).collect() } Vec::new() @@ -232,7 +232,7 @@ cfg_if::cfg_if! { use url::Url; fn get_trust_dns_name_server_from_url_str(url: &str) -> Option { - let url = if !url.contains("://") && url.len() > 0 { + let url = if !url.contains("://") && url.is_empty() { format!("udp://{}", url) } else { url.to_string() @@ -241,7 +241,7 @@ cfg_if::cfg_if! { if let Ok(url) = Url::parse(&url) { if let Some(url_host) = url.host_str() { let url_port = url.port().unwrap_or(53); - let url_protocol = match url.scheme().to_lowercase().as_str() { + let protocol = match url.scheme().to_lowercase().as_str() { "tcp" => Protocol::Tcp, "udp" => Protocol::Udp, _ => Protocol::Udp, @@ -249,8 +249,8 @@ cfg_if::cfg_if! { if let Ok(ip_addr) = IpAddr::from_str(url_host) { let socket_addr = SocketAddr::new(ip_addr, url_port); return Some(NameServerConfig { - socket_addr: socket_addr, - protocol: url_protocol, + socket_addr, + protocol, tls_dns_name: None, trust_nx_responses: false, bind_addr: None diff --git a/src/kdc.rs b/src/kdc.rs index 09ce827c..5f4a9be2 100644 --- a/src/kdc.rs +++ b/src/kdc.rs @@ -5,14 +5,17 @@ cfg_if::cfg_if! { } } -use crate::dns::detect_kdc_hosts_from_dns; -use crate::krb::Krb5Conf; - use std::env; +#[cfg(not(target_os = "windows"))] use std::path::Path; use std::str::FromStr; + use url::Url; +use crate::dns::detect_kdc_hosts_from_dns; +#[cfg(not(target_os = "windows"))] +use crate::krb::Krb5Conf; + #[cfg(target_os = "windows")] pub fn detect_kdc_hosts_from_system(domain: &str) -> Vec { let domain_upper = domain.to_uppercase(); @@ -20,8 +23,8 @@ pub fn detect_kdc_hosts_from_system(domain: &str) -> Vec { let domains_key_path = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Domains"; let domain_key_path = format!("{}\\{}", domains_key_path, &domain_upper); if let Ok(domain_key) = hklm.open_subkey(domain_key_path) { - let kdc_names: Vec = domain_key.get_value("KdcNames").unwrap_or(Vec::new()); - kdc_names.iter().map(|x| format!("tcp://{}:88", x).to_owned()).collect() + let kdc_names: Vec = domain_key.get_value("KdcNames").unwrap_or_default(); + kdc_names.iter().map(|x| format!("tcp://{}:88", x)).collect() } else { Vec::new() } @@ -57,15 +60,13 @@ pub fn detect_kdc_hosts(domain: &str) -> Vec { return vec![kdc_url]; } - let mut kdc_hosts = detect_kdc_hosts_from_system(domain); + let kdc_hosts = detect_kdc_hosts_from_system(domain); if !kdc_hosts.is_empty() { return kdc_hosts; } - kdc_hosts = detect_kdc_hosts_from_dns(domain); - - return kdc_hosts; + detect_kdc_hosts_from_dns(domain) } pub fn detect_kdc_host(domain: &str) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 534bb4d5..2c487eef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,7 @@ pub use kdc::{detect_kdc_host, detect_kdc_url}; pub use crate::sspi::kerberos::config::KerberosConfig; pub use crate::sspi::kerberos::{Kerberos, KERBEROS_VERSION, PACKAGE_INFO as KERBEROS_PACKAGE_INFO}; pub use crate::sspi::negotiate::{Negotiate, NegotiateConfig}; +pub use crate::sspi::pku2u::{self, Pku2u, Pku2uConfig, PACKAGE_INFO as PKU2U_PACKAGE_INFO}; #[cfg(windows)] pub use crate::sspi::winapi; pub use crate::sspi::{ diff --git a/src/sspi.rs b/src/sspi.rs index f54434ce..576d0f83 100644 --- a/src/sspi.rs +++ b/src/sspi.rs @@ -4,6 +4,7 @@ pub mod channel_bindings; pub mod internal; pub mod kerberos; pub mod negotiate; +pub mod pku2u; #[cfg(windows)] pub mod winapi; @@ -60,6 +61,7 @@ pub fn query_security_package_info(package_type: SecurityPackageType) -> Result< SecurityPackageType::Ntlm => Ok(ntlm::PACKAGE_INFO.clone()), SecurityPackageType::Kerberos => Ok(kerberos::PACKAGE_INFO.clone()), SecurityPackageType::Negotiate => Ok(negotiate::PACKAGE_INFO.clone()), + SecurityPackageType::Pku2u => Ok(pku2u::PACKAGE_INFO.clone()), SecurityPackageType::Other(s) => Err(Error::new( ErrorKind::Unknown, format!("Queried info about unknown package: {:?}", s), @@ -1071,6 +1073,7 @@ pub enum SecurityPackageType { Ntlm, Kerberos, Negotiate, + Pku2u, Other(String), } @@ -1080,6 +1083,7 @@ impl AsRef for SecurityPackageType { SecurityPackageType::Ntlm => ntlm::PKG_NAME, SecurityPackageType::Kerberos => kerberos::PKG_NAME, SecurityPackageType::Negotiate => negotiate::PKG_NAME, + SecurityPackageType::Pku2u => pku2u::PKG_NAME, SecurityPackageType::Other(name) => name.as_str(), } } @@ -1091,6 +1095,7 @@ impl string::ToString for SecurityPackageType { SecurityPackageType::Ntlm => ntlm::PKG_NAME.into(), SecurityPackageType::Kerberos => kerberos::PKG_NAME.into(), SecurityPackageType::Negotiate => negotiate::PKG_NAME.into(), + SecurityPackageType::Pku2u => pku2u::PKG_NAME.into(), SecurityPackageType::Other(name) => name.clone(), } } @@ -1104,6 +1109,7 @@ impl str::FromStr for SecurityPackageType { ntlm::PKG_NAME => Ok(SecurityPackageType::Ntlm), kerberos::PKG_NAME => Ok(SecurityPackageType::Kerberos), negotiate::PKG_NAME => Ok(SecurityPackageType::Negotiate), + pku2u::PKG_NAME => Ok(SecurityPackageType::Pku2u), s => Ok(SecurityPackageType::Other(s.to_string())), } } @@ -1513,6 +1519,31 @@ impl From for Error { error_type: ErrorKind::InvalidParameter, description: description.to_string(), }, + KerberosCryptoError::SeedBitLen(description) => Self { + error_type: ErrorKind::InvalidParameter, + description, + }, + KerberosCryptoError::AlgorithmIdentifierData(identifier) => Self { + error_type: ErrorKind::InvalidParameter, + description: format!("unknown algorithm identifier: {:?}", identifier), + }, + } + } +} + +impl From for Error { + fn from(error: picky_krb::crypto::diffie_hellman::DiffieHellmanError) -> Self { + use picky_krb::crypto::diffie_hellman::DiffieHellmanError; + + match error { + DiffieHellmanError::BitLen(description) => Self { + error_type: ErrorKind::InternalError, + description, + }, + error => Self { + error_type: ErrorKind::InternalError, + description: error.to_string(), + }, } } } diff --git a/src/sspi/internal/credssp.rs b/src/sspi/internal/credssp.rs index 25362137..8d2909b7 100644 --- a/src/sspi/internal/credssp.rs +++ b/src/sspi/internal/credssp.rs @@ -22,6 +22,7 @@ use crate::sspi::internal::SspiImpl; use crate::sspi::kerberos::config::KerberosConfig; use crate::sspi::kerberos::Kerberos; use crate::sspi::ntlm::{AuthIdentity, AuthIdentityBuffers, Ntlm, SIGNATURE_SIZE}; +use crate::sspi::pku2u::Pku2uConfig; use crate::sspi::{ self, CertTrustStatus, ClientRequestFlags, ContextNames, ContextSizes, CredentialUse, DataRepresentation, DecryptionFlags, EncryptionFlags, FilledAcceptSecurityContext, FilledAcquireCredentialsHandle, @@ -30,7 +31,7 @@ use crate::sspi::{ }; use crate::{ AcceptSecurityContextResult, AcquireCredentialsHandleResult, ErrorKind, InitializeSecurityContextResult, Negotiate, - NegotiateConfig, + NegotiateConfig, Pku2u, }; pub const EARLY_USER_AUTH_RESULT_PDU_SIZE: usize = 4; @@ -153,9 +154,11 @@ enum EndpointType { } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum ClientMode { Negotiate(NegotiateConfig), Kerberos(KerberosConfig), + Pku2u(Pku2uConfig), Ntlm, } @@ -193,7 +196,7 @@ impl CredSspClient { credentials, public_key, cred_ssp_mode, - client_nonce: OsRng::new()?.gen::<[u8; NONCE_SIZE]>(), + client_nonce: OsRng::default().gen::<[u8; NONCE_SIZE]>(), credentials_handle: None, ts_request_version: TS_REQUEST_VERSION, client_mode, @@ -215,7 +218,7 @@ impl CredSspClient { credentials, public_key, cred_ssp_mode, - client_nonce: OsRng::new()?.gen::<[u8; NONCE_SIZE]>(), + client_nonce: OsRng::default().gen::<[u8; NONCE_SIZE]>(), credentials_handle: None, ts_request_version, client_mode, @@ -235,6 +238,9 @@ impl CredSspClient { ClientMode::Kerberos(kerberos_config) => Some(CredSspContext::new(SspiContext::Kerberos( Kerberos::new_client_from_config(kerberos_config.clone())?, ))), + ClientMode::Pku2u(pku2u) => Some(CredSspContext::new(SspiContext::Pku2u( + Pku2u::new_client_from_config(pku2u.clone())?, + ))), ClientMode::Ntlm => Some(CredSspContext::new(SspiContext::Ntlm(Ntlm::new()))), }; let AcquireCredentialsHandleResult { credentials_handle, .. } = self @@ -286,6 +292,13 @@ impl CredSspClient { peer_version, )?); ts_request.client_nonce = Some(self.client_nonce); + + if let Some(nego_tokens) = &ts_request.nego_tokens { + if nego_tokens.is_empty() { + ts_request.nego_tokens = None; + } + } + self.state = CredSspState::AuthInfo; } @@ -395,6 +408,10 @@ impl> CredSspServer { try_cred_ssp_server!(Kerberos::new_server_from_config(kerberos_config.clone()), ts_request), ))), ClientMode::Ntlm => Some(CredSspContext::new(SspiContext::Ntlm(Ntlm::new()))), + ClientMode::Pku2u(pku2u) => Some(CredSspContext::new(SspiContext::Pku2u(try_cred_ssp_server!( + Pku2u::new_server_from_config(pku2u.clone()), + ts_request + )))), }; let AcquireCredentialsHandleResult { credentials_handle, .. } = try_cred_ssp_server!( self.context @@ -431,20 +448,13 @@ impl> CredSspServer { self.context.as_mut().unwrap().decrypt_ts_credentials(&auth_info), ts_request ); + self.state = CredSspState::Final; Ok(ServerState::Finished(read_credentials.into())) } CredSspState::NegoToken => { - let input = try_cred_ssp_server!( - ts_request.nego_tokens.take().ok_or_else(|| { - sspi::Error::new( - sspi::ErrorKind::InvalidToken, - String::from("Got empty nego_tokens field"), - ) - }), - ts_request - ); + let input = ts_request.nego_tokens.take().unwrap_or_default(); let input_token = SecurityBuffer::new(input, SecurityBufferType::Token); let mut output_token = vec![SecurityBuffer::new(Vec::with_capacity(1024), SecurityBufferType::Token)]; @@ -543,6 +553,7 @@ pub enum SspiContext { Ntlm(Ntlm), Kerberos(Kerberos), Negotiate(Negotiate), + Pku2u(Pku2u), } impl SspiImpl for SspiContext { @@ -557,6 +568,7 @@ impl SspiImpl for SspiContext { SspiContext::Ntlm(ntlm) => builder.transform(ntlm).execute(), SspiContext::Kerberos(kerberos) => builder.transform(kerberos).execute(), SspiContext::Negotiate(negotiate) => builder.transform(negotiate).execute(), + SspiContext::Pku2u(pku2u) => builder.transform(pku2u).execute(), } } @@ -568,6 +580,7 @@ impl SspiImpl for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.initialize_security_context_impl(builder), SspiContext::Kerberos(kerberos) => kerberos.initialize_security_context_impl(builder), SspiContext::Negotiate(negotiate) => negotiate.initialize_security_context_impl(builder), + SspiContext::Pku2u(pku2u) => pku2u.initialize_security_context_impl(builder), } } @@ -579,6 +592,7 @@ impl SspiImpl for SspiContext { SspiContext::Ntlm(ntlm) => builder.transform(ntlm).execute(), SspiContext::Kerberos(kerberos) => builder.transform(kerberos).execute(), SspiContext::Negotiate(negotiate) => builder.transform(negotiate).execute(), + SspiContext::Pku2u(pku2u) => builder.transform(pku2u).execute(), } } } @@ -589,6 +603,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.complete_auth_token(token), SspiContext::Kerberos(kerberos) => kerberos.complete_auth_token(token), SspiContext::Negotiate(negotiate) => negotiate.complete_auth_token(token), + SspiContext::Pku2u(pku2u) => pku2u.complete_auth_token(token), } } @@ -602,6 +617,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.encrypt_message(flags, message, sequence_number), SspiContext::Kerberos(kerberos) => kerberos.encrypt_message(flags, message, sequence_number), SspiContext::Negotiate(negotiate) => negotiate.encrypt_message(flags, message, sequence_number), + SspiContext::Pku2u(pku2u) => pku2u.encrypt_message(flags, message, sequence_number), } } @@ -614,6 +630,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.decrypt_message(message, sequence_number), SspiContext::Kerberos(kerberos) => kerberos.decrypt_message(message, sequence_number), SspiContext::Negotiate(negotiate) => negotiate.decrypt_message(message, sequence_number), + SspiContext::Pku2u(pku2u) => pku2u.decrypt_message(message, sequence_number), } } @@ -622,6 +639,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.query_context_sizes(), SspiContext::Kerberos(kerberos) => kerberos.query_context_sizes(), SspiContext::Negotiate(negotiate) => negotiate.query_context_sizes(), + SspiContext::Pku2u(pku2u) => pku2u.query_context_sizes(), } } fn query_context_names(&mut self) -> sspi::Result { @@ -629,6 +647,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.query_context_names(), SspiContext::Kerberos(kerberos) => kerberos.query_context_names(), SspiContext::Negotiate(negotiate) => negotiate.query_context_names(), + SspiContext::Pku2u(pku2u) => pku2u.query_context_names(), } } fn query_context_package_info(&mut self) -> sspi::Result { @@ -636,6 +655,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.query_context_package_info(), SspiContext::Kerberos(kerberos) => kerberos.query_context_package_info(), SspiContext::Negotiate(negotiate) => negotiate.query_context_package_info(), + SspiContext::Pku2u(pku2u) => pku2u.query_context_package_info(), } } fn query_context_cert_trust_status(&mut self) -> sspi::Result { @@ -643,6 +663,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.query_context_cert_trust_status(), SspiContext::Kerberos(kerberos) => kerberos.query_context_cert_trust_status(), SspiContext::Negotiate(negotiate) => negotiate.query_context_cert_trust_status(), + SspiContext::Pku2u(pku2u) => pku2u.query_context_cert_trust_status(), } } @@ -651,6 +672,7 @@ impl Sspi for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.change_password(change_password), SspiContext::Kerberos(kerberos) => kerberos.change_password(change_password), SspiContext::Negotiate(negotiate) => negotiate.change_password(change_password), + SspiContext::Pku2u(pku2u) => pku2u.change_password(change_password), } } } @@ -661,6 +683,7 @@ impl SspiEx for SspiContext { SspiContext::Ntlm(ntlm) => ntlm.custom_set_auth_identity(identity), SspiContext::Kerberos(kerberos) => kerberos.custom_set_auth_identity(identity), SspiContext::Negotiate(negotiate) => negotiate.custom_set_auth_identity(identity), + SspiContext::Pku2u(pku2u) => pku2u.custom_set_auth_identity(identity), } } } @@ -768,6 +791,7 @@ impl CredSspContext { } SspiContext::Kerberos(_) => {} SspiContext::Negotiate(_) => {} + SspiContext::Pku2u(_) => {} }; self.encrypt_message(&public_key) diff --git a/src/sspi/kerberos.rs b/src/sspi/kerberos.rs index da3ea79c..e2a78833 100644 --- a/src/sspi/kerberos.rs +++ b/src/sspi/kerberos.rs @@ -2,7 +2,7 @@ pub mod client; pub mod config; mod encryption_params; pub mod network_client; -mod server; +pub mod server; mod utils; use std::fmt::Debug; @@ -24,17 +24,16 @@ use self::client::extractors::{ extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_session_key_from_tgs_rep, }; use self::client::generators::{ - generate_ap_req, generate_as_req, generate_authenticator, generate_krb_priv_request, generate_neg_ap_req, - generate_neg_token_init, generate_tgs_req, get_client_principal_name_type, get_client_principal_realm, - ChecksumOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions, AUTHENTICATOR_DEFAULT_CHECKSUM, - DEFAULT_AP_REQ_OPTIONS, + generate_ap_req, generate_as_req, generate_as_req_kdc_body, generate_authenticator, generate_krb_priv_request, + generate_neg_ap_req, generate_neg_token_init, generate_pa_datas_for_as_req, generate_tgs_req, + get_client_principal_name_type, get_client_principal_realm, ChecksumOptions, EncKey, GenerateAsPaDataOptions, + GenerateAsReqOptions, GenerateAuthenticatorOptions, AUTHENTICATOR_DEFAULT_CHECKSUM, DEFAULT_AP_REQ_OPTIONS, }; use self::config::KerberosConfig; use self::server::extractors::extract_tgt_ticket; -use self::utils::{serialize_message, utf16_bytes_to_utf8_string}; +use self::utils::serialize_message; use super::channel_bindings::ChannelBindings; use crate::builders::ChangePassword; -use crate::detect_kdc_url; use crate::kerberos::client::extractors::extract_status_code_from_krb_priv_response; use crate::sspi::kerberos::client::extractors::extract_salt_from_krb_error; use crate::sspi::kerberos::client::generators::{generate_final_neg_token_targ, get_mech_list}; @@ -44,10 +43,11 @@ use crate::sspi::kerberos::server::extractors::{ use crate::sspi::kerberos::utils::{generate_initiator_raw, validate_mic_token}; use crate::sspi::ntlm::AuthIdentityBuffers; use crate::sspi::{self, Error, ErrorKind, Result, Sspi, SspiEx, SspiImpl, PACKAGE_ID_NONE}; +use crate::utils::{generate_random_symmetric_key, utf16_bytes_to_utf8_string}; use crate::{ - AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, ClientResponseFlags, ContextNames, - ContextSizes, CredentialUse, DecryptionFlags, InitializeSecurityContextResult, PackageCapabilities, PackageInfo, - SecurityBuffer, SecurityBufferType, SecurityPackageType, SecurityStatus, ServerResponseFlags, + detect_kdc_url, AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, ClientResponseFlags, + ContextNames, ContextSizes, CredentialUse, DecryptionFlags, InitializeSecurityContextResult, PackageCapabilities, + PackageInfo, SecurityBuffer, SecurityBufferType, SecurityPackageType, SecurityStatus, ServerResponseFlags, }; pub const PKG_NAME: &str = "Kerberos"; @@ -57,15 +57,16 @@ pub const SERVICE_NAME: &str = "TERMSRV"; pub const KADMIN: &str = "kadmin"; pub const CHANGE_PASSWORD_SERVICE_NAME: &str = "changepw"; -const DEFAULT_ENCRYPTION_TYPE: CipherSuite = CipherSuite::Aes256CtsHmacSha196; +// pub const SSPI_KDC_URL_ENV: &str = "SSPI_KDC_URL"; +pub const DEFAULT_ENCRYPTION_TYPE: CipherSuite = CipherSuite::Aes256CtsHmacSha196; /// [MS-KILE](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-KILE/%5bMS-KILE%5d.pdf) /// The RRC field is 12 if no encryption is requested or 28 if encryption is requested -const RRC: u16 = 28; +pub const RRC: u16 = 28; // wrap token header len -const MAX_SIGNATURE: usize = 16; +pub const MAX_SIGNATURE: usize = 16; // minimal len to fit encrypted public key in wrap token -const SECURITY_TRAILER: usize = 60; +pub const SECURITY_TRAILER: usize = 60; /// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2) /// "The service accepts requests on UDP port 464 and TCP port 464 as well." const KPASSWD_PORT: u16 = 464; @@ -105,28 +106,30 @@ pub struct Kerberos { impl Kerberos { pub fn new_client_from_config(config: KerberosConfig) -> Result { let kdc_url = config.url.clone(); + Ok(Self { state: KerberosState::Negotiate, config, auth_identity: None, encryption_params: EncryptionParams::default_for_client(), - seq_number: OsRng::new()?.gen::(), + seq_number: OsRng::default().gen::(), realm: None, - kdc_url: kdc_url, + kdc_url, channel_bindings: None, }) } pub fn new_server_from_config(config: KerberosConfig) -> Result { let kdc_url = config.url.clone(); + Ok(Self { state: KerberosState::Negotiate, config, auth_identity: None, encryption_params: EncryptionParams::default_for_server(), - seq_number: OsRng::new()?.gen::(), + seq_number: OsRng::default().gen::(), realm: None, - kdc_url: kdc_url, + kdc_url, channel_bindings: None, }) } @@ -150,10 +153,7 @@ impl Kerberos { if let Some((realm, kdc_url)) = self.get_kdc() { return match kdc_url.scheme() { "udp" | "tcp" => self.config.network_client.send(&kdc_url, data), - "http" | "https" => self - .config - .network_client - .send_http(&kdc_url, data, Some(realm.to_string())), + "http" | "https" => self.config.network_client.send_http(&kdc_url, data, Some(realm)), _ => Err(Error { error_type: ErrorKind::InternalError, description: "Invalid KDC server URL protocol scheme".to_owned(), @@ -166,9 +166,15 @@ impl Kerberos { }) } - pub fn as_exchange(&mut self, mut options: GenerateAsReqOptions) -> Result { - options.with_pre_auth = false; - let as_req = generate_as_req(&options)?; + pub fn as_exchange( + &mut self, + options: GenerateAsReqOptions, + mut pa_data_options: GenerateAsPaDataOptions, + ) -> Result { + pa_data_options.with_pre_auth = false; + let pa_datas = generate_pa_datas_for_as_req(&pa_data_options)?; + let kdc_req_body = generate_as_req_kdc_body(&options)?; + let as_req = generate_as_req(&pa_datas, kdc_req_body); let response = self.send(&serialize_message(&as_req)?)?; @@ -184,11 +190,14 @@ impl Kerberos { } if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { - options.salt = correct_salt.as_bytes().to_vec() + pa_data_options.salt = correct_salt.as_bytes().to_vec() } - options.with_pre_auth = true; - let as_req = generate_as_req(&options)?; + pa_data_options.with_pre_auth = false; + let pa_datas = generate_pa_datas_for_as_req(&pa_data_options)?; + + let kdc_req_body = generate_as_req_kdc_body(&options)?; + let as_req = generate_as_req(&pa_datas, kdc_req_body); let response = self.send(&serialize_message(&as_req)?)?; @@ -211,7 +220,8 @@ impl Sspi for Kerberos { message: &mut [SecurityBuffer], _sequence_number: u32, ) -> Result { - SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Token)?; + // checks if the Token buffer present + let _ = SecurityBuffer::find_buffer(message, SecurityBufferType::Token)?; let data = SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Data)?; let cipher = self @@ -372,16 +382,22 @@ impl Sspi for Kerberos { let cname_type = get_client_principal_name_type(username, domain); let realm = &get_client_principal_realm(username, domain); - let as_rep = self.as_exchange(GenerateAsReqOptions { - realm, - username, - password, - salt: salt.as_bytes().to_vec(), - enc_params: self.encryption_params.clone(), - cname_type, - snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME], - with_pre_auth: false, - })?; + let as_rep = self.as_exchange( + GenerateAsReqOptions { + realm, + username, + cname_type, + snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME], + // 4 = size of u32 + nonce: &OsRng::default().gen::<[u8; 4]>(), + }, + GenerateAsPaDataOptions { + password, + salt: salt.as_bytes().to_vec(), + enc_params: self.encryption_params.clone(), + with_pre_auth: false, + }, + )?; self.realm = Some(as_rep.0.crealm.0.to_string()); @@ -392,12 +408,23 @@ impl Sspi for Kerberos { let seq_num = self.next_seq_number(); + let enc_type = self + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut OsRng::default()); + let authenticator = generate_authenticator(GenerateAuthenticatorOptions { kdc_rep: &as_rep.0, seq_num: Some(seq_num), - sub_key: Some(OsRng::new()?.gen::<[u8; 32]>().to_vec()), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_seb_key, + }), checksum: None, channel_bindings: self.channel_bindings.as_ref(), + extensions: Vec::new(), })?; let krb_priv = generate_krb_priv_request( @@ -506,6 +533,7 @@ impl SspiImpl for Kerberos { let input = builder.input.as_ref().ok_or_else(|| { sspi::Error::new(ErrorKind::InvalidToken, "Input buffers must be specified".into()) })?; + if let Ok(sec_buffer) = SecurityBuffer::find_buffer(builder.input.as_ref().unwrap(), SecurityBufferType::ChannelBindings) { @@ -536,16 +564,22 @@ impl SspiImpl for Kerberos { let cname_type = get_client_principal_name_type(&username, &domain); let realm = &get_client_principal_realm(&username, &domain); - let as_rep = self.as_exchange(GenerateAsReqOptions { - realm, - username: &username, - password: &password, - salt: salt.as_bytes().to_vec(), - enc_params: self.encryption_params.clone(), - cname_type, - snames: &[TGT_SERVICE_NAME, realm], - with_pre_auth: false, - })?; + let as_rep = self.as_exchange( + GenerateAsReqOptions { + realm, + username: &username, + cname_type, + snames: &[TGT_SERVICE_NAME, realm], + // 4 = size of u32 + nonce: &OsRng::default().gen::<[u8; 4]>(), + }, + GenerateAsPaDataOptions { + password: &password, + salt: salt.as_bytes().to_vec(), + enc_params: self.encryption_params.clone(), + with_pre_auth: false, + }, + )?; self.realm = Some(as_rep.0.crealm.0.to_string()); @@ -554,10 +588,11 @@ impl SspiImpl for Kerberos { let mut authenticator = generate_authenticator(GenerateAuthenticatorOptions { kdc_rep: &as_rep.0, - seq_num: Some(OsRng::new()?.gen::()), + seq_num: Some(OsRng::default().gen::()), sub_key: None, checksum: None, channel_bindings: self.channel_bindings.as_ref(), + extensions: Vec::new(), })?; let session_key_1 = @@ -591,15 +626,28 @@ impl SspiImpl for Kerberos { &self.encryption_params, )?); + let seq_num = self.next_seq_number(); + + let enc_type = self + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut OsRng::default()); + let authenticator = generate_authenticator(GenerateAuthenticatorOptions { kdc_rep: &tgs_rep.0, - seq_num: Some(self.next_seq_number()), - sub_key: Some(OsRng::new()?.gen::<[u8; 32]>().to_vec()), + seq_num: Some(seq_num), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_sub_key, + }), checksum: Some(ChecksumOptions { checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), checksum_value: AUTHENTICATOR_DEFAULT_CHECKSUM.to_vec(), }), channel_bindings: self.channel_bindings.as_ref(), + extensions: Vec::new(), })?; let ap_req = generate_ap_req( diff --git a/src/sspi/kerberos/client/extractors.rs b/src/sspi/kerberos/client/extractors.rs index e2a70c5c..c688e1a6 100644 --- a/src/sspi/kerberos/client/extractors.rs +++ b/src/sspi/kerberos/client/extractors.rs @@ -41,12 +41,7 @@ pub fn extract_session_key_from_as_rep( let key = cipher.generate_key_from_password(password.as_bytes(), salt.as_bytes())?; - let enc_data = cipher - .decrypt(&key, AS_REP_ENC, &as_rep.0.enc_part.0.cipher.0 .0) - .map_err(|e| Error { - error_type: ErrorKind::DecryptFailure, - description: format!("Cannot decrypt as_rep.enc_part: {:?}", e), - })?; + let enc_data = cipher.decrypt(&key, AS_REP_ENC, &as_rep.0.enc_part.0.cipher.0 .0)?; let enc_as_rep_part: EncAsRepPart = picky_asn1_der::from_bytes(&enc_data)?; diff --git a/src/sspi/kerberos/client/generators.rs b/src/sspi/kerberos/client/generators.rs index f8512dba..74593abe 100644 --- a/src/sspi/kerberos/client/generators.rs +++ b/src/sspi/kerberos/client/generators.rs @@ -1,9 +1,7 @@ -use std::convert::TryFrom; use std::str::FromStr; use chrono::{Duration, Utc}; use md5::{Digest, Md5}; -use oid::ObjectIdentifier; use picky_asn1::bit_string::BitString; use picky_asn1::date::GeneralizedTime; use picky_asn1::restricted_string::IA5String; @@ -15,7 +13,7 @@ use picky_asn1::wrapper::{ }; use picky_asn1_der::application_tag::ApplicationTag; use picky_asn1_der::Asn1RawDer; -use picky_asn1_x509::oids::{KRB5, KRB5_USER_TO_USER, MS_KRB5, SPNEGO}; +use picky_asn1_x509::oids; use picky_krb::constants::gss_api::{ ACCEPT_COMPLETE, ACCEPT_INCOMPLETE, AP_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE, TGT_REQ_TOKEN_ID, }; @@ -50,7 +48,7 @@ use crate::{Error, ErrorKind}; const TGT_TICKET_LIFETIME_DAYS: i64 = 3; const NONCE_LEN: usize = 4; -const MAX_MICROSECONDS_IN_SECOND: u32 = 999_999; +pub const MAX_MICROSECONDS_IN_SECOND: u32 = 999_999; const MD5_CHECKSUM_TYPE: [u8; 1] = [0x07]; pub const DEFAULT_AS_REQ_OPTIONS: [u8; 4] = [0x40, 0x81, 0x00, 0x10]; @@ -89,33 +87,21 @@ pub fn get_client_principal_realm(username: &str, domain: &str) -> String { domain.to_string() } -pub struct GenerateAsReqOptions<'a> { - pub realm: &'a str, - pub username: &'a str, +pub struct GenerateAsPaDataOptions<'a> { pub password: &'a str, pub salt: Vec, pub enc_params: EncryptionParams, - pub cname_type: u8, - pub snames: &'a [&'a str], pub with_pre_auth: bool, } -pub fn generate_as_req(options: &GenerateAsReqOptions) -> Result { - let GenerateAsReqOptions { - realm, - username, +pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions) -> Result> { + let GenerateAsPaDataOptions { password, salt, enc_params, - cname_type, - snames, with_pre_auth, } = options; - let expiration_date = Utc::now() - .checked_add_signed(Duration::days(TGT_TICKET_LIFETIME_DAYS)) - .unwrap(); - let mut pa_datas = if *with_pre_auth { let current_date = Utc::now(); let mut microseconds = current_date.timestamp_subsec_micros(); @@ -155,9 +141,34 @@ pub fn generate_as_req(options: &GenerateAsReqOptions) -> Result { })?)), }); + Ok(pa_datas) +} + +pub struct GenerateAsReqOptions<'a> { + pub realm: &'a str, + pub username: &'a str, + pub cname_type: u8, + pub snames: &'a [&'a str], + pub nonce: &'a [u8], +} + +pub fn generate_as_req_kdc_body(options: &GenerateAsReqOptions) -> Result { + let GenerateAsReqOptions { + realm, + username, + cname_type, + snames, + nonce, + } = options; + + let expiration_date = Utc::now() + .checked_add_signed(Duration::days(TGT_TICKET_LIFETIME_DAYS)) + .unwrap(); + let address = Some(ExplicitContextTag9::from(Asn1SequenceOf::from(vec![HostAddress { addr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NET_BIOS_ADDR_TYPE])), - address: ExplicitContextTag1::from(OctetStringAsn1::from(whoami::hostname().as_bytes().to_vec())), + // address: ExplicitContextTag1::from(OctetStringAsn1::from(whoami::hostname().as_bytes().to_vec())), + address: ExplicitContextTag1::from(OctetStringAsn1::from("DESKTOP-8F33RFH\x20".as_bytes().to_vec())), }]))); let mut service_names = Vec::with_capacity(snames.len()); @@ -165,40 +176,46 @@ pub fn generate_as_req(options: &GenerateAsReqOptions) -> Result { service_names.push(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?)); } - Ok(AsReq::from(KdcReq { + Ok(KdcReqBody { + kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes( + DEFAULT_AS_REQ_OPTIONS.to_vec(), + ))), + cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![*cname_type])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![KerberosStringAsn1::from( + IA5String::from_string((*username).into())?, + )])), + }))), + realm: ExplicitContextTag2::from(Realm::from(IA5String::from_string((*realm).into())?)), + sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)), + }))), + from: Optional::from(None), + till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))), + rtime: Optional::from(Some(ExplicitContextTag6::from(GeneralizedTimeAsn1::from( + GeneralizedTime::from(expiration_date), + )))), + nonce: ExplicitContextTag7::from(IntegerAsn1::from(nonce.to_vec())), + etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![ + IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]), + IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]), + ])), + addresses: Optional::from(address), + enc_authorization_data: Optional::from(None), + additional_tickets: Optional::from(None), + }) +} + +pub fn generate_as_req(pa_datas: &[PaData], kdc_req_body: KdcReqBody) -> AsReq { + AsReq::from(KdcReq { pvno: ExplicitContextTag1::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), msg_type: ExplicitContextTag2::from(IntegerAsn1::from(vec![AS_REQ_MSG_TYPE])), - padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(pa_datas)))), - req_body: ExplicitContextTag4::from(KdcReqBody { - kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes( - DEFAULT_AS_REQ_OPTIONS.to_vec(), - ))), - cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName { - name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![*cname_type])), - name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![KerberosStringAsn1::from( - IA5String::from_string((*username).into())?, - )])), - }))), - realm: ExplicitContextTag2::from(Realm::from(IA5String::from_string((*realm).into())?)), - sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName { - name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), - name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)), - }))), - from: Optional::from(None), - till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))), - rtime: Optional::from(Some(ExplicitContextTag6::from(GeneralizedTimeAsn1::from( - GeneralizedTime::from(expiration_date), - )))), - nonce: ExplicitContextTag7::from(IntegerAsn1::from(OsRng::new()?.gen::<[u8; NONCE_LEN]>().to_vec())), - etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![ - IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]), - IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]), - ])), - addresses: Optional::from(address), - enc_authorization_data: Optional::from(None), - additional_tickets: Optional::from(None), - }), - })) + padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from( + pa_datas.to_owned(), + )))), + req_body: ExplicitContextTag4::from(kdc_req_body), + }) } pub fn generate_tgs_req( @@ -246,7 +263,7 @@ pub fn generate_tgs_req( from: Optional::from(None), till: ExplicitContextTag5::from(GeneralizedTimeAsn1::from(GeneralizedTime::from(expiration_date))), rtime: Optional::from(None), - nonce: ExplicitContextTag7::from(IntegerAsn1::from(OsRng::new()?.gen::<[u8; NONCE_LEN]>().to_vec())), + nonce: ExplicitContextTag7::from(IntegerAsn1::from(OsRng::default().gen::<[u8; NONCE_LEN]>().to_vec())), etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![ IntegerAsn1::from(vec![CipherSuite::Aes256CtsHmacSha196.into()]), IntegerAsn1::from(vec![CipherSuite::Aes128CtsHmacSha196.into()]), @@ -300,12 +317,23 @@ pub struct ChecksumOptions { pub checksum_value: Vec, } +pub struct AuthenticatorChecksumExtension { + pub extension_type: u32, + pub extension_value: Vec, +} + +pub struct EncKey { + pub key_type: CipherSuite, + pub key_value: Vec, +} + pub struct GenerateAuthenticatorOptions<'a> { pub kdc_rep: &'a KdcRep, pub seq_num: Option, - pub sub_key: Option>, + pub sub_key: Option, pub checksum: Option, pub channel_bindings: Option<&'a ChannelBindings>, + pub extensions: Vec, } pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result { @@ -315,6 +343,7 @@ pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result Result MechTypeList { - MechTypeList::from(vec![ - MechType::from(ObjectIdentifier::try_from(MS_KRB5).unwrap()), - MechType::from(ObjectIdentifier::try_from(KRB5).unwrap()), - ]) + MechTypeList::from(vec![MechType::from(oids::ms_krb5()), MechType::from(oids::krb5())]) } pub fn generate_neg_token_init(username: &str) -> Result> { let krb5_neg_token_init: ApplicationTag<_, 0> = ApplicationTag::from(KrbMessage { - krb5_oid: ObjectIdentifierAsn1::from(ObjectIdentifier::try_from(KRB5_USER_TO_USER).unwrap()), + krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), krb5_token_id: TGT_REQ_TOKEN_ID, krb_msg: TgtReq { pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), @@ -464,7 +488,7 @@ pub fn generate_neg_token_init(username: &str) -> Result Result Result> { let krb_blob: ApplicationTag<_, 0> = ApplicationTag(KrbMessage { - krb5_oid: ObjectIdentifierAsn1::from(ObjectIdentifier::try_from(KRB5_USER_TO_USER).unwrap()), + krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), krb5_token_id: AP_REQ_TOKEN_ID, krb_msg: ap_req, }); diff --git a/src/sspi/kerberos/config.rs b/src/sspi/kerberos/config.rs index e683cf7f..54f13c17 100644 --- a/src/sspi/kerberos/config.rs +++ b/src/sspi/kerberos/config.rs @@ -6,8 +6,9 @@ use url::Url; #[cfg(feature = "network_client")] use super::network_client::reqwest_network_client::ReqwestNetworkClient; use super::network_client::NetworkClient; - use crate::kdc::detect_kdc_url; +use crate::negotiate::{NegotiatedProtocol, ProtocolConfig}; +use crate::{Kerberos, Result}; pub struct KerberosConfig { pub url: Option, @@ -22,6 +23,18 @@ impl Debug for KerberosConfig { } } +impl ProtocolConfig for KerberosConfig { + fn new_client(&self) -> Result { + Ok(NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config( + Clone::clone(self), + )?)) + } + + fn clone(&self) -> Box { + Box::new(Clone::clone(self)) + } +} + pub fn parse_kdc_url(mut kdc: String) -> Option { if !kdc.contains("://") { kdc = format!("tcp://{}", kdc); @@ -32,11 +45,9 @@ pub fn parse_kdc_url(mut kdc: String) -> Option { impl KerberosConfig { pub fn get_kdc_url(self, domain: &str) -> Option { if let Some(kdc_url) = self.url { - Some(kdc_url).clone() - } else if let Some(kdc_url) = detect_kdc_url(&domain) { - Some(kdc_url).clone() + Some(kdc_url) } else { - None + detect_kdc_url(domain) } } diff --git a/src/sspi/kerberos/server/extractors.rs b/src/sspi/kerberos/server/extractors.rs index 18e87066..a2ebf579 100644 --- a/src/sspi/kerberos/server/extractors.rs +++ b/src/sspi/kerberos/server/extractors.rs @@ -25,8 +25,7 @@ pub fn extract_ap_rep_from_neg_token_targ(token: &NegTokenTarg1) -> Result = - picky_asn1_der::from_reader(&mut data).map_err(|e| Error::new(ErrorKind::InternalError, format!("{:?}", e)))?; + let _oid: ApplicationTag = picky_asn1_der::from_reader(&mut data)?; let mut t = [0, 0]; data.read_exact(&mut t)?; @@ -52,8 +51,7 @@ pub fn extract_sub_session_key_from_ap_rep( description: format!("Cannot decrypt ap_rep.enc_part: {:?}", err), })?; - let ap_rep_enc_part: EncApRepPart = - picky_asn1_der::from_bytes(&res).map_err(|e| Error::new(ErrorKind::InvalidToken, format!("{:?}", e)))?; + let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?; Ok(ap_rep_enc_part .0 @@ -67,21 +65,18 @@ pub fn extract_sub_session_key_from_ap_rep( } pub fn extract_tgt_ticket(data: &[u8]) -> Result> { - let neg_token_targ: NegTokenTarg1 = - picky_asn1_der::from_bytes(data).map_err(|e| Error::new(ErrorKind::InvalidToken, format!("{:?}", e)))?; + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(data)?; if let Some(resp_token) = neg_token_targ.0.response_token.0.as_ref().map(|ticket| &ticket.0 .0) { let mut c = resp_token.as_slice(); - let _oid: ApplicationTag = - picky_asn1_der::from_reader(&mut c).map_err(|e| Error::new(ErrorKind::InvalidToken, format!("{:?}", e)))?; + let _oid: ApplicationTag = picky_asn1_der::from_reader(&mut c)?; let mut t = [0, 0]; c.read_exact(&mut t)?; - let tgt_rep: TgtRep = - picky_asn1_der::from_reader(&mut c).map_err(|e| Error::new(ErrorKind::InvalidToken, format!("{:?}", e)))?; + let tgt_rep: TgtRep = picky_asn1_der::from_reader(&mut c)?; Ok(Some(tgt_rep.ticket.0)) } else { diff --git a/src/sspi/kerberos/utils.rs b/src/sspi/kerberos/utils.rs index 1351dffa..539be35b 100644 --- a/src/sspi/kerberos/utils.rs +++ b/src/sspi/kerberos/utils.rs @@ -1,4 +1,3 @@ -use std::convert::TryInto; use std::io::Write; use picky_krb::constants::key_usages::INITIATOR_SIGN; @@ -23,16 +22,6 @@ pub fn serialize_message(v: &T) -> Result> { Ok(data) } -pub fn utf16_bytes_to_utf8_string(data: &[u8]) -> String { - debug_assert_eq!(data.len() % 2, 0); - String::from_utf16_lossy( - &data - .chunks(2) - .map(|c| u16::from_le_bytes(c.try_into().unwrap())) - .collect::>(), - ) -} - pub fn validate_mic_token(raw_token: &[u8], key_usage: i32, params: &EncryptionParams) -> Result<()> { let token = MicToken::decode(raw_token)?; diff --git a/src/sspi/negotiate.rs b/src/sspi/negotiate.rs index 47274f3b..fa9484cd 100644 --- a/src/sspi/negotiate.rs +++ b/src/sspi/negotiate.rs @@ -1,15 +1,23 @@ +use std::fmt::Debug; + use lazy_static::lazy_static; use crate::internal::SspiImpl; +#[cfg(feature = "network_client")] use crate::kdc::detect_kdc_url; +#[cfg(feature = "network_client")] use crate::kerberos::client::generators::get_client_principal_realm; #[cfg(feature = "network_client")] use crate::kerberos::network_client::reqwest_network_client::ReqwestNetworkClient; +use crate::ntlm::NtlmConfig; use crate::sspi::{Result, PACKAGE_ID_NONE}; +use crate::utils::is_azure_ad_domain; +#[cfg(feature = "network_client")] +use crate::KerberosConfig; use crate::{ builders, AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, AuthIdentityBuffers, CertTrustStatus, ContextNames, ContextSizes, CredentialUse, DecryptionFlags, Error, ErrorKind, - InitializeSecurityContextResult, Kerberos, KerberosConfig, Ntlm, PackageCapabilities, PackageInfo, SecurityBuffer, + InitializeSecurityContextResult, Kerberos, Ntlm, PackageCapabilities, PackageInfo, Pku2u, SecurityBuffer, SecurityPackageType, SecurityStatus, Sspi, SspiEx, }; @@ -25,37 +33,55 @@ lazy_static! { }; } -#[derive(Debug, Clone)] +pub trait ProtocolConfig: Debug { + fn new_client(&self) -> Result; + fn clone(&self) -> Box; +} + +#[derive(Debug)] pub struct NegotiateConfig { + pub protocol_config: Box, pub package_list: Option, - pub krb_config: Option, } impl NegotiateConfig { - pub fn new() -> Self { + pub fn new(protocol_config: Box, package_list: Option) -> Self { Self { - package_list: None, - krb_config: None, + protocol_config, + package_list, } } - pub fn new_with_kerberos(krb_config: KerberosConfig) -> Self { + pub fn from_protocol_config(protocol_config: Box) -> Self { Self { + protocol_config, package_list: None, - krb_config: Some(krb_config), } } } impl Default for NegotiateConfig { fn default() -> Self { - Self::new() + Self { + protocol_config: Box::new(NtlmConfig), + package_list: None, + } + } +} + +impl Clone for NegotiateConfig { + fn clone(&self) -> Self { + Self { + protocol_config: self.protocol_config.clone(), + package_list: None, + } } } #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum NegotiatedProtocol { + Pku2u(Pku2u), Kerberos(Kerberos), Ntlm(Ntlm), } @@ -67,99 +93,121 @@ pub struct Negotiate { auth_identity: Option, } +struct PackageListConfig { + ntlm: bool, + kerberos: bool, + pku2u: bool, +} + impl Negotiate { pub fn new(config: NegotiateConfig) -> Result { - let mut protocol = if let Some(krb_config) = config.krb_config { - Kerberos::new_client_from_config(krb_config) - .map(NegotiatedProtocol::Kerberos) - .unwrap_or_else(|_| NegotiatedProtocol::Ntlm(Ntlm::new())) - } else { - NegotiatedProtocol::Ntlm(Ntlm::new()) - }; - - if let Some(filtered_protocol) = Self::filter_protocol(&protocol, &config.package_list) { + let mut protocol = config.protocol_config.new_client()?; + if let Some(filtered_protocol) = Self::filter_protocol(&protocol, &config.package_list)? { protocol = filtered_protocol; } Ok(Negotiate { - protocol: protocol, - package_list: config.package_list.clone(), + protocol, + package_list: config.package_list, auth_identity: None, }) } - #[cfg(feature = "network_client")] + // negotiates the authorization protocol based on the username and the domain + // Decision rules: + // 1) if `self.protocol` is not NTLM then we've already negotiated a suitable protocol. Nothing to do. + // 2) if the provided domain is Azure AD domain then it'll use Pku2u + // 3) if the provided username is FQDN and we can resolve KDC then it'll use Kerberos + // 4) if SSPI_KDC_URL_ENV is set then it'll also use Kerberos + // 5) in any other cases, it'll use NTLM fn negotiate_protocol(&mut self, username: &str, domain: &str) -> Result<()> { if let NegotiatedProtocol::Ntlm(_) = &self.protocol { - let realm = get_client_principal_realm(username, domain); - if let Some(kdc_url) = detect_kdc_url(&realm) { - self.protocol = NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(KerberosConfig { - url: Some(kdc_url), - network_client: Box::new(ReqwestNetworkClient::new()), - })?) + #[cfg(target_os = "windows")] + if is_azure_ad_domain(domain) { + use super::pku2u::Pku2uConfig; + + self.protocol = + NegotiatedProtocol::Pku2u(Pku2u::new_client_from_config(Pku2uConfig::default_client_config()?)?); + } + + #[cfg(feature = "network_client")] + if let Some(host) = detect_kdc_url(&get_client_principal_realm(username, domain)) { + self.protocol = + NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config(crate::KerberosConfig { + url: Some(host), + network_client: Box::new(ReqwestNetworkClient::new()), + })?); } } - if let Some(filtered_protocol) = Self::filter_protocol(&self.protocol, &self.package_list) { + if let Some(filtered_protocol) = Self::filter_protocol(&self.protocol, &self.package_list)? { self.protocol = filtered_protocol; } Ok(()) } - fn get_package_list_config(package_list: &Option) -> (bool, bool) { - let mut ntlm_package: bool = true; - let mut kerberos_package: bool = true; + fn parse_package_list_config(package_list: &Option) -> PackageListConfig { + let mut ntlm: bool = true; + let mut kerberos: bool = true; + let mut pku2u: bool = true; if let Some(package_list) = &package_list { - for package in package_list.split(",") { - let (package_name, enabled) = if package.starts_with("!") { - let package_name = package[1..].to_lowercase(); - (package_name, false) + for package in package_list.split(',') { + let (package_name, enabled) = if let Some(package_name) = package.strip_prefix('!') { + (package_name.to_lowercase(), false) } else { let package_name = package.to_lowercase(); (package_name, true) }; match package_name.as_str() { - "ntlm" => { - ntlm_package = enabled; - } - "kerberos" => { - kerberos_package = enabled; - } - _ => { - eprintln!("unexpected package name: {}", &package_name); - } + "ntlm" => ntlm = enabled, + "kerberos" => kerberos = enabled, + "pku2u" => pku2u = enabled, + _ => eprintln!("unexpected package name: {}", &package_name), } } } - (ntlm_package, kerberos_package) + PackageListConfig { ntlm, kerberos, pku2u } } fn filter_protocol( negotiated_protocol: &NegotiatedProtocol, package_list: &Option, - ) -> Option { + ) -> Result> { let mut filtered_protocol = None; - let (ntlm_package, kerberos_package) = Self::get_package_list_config(package_list); + let PackageListConfig { ntlm, kerberos, pku2u } = Self::parse_package_list_config(package_list); match &negotiated_protocol { + NegotiatedProtocol::Pku2u(_) => { + if !pku2u { + filtered_protocol = Some(NegotiatedProtocol::Ntlm(Ntlm::new())); + } + } NegotiatedProtocol::Kerberos(_) => { - if !kerberos_package { + if !kerberos { filtered_protocol = Some(NegotiatedProtocol::Ntlm(Ntlm::new())); } } NegotiatedProtocol::Ntlm(_) => { - if !ntlm_package { - let kerberos_client = Kerberos::new_client_from_config(KerberosConfig::from_env()).unwrap(); + #[cfg(not(feature = "network_client"))] + if !ntlm { + return Err(Error::new( + ErrorKind::InternalError, + "Can not initialize Kerberos: network client is not provided".into(), + )); + } + #[cfg(feature = "network_client")] + if !ntlm { + let kerberos_client = Kerberos::new_client_from_config(KerberosConfig::from_env())?; filtered_protocol = Some(NegotiatedProtocol::Kerberos(kerberos_client)); } } } - filtered_protocol + Ok(filtered_protocol) } } @@ -168,6 +216,7 @@ impl SspiEx for Negotiate { self.auth_identity = Some(identity.clone().into()); match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.custom_set_auth_identity(identity), NegotiatedProtocol::Kerberos(kerberos) => kerberos.custom_set_auth_identity(identity), NegotiatedProtocol::Ntlm(ntlm) => ntlm.custom_set_auth_identity(identity), } @@ -177,6 +226,7 @@ impl SspiEx for Negotiate { impl Sspi for Negotiate { fn complete_auth_token(&mut self, token: &mut [SecurityBuffer]) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.complete_auth_token(token), NegotiatedProtocol::Kerberos(kerberos) => kerberos.complete_auth_token(token), NegotiatedProtocol::Ntlm(ntlm) => ntlm.complete_auth_token(token), } @@ -189,6 +239,7 @@ impl Sspi for Negotiate { sequence_number: u32, ) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.encrypt_message(flags, message, sequence_number), NegotiatedProtocol::Kerberos(kerberos) => kerberos.encrypt_message(flags, message, sequence_number), NegotiatedProtocol::Ntlm(ntlm) => ntlm.encrypt_message(flags, message, sequence_number), } @@ -196,6 +247,7 @@ impl Sspi for Negotiate { fn decrypt_message(&mut self, message: &mut [SecurityBuffer], sequence_number: u32) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.decrypt_message(message, sequence_number), NegotiatedProtocol::Kerberos(kerberos) => kerberos.decrypt_message(message, sequence_number), NegotiatedProtocol::Ntlm(ntlm) => ntlm.decrypt_message(message, sequence_number), } @@ -203,6 +255,7 @@ impl Sspi for Negotiate { fn query_context_sizes(&mut self) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_sizes(), NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_sizes(), NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_sizes(), } @@ -210,6 +263,7 @@ impl Sspi for Negotiate { fn query_context_names(&mut self) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_names(), NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_names(), NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_names(), } @@ -217,6 +271,7 @@ impl Sspi for Negotiate { fn query_context_package_info(&mut self) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_package_info(), NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_package_info(), NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_package_info(), } @@ -224,16 +279,17 @@ impl Sspi for Negotiate { fn query_context_cert_trust_status(&mut self) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.query_context_cert_trust_status(), NegotiatedProtocol::Kerberos(kerberos) => kerberos.query_context_cert_trust_status(), NegotiatedProtocol::Ntlm(ntlm) => ntlm.query_context_cert_trust_status(), } } fn change_password(&mut self, change_password: builders::ChangePassword) -> Result<()> { - #[cfg(feature = "network_client")] self.negotiate_protocol(&change_password.account_name, &change_password.domain_name)?; match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.change_password(change_password), NegotiatedProtocol::Kerberos(kerberos) => kerberos.change_password(change_password), NegotiatedProtocol::Ntlm(ntlm) => ntlm.change_password(change_password), } @@ -255,18 +311,14 @@ impl SspiImpl for Negotiate { )); } - #[cfg(feature = "network_client")] if let Some(identity) = builder.auth_data { - if let Some(domain) = &identity.domain { - self.negotiate_protocol(&identity.username, &domain)?; - } else { - self.negotiate_protocol(&identity.username, "")?; - } + self.negotiate_protocol(&identity.username, identity.domain.as_deref().unwrap_or_default())?; } self.auth_identity = builder.auth_data.cloned().map(AuthIdentityBuffers::from); match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.acquire_credentials_handle_impl(builder)?, NegotiatedProtocol::Kerberos(kerberos) => kerberos.acquire_credentials_handle_impl(builder)?, NegotiatedProtocol::Ntlm(ntlm) => ntlm.acquire_credentials_handle_impl(builder)?, }; @@ -281,14 +333,13 @@ impl SspiImpl for Negotiate { &mut self, builder: &mut builders::FilledInitializeSecurityContext<'a, Self::CredentialsHandle>, ) -> Result { - #[cfg(feature = "network_client")] if let Some(Some(identity)) = builder.credentials_handle { - let auth_identity: AuthIdentity = (*identity).clone().into(); - let username = auth_identity.username.to_owned(); + let auth_identity: AuthIdentity = identity.clone().into(); + if let Some(domain) = &auth_identity.domain { - self.negotiate_protocol(&username, &domain)?; + self.negotiate_protocol(&auth_identity.username, domain)?; } else { - self.negotiate_protocol(&username, "")?; + self.negotiate_protocol(&auth_identity.username, "")?; } } @@ -305,6 +356,7 @@ impl SspiImpl for Negotiate { } match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.initialize_security_context_impl(builder), NegotiatedProtocol::Kerberos(kerberos) => kerberos.initialize_security_context_impl(builder), NegotiatedProtocol::Ntlm(ntlm) => ntlm.initialize_security_context_impl(builder), } @@ -315,6 +367,7 @@ impl SspiImpl for Negotiate { builder: builders::FilledAcceptSecurityContext<'a, Self::AuthenticationData, Self::CredentialsHandle>, ) -> Result { match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => pku2u.accept_security_context_impl(builder), NegotiatedProtocol::Kerberos(kerberos) => kerberos.accept_security_context_impl(builder), NegotiatedProtocol::Ntlm(ntlm) => ntlm.accept_security_context_impl(builder), } diff --git a/src/sspi/ntlm.rs b/src/sspi/ntlm.rs index 3806072b..27c8c5e5 100644 --- a/src/sspi/ntlm.rs +++ b/src/sspi/ntlm.rs @@ -12,6 +12,7 @@ use serde_derive::{Deserialize, Serialize}; use super::channel_bindings::ChannelBindings; use crate::crypto::{compute_hmac_md5, Rc4, HASH_SIZE}; +use crate::negotiate::{NegotiatedProtocol, ProtocolConfig}; use crate::sspi::internal::SspiImpl; use crate::sspi::{ self, CertTrustStatus, ClientResponseFlags, ContextNames, ContextSizes, CredentialUse, DecryptionFlags, @@ -19,7 +20,9 @@ use crate::sspi::{ PackageCapabilities, PackageInfo, SecurityBuffer, SecurityBufferType, SecurityPackageType, SecurityStatus, ServerResponseFlags, Sspi, SspiEx, PACKAGE_ID_NONE, }; -use crate::{utils, AcceptSecurityContextResult, AcquireCredentialsHandleResult, InitializeSecurityContextResult}; +use crate::{ + utils, AcceptSecurityContextResult, AcquireCredentialsHandleResult, InitializeSecurityContextResult, Result, +}; pub const PKG_NAME: &str = "NTLM"; pub const NTLM_VERSION_SIZE: usize = 8; @@ -48,6 +51,19 @@ lazy_static! { }; } +#[derive(Debug, Clone)] +pub struct NtlmConfig; + +impl ProtocolConfig for NtlmConfig { + fn new_client(&self) -> Result { + Ok(NegotiatedProtocol::Ntlm(Ntlm::new())) + } + + fn clone(&self) -> Box { + Box::new(Clone::clone(self)) + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum NtlmState { Initial, diff --git a/src/sspi/ntlm/messages/client/authenticate.rs b/src/sspi/ntlm/messages/client/authenticate.rs index 979cc17f..1fa6ab97 100644 --- a/src/sspi/ntlm/messages/client/authenticate.rs +++ b/src/sspi/ntlm/messages/client/authenticate.rs @@ -118,7 +118,7 @@ pub fn write_authenticate( ntlm_v2_hash.as_ref(), challenge_message.timestamp, )?; - let session_key = OsRng::new()?.gen::<[u8; SESSION_KEY_SIZE]>(); + let session_key = OsRng::default().gen::<[u8; SESSION_KEY_SIZE]>(); let encrypted_session_key_vec = Rc4::new(&key_exchange_key).process(session_key.as_ref()); let mut encrypted_session_key = [0x00; ENCRYPTED_RANDOM_SESSION_KEY_SIZE]; encrypted_session_key.clone_from_slice(encrypted_session_key_vec.as_ref()); diff --git a/src/sspi/ntlm/messages/computations.rs b/src/sspi/ntlm/messages/computations.rs index f39674a3..f9acd428 100644 --- a/src/sspi/ntlm/messages/computations.rs +++ b/src/sspi/ntlm/messages/computations.rs @@ -116,7 +116,7 @@ pub fn get_authenticate_target_info( } pub fn generate_challenge() -> Result<[u8; CHALLENGE_SIZE], rand::Error> { - Ok(OsRng::new()?.gen::<[u8; CHALLENGE_SIZE]>()) + Ok(OsRng::default().gen::<[u8; CHALLENGE_SIZE]>()) } pub fn generate_timestamp() -> sspi::Result { diff --git a/src/sspi/pku2u.rs b/src/sspi/pku2u.rs new file mode 100644 index 00000000..bb2a81f4 --- /dev/null +++ b/src/sspi/pku2u.rs @@ -0,0 +1,798 @@ +#[cfg(target_os = "windows")] +mod cert_utils; +mod config; +mod extractors; +mod generators; +#[macro_use] +mod macros; +mod validate; + +use std::io::Write; +use std::str::FromStr; + +pub use config::Pku2uConfig; +use lazy_static::lazy_static; +use picky_asn1_x509::signed_data::SignedData; +use picky_krb::constants::gss_api::{AP_REQ_TOKEN_ID, AS_REQ_TOKEN_ID, AUTHENTICATOR_CHECKSUM_TYPE}; +use picky_krb::constants::key_usages::{ACCEPTOR_SIGN, INITIATOR_SIGN}; +use picky_krb::crypto::diffie_hellman::{generate_key, DhNonce}; +use picky_krb::crypto::{ChecksumSuite, CipherSuite}; +use picky_krb::gss_api::{NegTokenTarg1, WrapToken}; +use picky_krb::messages::{ApRep, AsRep}; +use picky_krb::negoex::data_types::MessageType; +use picky_krb::negoex::messages::{Exchange, Nego, Verify}; +use picky_krb::negoex::{NegoexMessage, RANDOM_ARRAY_SIZE}; +use picky_krb::pkinit::PaPkAsRep; +use rand::rngs::OsRng; +use rand::Rng; +use uuid::Uuid; + +use self::generators::{ + generate_client_dh_parameters, generate_neg, generate_neg_token_init, generate_neg_token_targ, + generate_pa_datas_for_as_req, generate_pku2u_nego_req, generate_server_dh_parameters, DH_NONCE_LEN, + WELLKNOWN_REALM, +}; +use crate::builders::ChangePassword; +use crate::internal::SspiImpl; +use crate::kerberos::client::generators::{ + generate_ap_req, generate_as_req, generate_as_req_kdc_body, ChecksumOptions, EncKey, GenerateAsReqOptions, + GenerateAuthenticatorOptions, +}; +use crate::kerberos::server::extractors::extract_sub_session_key_from_ap_rep; +use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE, MAX_SIGNATURE, RRC, SECURITY_TRAILER}; +use crate::pku2u::generators::generate_as_req_username_from_certificate; +use crate::sspi::pku2u::cert_utils::validation::validate_server_p2p_certificate; +use crate::sspi::pku2u::extractors::{ + extract_krb_rep, extract_pa_pk_as_rep, extract_server_dh_public_key, extract_server_nonce, + extract_session_key_from_as_rep, +}; +use crate::sspi::pku2u::generators::{generate_authenticator, generate_authenticator_extension}; +use crate::sspi::pku2u::validate::validate_signed_data; +use crate::sspi::{self, PACKAGE_ID_NONE}; +use crate::utils::generate_random_symmetric_key; +use crate::{ + AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, AuthIdentityBuffers, CertTrustStatus, + ClientResponseFlags, ContextNames, ContextSizes, CredentialUse, DecryptionFlags, EncryptionFlags, Error, ErrorKind, + InitializeSecurityContextResult, PackageCapabilities, PackageInfo, Result, SecurityBuffer, SecurityBufferType, + SecurityPackageType, SecurityStatus, Sspi, SspiEx, +}; + +pub const PKG_NAME: &str = "Pku2u"; + +pub const AZURE_AD_DOMAIN: &str = "AzureAD"; + +/// [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1) +const AUTHENTICATOR_DEFAULT_CHECKSUM: [u8; 24] = [ + 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 64, 0, 0, +]; + +/// Default NEGOEX authentication scheme +pub const DEFAULT_NEGOEX_AUTH_SCHEME: &str = "0d53335c-f9ea-4d0d-b2ec-4ae3786ec308"; + +/// sealed = true +/// other flags = false +pub const CLIENT_WRAP_TOKEN_FLAGS: u8 = 2; +/// sealed = true +/// send by acceptor = true +/// acceptor subkey = false +pub const SERVER_WRAP_TOKEN_FLAGS: u8 = 3; + +const DEFAULT_AP_REQ_OPTIONS: [u8; 4] = [0x20, 0x00, 0x00, 0x00]; + +lazy_static! { + pub static ref PACKAGE_INFO: PackageInfo = PackageInfo { + capabilities: PackageCapabilities::empty(), + rpc_id: PACKAGE_ID_NONE, + max_token_len: 0xbb80, // 48 000 bytes: default maximum token len in Windows + name: SecurityPackageType::Pku2u, + comment: String::from("Pku2u"), + }; +} + +#[derive(Debug, Clone)] +pub enum Pku2uState { + Negotiate, + Preauthentication, + AsExchange, + ApExchange, + PubKeyAuth, + Credentials, + Final, +} + +#[derive(Debug, Clone)] +enum Pku2uMode { + Client, + Server, +} + +#[derive(Debug, Clone)] +pub struct DhParameters { + // g + base: Vec, + // p + modulus: Vec, + // + q: Vec, + // generated private key + private_key: Vec, + // received public key + other_public_key: Option>, + client_nonce: Option<[u8; DH_NONCE_LEN]>, + server_nonce: Option<[u8; DH_NONCE_LEN]>, +} + +#[derive(Debug, Clone)] +pub struct Pku2u { + mode: Pku2uMode, + config: Pku2uConfig, + state: Pku2uState, + encryption_params: EncryptionParams, + auth_identity: Option, + conversation_id: Uuid, + auth_scheme: Option, + seq_number: u32, + dh_parameters: DhParameters, + // all sent and received NEGOEX messages concatenated in one vector + // we need it for the further checksum calculation + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-negoex/9de2cde2-bd98-40a4-9efa-0f5a1d6cc88e + // The checksum is performed on all previous NEGOEX messages in the context negotiation. + negoex_messages: Vec, + // two last GSS-API messages concatenated in one vector + // we need it for the further authenticator checksum calculation + // https://datatracker.ietf.org/doc/html/draft-zhu-pku2u-04#section-6 + // The checksum is performed on all previous NEGOEX messages in the context negotiation. + gss_api_messages: Vec, + negoex_random: [u8; RANDOM_ARRAY_SIZE], +} + +impl Pku2u { + pub fn new_server_from_config(config: Pku2uConfig) -> Result { + let mut rng = OsRng::default(); + + Ok(Self { + mode: Pku2uMode::Server, + config, + state: Pku2uState::Preauthentication, + encryption_params: EncryptionParams::default_for_server(), + auth_identity: None, + conversation_id: Uuid::default(), + auth_scheme: Some(Uuid::from_str(DEFAULT_NEGOEX_AUTH_SCHEME).unwrap()), + seq_number: 2, + // https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.3 + // Contains the nonce in the pkAuthenticator field in the request if the DH keys are NOT reused, + // 0 otherwise. + // generate dh parameters at the start in order to not waste time during authorization + dh_parameters: generate_server_dh_parameters(&mut rng)?, + negoex_messages: Vec::new(), + gss_api_messages: Vec::new(), + negoex_random: rng.gen::<[u8; RANDOM_ARRAY_SIZE]>(), + }) + } + + pub fn new_client_from_config(config: Pku2uConfig) -> Result { + let mut rng = OsRng::default(); + + Ok(Self { + mode: Pku2uMode::Client, + config, + state: Pku2uState::Negotiate, + encryption_params: EncryptionParams::default_for_client(), + auth_identity: None, + conversation_id: Uuid::new_v4(), + auth_scheme: None, + seq_number: 0, + // https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.3 + // Contains the nonce in the pkAuthenticator field in the request if the DH keys are NOT reused, + // 0 otherwise. + // generate dh parameters at the start in order to not waste time during authorization + dh_parameters: generate_client_dh_parameters(&mut rng)?, + negoex_messages: Vec::new(), + gss_api_messages: Vec::new(), + negoex_random: rng.gen::<[u8; RANDOM_ARRAY_SIZE]>(), + }) + } + + pub fn next_seq_number(&mut self) -> u32 { + let seq_num = self.seq_number; + self.seq_number += 1; + + seq_num + } +} + +impl Sspi for Pku2u { + fn complete_auth_token(&mut self, _token: &mut [SecurityBuffer]) -> Result { + Ok(SecurityStatus::Ok) + } + + fn encrypt_message( + &mut self, + _flags: EncryptionFlags, + message: &mut [SecurityBuffer], + sequence_number: u32, + ) -> Result { + // checks if the Token buffer present + let _ = SecurityBuffer::find_buffer(message, SecurityBufferType::Token)?; + let data = SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Data)?; + + let cipher = self + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher(); + + // the sub-session key is always preferred over the session key + let key = if let Some(key) = self.encryption_params.sub_session_key.as_ref() { + key + } else if let Some(key) = self.encryption_params.session_key.as_ref() { + key + } else { + return Err(Error::new( + ErrorKind::EncryptFailure, + "No encryption key provided".into(), + )); + }; + let key_usage = self.encryption_params.sspi_encrypt_key_usage; + + let mut wrap_token = WrapToken::with_seq_number(sequence_number as u64); + wrap_token.flags = match self.mode { + Pku2uMode::Client => CLIENT_WRAP_TOKEN_FLAGS, + Pku2uMode::Server => SERVER_WRAP_TOKEN_FLAGS, + }; + + let mut payload = data.buffer.to_vec(); + payload.extend_from_slice(&wrap_token.header()); + + let mut checksum = cipher.encrypt(key, key_usage, &payload)?; + checksum.rotate_right(RRC.into()); + + wrap_token.set_rrc(RRC); + wrap_token.set_checksum(checksum); + + let mut raw_wrap_token = Vec::with_capacity(92); + wrap_token.encode(&mut raw_wrap_token)?; + + match self.state { + Pku2uState::PubKeyAuth | Pku2uState::Credentials => { + if raw_wrap_token.len() < SECURITY_TRAILER { + return Err(Error::new(ErrorKind::EncryptFailure, "Cannot encrypt the data".into())); + } + + *data.buffer.as_mut() = raw_wrap_token[SECURITY_TRAILER..].to_vec(); + let header = SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Token)?; + *header.buffer.as_mut() = raw_wrap_token[0..SECURITY_TRAILER].to_vec(); + } + _ => { + return Err(Error { + error_type: ErrorKind::OutOfSequence, + description: "Kerberos context is not established or finished".to_owned(), + }) + } + }; + + Ok(SecurityStatus::Ok) + } + + fn decrypt_message(&mut self, message: &mut [SecurityBuffer], _sequence_number: u32) -> Result { + let mut encrypted = SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Token)? + .buffer + .clone(); + let data = SecurityBuffer::find_buffer_mut(message, SecurityBufferType::Data)?; + + encrypted.extend_from_slice(&data.buffer); + + let cipher = self + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher(); + + // the sub-session key is always preferred over the session key + let key = if let Some(key) = self.encryption_params.sub_session_key.as_ref() { + key + } else if let Some(key) = self.encryption_params.session_key.as_ref() { + key + } else { + return Err(Error::new( + ErrorKind::DecryptFailure, + "No encryption key provided".into(), + )); + }; + let key_usage = self.encryption_params.sspi_decrypt_key_usage; + + let mut wrap_token = WrapToken::decode(encrypted.as_slice())?; + wrap_token.checksum.rotate_left(RRC.into()); + + let mut decrypted = cipher.decrypt(key, key_usage, &wrap_token.checksum)?; + + // remove wrap token header + decrypted.truncate(decrypted.len() - WrapToken::header_len()); + + match self.state { + Pku2uState::PubKeyAuth => { + self.state = Pku2uState::Credentials; + + *data.buffer.as_mut() = decrypted; + Ok(DecryptionFlags::empty()) + } + Pku2uState::Credentials => { + self.state = Pku2uState::Final; + + *data.buffer.as_mut() = decrypted; + Ok(DecryptionFlags::empty()) + } + _ => { + *data.buffer.as_mut() = decrypted; + Ok(DecryptionFlags::empty()) + } + } + } + + fn query_context_sizes(&mut self) -> Result { + Ok(ContextSizes { + max_token: PACKAGE_INFO.max_token_len, + max_signature: MAX_SIGNATURE as u32, + block: 0, + security_trailer: SECURITY_TRAILER as u32, + }) + } + + fn query_context_names(&mut self) -> Result { + if let Some(ref identity_buffers) = self.auth_identity { + let identity: AuthIdentity = identity_buffers.clone().into(); + Ok(ContextNames { + username: identity.username, + domain: identity.domain, + }) + } else { + Err(sspi::Error::new( + sspi::ErrorKind::NoCredentials, + String::from("Requested Names, but no credentials were provided"), + )) + } + } + + fn query_context_package_info(&mut self) -> Result { + sspi::query_security_package_info(SecurityPackageType::Pku2u) + } + + fn query_context_cert_trust_status(&mut self) -> Result { + Err(Error::new( + ErrorKind::UnsupportedFunction, + "Certificate trust status is not supported".to_owned(), + )) + } + + fn change_password(&mut self, _change_password: ChangePassword) -> Result<()> { + Err(Error::new( + ErrorKind::UnsupportedFunction, + "change_password is not supported in PKU2U".into(), + )) + } +} + +impl SspiImpl for Pku2u { + type CredentialsHandle = Option; + + type AuthenticationData = AuthIdentity; + + fn acquire_credentials_handle_impl<'a>( + &'a mut self, + builder: crate::builders::FilledAcquireCredentialsHandle<'a, Self::CredentialsHandle, Self::AuthenticationData>, + ) -> Result> { + if builder.credential_use == CredentialUse::Outbound && builder.auth_data.is_none() { + return Err(Error::new( + ErrorKind::NoCredentials, + String::from("The client must specify the auth data"), + )); + } + + self.auth_identity = builder.auth_data.cloned().map(AuthIdentityBuffers::from); + + Ok(AcquireCredentialsHandleResult { + credentials_handle: self.auth_identity.clone(), + expiry: None, + }) + } + + fn initialize_security_context_impl<'a>( + &mut self, + builder: &mut crate::builders::FilledInitializeSecurityContext<'a, Self::CredentialsHandle>, + ) -> Result { + let status = match self.state { + Pku2uState::Negotiate => { + let auth_scheme = Uuid::from_str(DEFAULT_NEGOEX_AUTH_SCHEME).unwrap(); + + let mut mech_token = Vec::new(); + + let snames = check_if_empty!(builder.target_name, "service target name is not provided") + .split('/') + .collect(); + + let nego = Nego::new( + MessageType::InitiatorNego, + self.conversation_id, + self.next_seq_number(), + self.negoex_random, + vec![auth_scheme], + vec![], + ); + nego.encode(&mut mech_token)?; + + let exchange = Exchange::new( + MessageType::InitiatorMetaData, + self.conversation_id, + self.next_seq_number(), + auth_scheme, + picky_asn1_der::to_vec(&generate_pku2u_nego_req(snames, &self.config)?)?, + ); + exchange.encode(&mut mech_token)?; + + self.negoex_messages.extend_from_slice(&mech_token); + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, SecurityBufferType::Token)?; + output_token + .buffer + .write_all(&picky_asn1_der::to_vec(&generate_neg_token_init(mech_token)?)?)?; + + self.state = Pku2uState::Preauthentication; + + SecurityStatus::ContinueNeeded + } + Pku2uState::Preauthentication => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified".into()))?; + let input_token = SecurityBuffer::find_buffer(input, SecurityBufferType::Token)?; + + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; + let buffer = neg_token_targ + .0 + .response_token + .0 + .ok_or_else(|| { + Error::new(ErrorKind::InvalidToken, "Missing response_token in NegTokenTarg".into()) + })? + .0 + .0; + + self.negoex_messages.extend_from_slice(&buffer); + + let acceptor_nego = Nego::decode(&buffer)?; + + check_conversation_id!(acceptor_nego.header.conversation_id, self.conversation_id); + check_sequence_number!(acceptor_nego.header.sequence_num, self.next_seq_number()); + + // We support only one auth scheme. So the server must choose it otherwise it's an invalid behaviour + if let Some(auth_scheme) = acceptor_nego.auth_schemes.get(0) { + if *auth_scheme == Uuid::from_str(DEFAULT_NEGOEX_AUTH_SCHEME).unwrap() { + self.auth_scheme = Some(*auth_scheme); + } else { + return + Err(Error::new( + ErrorKind::InvalidToken, + format!( + "The server selected unsupported auth scheme {:?}. The only one supported auth scheme: {}", + auth_scheme, DEFAULT_NEGOEX_AUTH_SCHEME) + )); + } + } else { + return Err(Error::new( + ErrorKind::InvalidToken, + "Server didn't send any auth scheme".into(), + )); + } + + if buffer.len() < acceptor_nego.header.header_len as usize { + return Err(Error::new(ErrorKind::InvalidToken, "NEGOEX buffer is too short".into())); + } + + let acceptor_exchange_data = &buffer[(acceptor_nego.header.message_len as usize)..]; + let acceptor_exchange = Exchange::decode(acceptor_exchange_data)?; + + check_conversation_id!(acceptor_exchange.header.conversation_id, self.conversation_id); + check_sequence_number!(acceptor_exchange.header.sequence_num, self.next_seq_number()); + check_auth_scheme!(acceptor_exchange.auth_scheme, self.auth_scheme); + + let mut mech_token = Vec::new(); + + let snames = check_if_empty!(builder.target_name, "service target name is not provided") + .split('/') + .collect::>(); + + let kdc_req_body = generate_as_req_kdc_body(&GenerateAsReqOptions { + realm: WELLKNOWN_REALM, + username: &generate_as_req_username_from_certificate(&self.config.p2p_certificate)?, + cname_type: 0x80, + snames: &snames, + // we don't need the nonce in Pku2u + nonce: &[0], + })?; + let pa_datas = generate_pa_datas_for_as_req( + &self.config.p2p_certificate, + &kdc_req_body, + &self.dh_parameters, + &self.config.private_key, + )?; + + let exchange_data = + picky_asn1_der::to_vec(&generate_neg(generate_as_req(&pa_datas, kdc_req_body), AS_REQ_TOKEN_ID))?; + self.gss_api_messages.extend_from_slice(&exchange_data); + + let exchange = Exchange::new( + MessageType::ApRequest, + self.conversation_id, + self.next_seq_number(), + check_if_empty!(self.auth_scheme, "auth scheme is not set"), + exchange_data, + ); + exchange.encode(&mut mech_token)?; + + self.negoex_messages.extend_from_slice(&mech_token); + + let response_token = picky_asn1_der::to_vec(&generate_neg_token_targ(mech_token)?)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, SecurityBufferType::Token)?; + output_token.buffer.write_all(&response_token)?; + + self.state = Pku2uState::AsExchange; + + SecurityStatus::ContinueNeeded + } + Pku2uState::AsExchange => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified".into()))?; + let input_token = SecurityBuffer::find_buffer(input, SecurityBufferType::Token)?; + + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; + let buffer = neg_token_targ + .0 + .response_token + .0 + .ok_or_else(|| { + Error::new(ErrorKind::InvalidToken, "Missing response_token in NegTokenTarg".into()) + })? + .0 + .0; + + self.negoex_messages.extend_from_slice(&buffer); + + let acceptor_exchange = Exchange::decode(&buffer)?; + + check_conversation_id!(acceptor_exchange.header.conversation_id, self.conversation_id); + check_sequence_number!(acceptor_exchange.header.sequence_num, self.next_seq_number()); + check_auth_scheme!(acceptor_exchange.auth_scheme, self.auth_scheme); + + self.gss_api_messages.extend_from_slice(&acceptor_exchange.exchange); + + let (as_rep, _): (AsRep, _) = extract_krb_rep(&acceptor_exchange.exchange)?; + + let dh_rep_info = match extract_pa_pk_as_rep(&as_rep)? { + PaPkAsRep::DhInfo(dh) => dh.0, + PaPkAsRep::EncKeyPack(_) => { + return Err(Error::new( + ErrorKind::OperationNotSupported, + "encKeyPack is not supported for the PA-PK-AS-REP".into(), + )) + } + }; + + let server_nonce = extract_server_nonce(&dh_rep_info)?; + self.dh_parameters.server_nonce = Some(server_nonce); + + let signed_data: SignedData = picky_asn1_der::from_bytes(&dh_rep_info.dh_signed_data.0)?; + + let rsa_public_key = validate_server_p2p_certificate(&signed_data)?; + validate_signed_data(&signed_data, &rsa_public_key)?; + + let public_key = extract_server_dh_public_key(&signed_data)?; + self.dh_parameters.other_public_key = Some(public_key); + + self.encryption_params.encryption_type = + Some(CipherSuite::try_from(as_rep.0.enc_part.0.etype.0 .0.as_slice())?); + self.encryption_params.session_key = Some(generate_key( + check_if_empty!(self.dh_parameters.other_public_key.as_ref(), "dh public key is not set"), + &self.dh_parameters.private_key, + &self.dh_parameters.modulus, + Some(DhNonce { + client_nonce: check_if_empty!( + self.dh_parameters.client_nonce.as_ref(), + "dh client none is not set" + ), + server_nonce: check_if_empty!( + self.dh_parameters.server_nonce.as_ref(), + "dh server nonce is not set" + ), + }), + check_if_empty!( + self.encryption_params.encryption_type.as_ref(), + "encryption type is not set" + ) + .cipher() + .as_ref(), + )?); + + self.encryption_params.session_key = Some(extract_session_key_from_as_rep( + &as_rep, + check_if_empty!(self.encryption_params.session_key.as_ref(), "session key is not set"), + &self.encryption_params, + )?); + + let exchange_seq_number = self.next_seq_number(); + let verify_seq_number = self.next_seq_number(); + + let enc_type = self + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut OsRng::default()); + + let authenticator = generate_authenticator(GenerateAuthenticatorOptions { + kdc_rep: &as_rep.0, + seq_num: Some(exchange_seq_number), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_sub_key.clone(), + }), + checksum: Some(ChecksumOptions { + checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), + checksum_value: AUTHENTICATOR_DEFAULT_CHECKSUM.to_vec(), + }), + channel_bindings: None, + extensions: vec![generate_authenticator_extension( + &authenticator_sub_key, + &self.gss_api_messages, + )?], + })?; + let ap_req = generate_ap_req( + as_rep.0.ticket.0, + check_if_empty!(self.encryption_params.session_key.as_ref(), "session key is not set"), + &authenticator, + &self.encryption_params, + &DEFAULT_AP_REQ_OPTIONS, + )?; + + let mut mech_token = Vec::new(); + + let exchange = Exchange::new( + MessageType::ApRequest, + self.conversation_id, + exchange_seq_number, + check_if_empty!(self.auth_scheme, "auth_scheme is not set"), + picky_asn1_der::to_vec(&generate_neg(ap_req, AP_REQ_TOKEN_ID))?, + ); + exchange.encode(&mut mech_token)?; + + exchange.encode(&mut self.negoex_messages)?; + + let verify = Verify::new( + MessageType::Verify, + self.conversation_id, + verify_seq_number, + check_if_empty!(self.auth_scheme, "auth_scheme is not set"), + ChecksumSuite::HmacSha196Aes256.into(), + ChecksumSuite::HmacSha196Aes256.hasher().checksum( + &authenticator_sub_key, + INITIATOR_SIGN, + &self.negoex_messages, + )?, + ); + verify.encode(&mut mech_token)?; + + verify.encode(&mut self.negoex_messages)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, SecurityBufferType::Token)?; + output_token + .buffer + .write_all(&picky_asn1_der::to_vec(&generate_neg_token_targ(mech_token)?)?)?; + + self.state = Pku2uState::ApExchange; + + SecurityStatus::ContinueNeeded + } + Pku2uState::ApExchange => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified".into()))?; + let input_token = SecurityBuffer::find_buffer(input, SecurityBufferType::Token)?; + + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(&input_token.buffer)?; + + let buffer = neg_token_targ + .0 + .response_token + .0 + .ok_or_else(|| { + Error::new(ErrorKind::InvalidToken, "Missing response_token in NegTokenTarg".into()) + })? + .0 + .0; + + let acceptor_exchange = Exchange::decode(&buffer)?; + + check_conversation_id!(acceptor_exchange.header.conversation_id, self.conversation_id); + check_sequence_number!(acceptor_exchange.header.sequence_num, self.next_seq_number()); + check_auth_scheme!(acceptor_exchange.auth_scheme, self.auth_scheme); + + if buffer.len() < acceptor_exchange.header.header_len as usize { + return Err(Error::new(ErrorKind::InvalidToken, "NEGOEX buffer is too short".into())); + } + + self.negoex_messages + .extend_from_slice(&buffer[0..(acceptor_exchange.header.message_len as usize)]); + + let acceptor_verify_data = &buffer[(acceptor_exchange.header.message_len as usize)..]; + let acceptor_verify = Verify::decode(acceptor_verify_data)?; + + check_conversation_id!(acceptor_verify.header.conversation_id, self.conversation_id); + check_sequence_number!(acceptor_verify.header.sequence_num, self.next_seq_number()); + check_auth_scheme!(acceptor_verify.auth_scheme, self.auth_scheme); + + let (ap_rep, _): (ApRep, _) = extract_krb_rep(&acceptor_exchange.exchange)?; + + self.encryption_params.sub_session_key = Some(extract_sub_session_key_from_ap_rep( + &ap_rep, + check_if_empty!(self.encryption_params.session_key.as_ref(), "session key is not set"), + &self.encryption_params, + )?); + + let acceptor_checksum = ChecksumSuite::try_from(acceptor_verify.checksum.checksum_type as usize)? + .hasher() + .checksum( + check_if_empty!( + self.encryption_params.sub_session_key.as_ref(), + "sub session key is not set" + ), + ACCEPTOR_SIGN, + &self.negoex_messages, + )?; + if acceptor_verify.checksum.checksum_value != acceptor_checksum { + return Err(Error::new( + ErrorKind::MessageAltered, + "bad Verify message signature".into(), + )); + } + + self.state = Pku2uState::PubKeyAuth; + + SecurityStatus::Ok + } + _ => { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!("Got wrong PKU2U state: {:?}", self.state), + )) + } + }; + + Ok(InitializeSecurityContextResult { + status, + flags: ClientResponseFlags::empty(), + expiry: None, + }) + } + + fn accept_security_context_impl<'a>( + &'a mut self, + _builder: crate::builders::FilledAcceptSecurityContext<'a, Self::AuthenticationData, Self::CredentialsHandle>, + ) -> Result { + Err(Error::new( + ErrorKind::UnsupportedFunction, + "accept_security_context_impl is not implemented yet".into(), + )) + } +} + +impl SspiEx for Pku2u { + fn custom_set_auth_identity(&mut self, identity: Self::AuthenticationData) { + self.auth_identity = Some(identity.into()); + } +} diff --git a/src/sspi/pku2u/cert_utils/mod.rs b/src/sspi/pku2u/cert_utils/mod.rs new file mode 100644 index 00000000..41ee50b9 --- /dev/null +++ b/src/sspi/pku2u/cert_utils/mod.rs @@ -0,0 +1,6 @@ +pub mod validation; +#[cfg(target_os = "windows")] +pub mod win_extraction; + +#[cfg(target_os = "windows")] +pub use win_extraction as extraction; diff --git a/src/sspi/pku2u/cert_utils/validation.rs b/src/sspi/pku2u/cert_utils/validation.rs new file mode 100644 index 00000000..31321376 --- /dev/null +++ b/src/sspi/pku2u/cert_utils/validation.rs @@ -0,0 +1,45 @@ +use num_bigint_dig::BigUint; +use picky::key::PublicKey as RsaPublicKey; +use picky_asn1_x509::signed_data::{CertificateChoices, SignedData}; +use picky_asn1_x509::{Certificate, PublicKey}; + +use crate::{Error, ErrorKind, Result}; + +/// validates server's p2p certificate. +/// If certificate is valid then return its public key. +pub fn validate_server_p2p_certificate(signed_data: &SignedData) -> Result { + let certificates = &signed_data.certificates.0 .0; + + if let Some(certificate) = certificates.iter().next() { + let cert: Certificate = match certificate { + CertificateChoices::Certificate(cert) => picky_asn1_der::from_bytes(&cert.0)?, + _ => { + return Err(Error::new( + ErrorKind::CertificateUnknown, + "Received unknown certificate format".into(), + )) + } + }; + + let public_key = match cert.tbs_certificate.subject_public_key_info.subject_public_key { + PublicKey::Rsa(rsa) => rsa, + _ => { + return Err(Error::new( + ErrorKind::CertificateUnknown, + "Received certificate has unsupported public key type. Only RSA is supported.".into(), + )) + } + } + .0; + + return Ok(RsaPublicKey::from_rsa_components( + &BigUint::from_bytes_be(&public_key.modulus.0), + &BigUint::from_bytes_be(&public_key.public_exponent.0), + )); + } + + Err(Error::new( + ErrorKind::CertificateUnknown, + "Received invalid server certificates".into(), + )) +} diff --git a/src/sspi/pku2u/cert_utils/win_extraction.rs b/src/sspi/pku2u/cert_utils/win_extraction.rs new file mode 100644 index 00000000..93063630 --- /dev/null +++ b/src/sspi/pku2u/cert_utils/win_extraction.rs @@ -0,0 +1,340 @@ +use std::ffi::c_void; +use std::io::Read; +use std::ptr::{null, null_mut}; +use std::slice::from_raw_parts; + +use byteorder::{LittleEndian, ReadBytesExt}; +use num_bigint_dig::BigUint; +use picky::key::PrivateKey; +use picky_asn1_x509::{oids, AttributeTypeAndValueParameters, Certificate, ExtensionView}; +use winapi::shared::bcrypt::{BCRYPT_RSAFULLPRIVATE_BLOB, BCRYPT_RSAPUBLIC_MAGIC}; +use winapi::um::ncrypt::NCryptFreeObject; +use winapi::um::wincrypt::{ + CertCloseStore, CertEnumCertificatesInStore, CertFreeCertificateContext, CertOpenStore, + CryptAcquireCertificatePrivateKey, CERT_CONTEXT, CERT_STORE_PROV_SYSTEM_W, CERT_SYSTEM_STORE_CURRENT_USER_ID, + CERT_SYSTEM_STORE_LOCATION_SHIFT, CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG, HCRYPTPROV_OR_NCRYPT_KEY_HANDLE, +}; +use windows_sys::Win32::Foundation; +use windows_sys::Win32::Security::Cryptography::{NCryptExportKey, CERT_KEY_SPEC}; + +use crate::utils::string_to_utf16; +use crate::{Error, ErrorKind, Result}; + +/// [BCRYPT_RSAKEY_BLOB](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob) +/// ```not_rust +/// typedef struct _BCRYPT_RSAKEY_BLOB { +/// ULONG Magic; +/// ULONG BitLength; +/// ULONG cbPublicExp; +/// ULONG cbModulus; +/// ULONG cbPrime1; +/// ULONG cbPrime2; +/// } BCRYPT_RSAKEY_BLOB; +/// ``` +#[derive(Debug)] +struct BcryptRsaKeyBlob { + pub magic: u32, + pub bit_len: u32, + pub public_exp: u32, + pub modulus: u32, + pub prime1: u32, + pub prime2: u32, +} + +impl BcryptRsaKeyBlob { + pub fn from_read(mut data: impl Read) -> Result { + Ok(Self { + magic: data.read_u32::()?, + bit_len: data.read_u32::()?, + public_exp: data.read_u32::()?, + modulus: data.read_u32::()?, + prime1: data.read_u32::()?, + prime2: data.read_u32::()?, + }) + } +} + +fn decode_private_key(mut buffer: impl Read) -> Result { + let rsa_key_blob = BcryptRsaKeyBlob::from_read(&mut buffer)?; + + if rsa_key_blob.magic == BCRYPT_RSAPUBLIC_MAGIC { + return Err(Error::new( + ErrorKind::InternalError, + "Cannot extract certificate private key".into(), + )); + } + + let mut public_exp = vec![0; rsa_key_blob.public_exp as usize]; + buffer.read_exact(&mut public_exp)?; + + let mut modulus = vec![0; rsa_key_blob.modulus as usize]; + buffer.read_exact(&mut modulus)?; + + let mut prime1 = vec![0; rsa_key_blob.prime1 as usize]; + buffer.read_exact(&mut prime1)?; + + let mut prime2 = vec![0; rsa_key_blob.prime2 as usize]; + buffer.read_exact(&mut prime2)?; + + let mut exp = vec![0; rsa_key_blob.prime1 as usize]; + buffer.read_exact(&mut exp)?; + + let mut exp = vec![0; rsa_key_blob.prime2 as usize]; + buffer.read_exact(&mut exp)?; + + let mut coef = vec![0; rsa_key_blob.prime1 as usize]; + buffer.read_exact(&mut coef)?; + + let mut private_exp = vec![0; (rsa_key_blob.bit_len / 8) as usize]; + buffer.read_exact(&mut private_exp)?; + + let rsa_private_key = PrivateKey::from_rsa_components( + &BigUint::from_bytes_be(&modulus), + &BigUint::from_bytes_be(&public_exp), + &BigUint::from_bytes_be(&private_exp), + &vec![BigUint::from_bytes_be(&prime1), BigUint::from_bytes_be(&prime2)], + ) + .map_err(|err| { + Error::new( + ErrorKind::InternalError, + format!("Can not create a private from components: {:?}", err), + ) + })?; + + Ok(rsa_private_key) +} + +/// Validates the device certificate +/// Requirements for the device certificate: +/// 1. Issuer CN starts with 'MS-Organization-P2P-Access' +/// 2. ClientAuth extended key usage present +fn validate_client_p2p_certificate(certificate: &Certificate) -> bool { + let mut cn = false; + + for attr_type_and_value in certificate.tbs_certificate.issuer.0 .0.iter() { + for v in attr_type_and_value.0.iter() { + if v.ty.0 == oids::at_common_name() { + if let AttributeTypeAndValueParameters::CommonName(name) = &v.value { + if name.to_utf8_lossy().starts_with("MS-Organization-P2P-Access") { + cn = true; + } + } + } + } + } + + if !cn { + return false; + } + + let mut client_auth = false; + + for extension in &certificate.tbs_certificate.extensions.0 .0 { + if extension.extn_id().0 == oids::extended_key_usage() { + if let ExtensionView::ExtendedKeyUsage(ext_key_usage) = extension.extn_value() { + if ext_key_usage.contains(oids::kp_client_auth()) { + client_auth = true; + } + } + } + } + + client_auth +} + +unsafe fn export_certificate_private_key(cert: *const CERT_CONTEXT) -> Result { + let mut private_key_handle = HCRYPTPROV_OR_NCRYPT_KEY_HANDLE::default(); + let mut spec = CERT_KEY_SPEC::default(); + let mut free = Foundation::BOOL::default(); + + let status = CryptAcquireCertificatePrivateKey( + cert, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG, + null_mut(), + &mut private_key_handle, + &mut spec, + &mut free, + ); + + if status == 0 || private_key_handle == 0 { + return Err(Error::new( + ErrorKind::InternalError, + "Cannot extract certificate private key: invalid handle".into(), + )); + } + + let mut private_key_buffer_len = 0; + + let blob_type_wide = string_to_utf16(BCRYPT_RSAFULLPRIVATE_BLOB); + + // The first call need to determine the size of the needed buffer for the private key + // https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptexportkey + // If pbOutput parameter is NULL, this function will place the required size in the pcbResult parameter. + let status = NCryptExportKey( + private_key_handle as _, + 0, + blob_type_wide.as_ptr() as *const _, + null(), + null::() as *mut _, + 0, + &mut private_key_buffer_len, + 0, + ); + + if status != 0 { + NCryptFreeObject(private_key_handle); + + return Err(Error::new( + ErrorKind::InternalError, + format!( + "Cannot extract certificate private key: unsuccessful extraction: {:x?}", + status + ), + )); + } + + let mut private_key_blob = vec![0; private_key_buffer_len as usize]; + + let status = NCryptExportKey( + private_key_handle as _, + 0, + blob_type_wide.as_ptr() as *const _, + null(), + private_key_blob.as_mut_ptr(), + private_key_blob.len() as _, + &mut private_key_buffer_len, + 0, + ); + + NCryptFreeObject(private_key_handle); + + if status != 0 { + return Err(Error::new( + ErrorKind::InternalError, + format!( + "Cannot extract certificate private key: unsuccessful extraction: {:x?}", + status + ), + )); + } + + let private_key = decode_private_key(&private_key_blob[0..private_key_buffer_len as usize])?; + + Ok(private_key) +} + +unsafe fn extract_client_p2p_certificate(cert_store: *mut c_void) -> Result<(Certificate, PrivateKey)> { + let mut certificate = CertEnumCertificatesInStore(cert_store, null_mut()); + + while !certificate.is_null() { + let cert_der = from_raw_parts((*certificate).pbCertEncoded, (*certificate).cbCertEncoded as usize); + let cert: Certificate = picky_asn1_der::from_bytes(cert_der)?; + + if !validate_client_p2p_certificate(&cert) { + let next_certificate = CertEnumCertificatesInStore(cert_store, certificate); + + certificate = next_certificate; + + continue; + } + + let private_key = export_certificate_private_key(certificate); + + CertFreeCertificateContext(certificate); + + return Ok((cert, private_key?)); + } + + Err(Error::new( + ErrorKind::InternalError, + "Cannot find appropriate device certificate".into(), + )) +} + +// There is no specification/documentation that said where the P2P certificates should be installed. +// During dev testing, we notice that they always are in the Personal folder. +// So we assume that the needed certificates are placed in this folder +// It uses the "My" certificates store that has access to the Personal folder in order to extract those certificates. +pub fn extract_client_p2p_cert_and_key() -> Result<(Certificate, PrivateKey)> { + unsafe { + // "My\0" encoded as a wide string. + // More info: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopenstore#remarks + let my: [u16; 3] = [77, 121, 0]; + let cert_store = CertOpenStore( + CERT_STORE_PROV_SYSTEM_W, + 0, + 0, + CERT_SYSTEM_STORE_CURRENT_USER_ID << CERT_SYSTEM_STORE_LOCATION_SHIFT, + my.as_ptr() as *const _, + ); + + if cert_store.is_null() { + return Err(Error::new( + ErrorKind::InternalError, + "Cannot initialize certificate store: permission denied".into(), + )); + } + + let cert_and_key = extract_client_p2p_certificate(cert_store); + + CertCloseStore(cert_store, 0); + + cert_and_key + } +} + +#[cfg(test)] +mod tests { + use picky_asn1_x509::Certificate; + + use super::validate_client_p2p_certificate; + + #[test] + fn test_client_p2p_certificate_validation() { + let certificate: Certificate = picky_asn1_der::from_bytes(&[ + 48, 130, 3, 213, 48, 130, 2, 189, 160, 3, 2, 1, 2, 2, 16, 51, 247, 184, 98, 224, 162, 21, 50, 174, 177, + 189, 96, 58, 124, 107, 164, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 77, 49, 75, 48, + 73, 6, 3, 85, 4, 3, 30, 66, 0, 77, 0, 83, 0, 45, 0, 79, 0, 114, 0, 103, 0, 97, 0, 110, 0, 105, 0, 122, 0, + 97, 0, 116, 0, 105, 0, 111, 0, 110, 0, 45, 0, 80, 0, 50, 0, 80, 0, 45, 0, 65, 0, 99, 0, 99, 0, 101, 0, 115, + 0, 115, 0, 32, 0, 91, 0, 50, 0, 48, 0, 50, 0, 50, 0, 93, 48, 30, 23, 13, 50, 50, 49, 48, 50, 54, 49, 51, + 50, 51, 53, 56, 90, 23, 13, 50, 50, 49, 48, 50, 54, 49, 52, 50, 56, 53, 56, 90, 48, 129, 142, 49, 52, 48, + 50, 6, 10, 9, 146, 38, 137, 147, 242, 44, 100, 1, 25, 22, 36, 97, 57, 50, 53, 50, 52, 52, 56, 45, 57, 97, + 98, 55, 45, 52, 57, 98, 48, 45, 98, 98, 53, 99, 45, 102, 50, 102, 57, 50, 51, 99, 56, 52, 54, 55, 50, 49, + 61, 48, 59, 6, 3, 85, 4, 3, 12, 52, 83, 45, 49, 45, 49, 50, 45, 49, 45, 51, 54, 53, 51, 50, 49, 49, 48, 50, + 50, 45, 49, 51, 51, 57, 48, 48, 54, 52, 50, 50, 45, 50, 54, 50, 55, 53, 55, 51, 57, 48, 48, 45, 49, 53, 54, + 48, 55, 51, 52, 57, 49, 57, 49, 23, 48, 21, 6, 3, 85, 4, 3, 12, 14, 115, 55, 64, 100, 97, 116, 97, 97, 110, + 115, 46, 99, 111, 109, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, + 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 199, 60, 253, 49, 157, 172, 15, 185, 180, 104, 241, 218, 22, 185, + 120, 213, 135, 223, 222, 100, 75, 148, 218, 177, 71, 131, 140, 8, 195, 173, 7, 244, 41, 200, 45, 77, 173, + 68, 205, 213, 27, 72, 246, 147, 167, 184, 52, 81, 44, 28, 143, 238, 201, 186, 143, 111, 62, 224, 73, 86, + 69, 249, 239, 44, 79, 115, 37, 185, 243, 1, 23, 234, 116, 28, 244, 221, 99, 62, 177, 39, 128, 239, 115, 47, + 184, 135, 25, 43, 109, 246, 200, 11, 116, 38, 99, 167, 136, 48, 59, 187, 188, 40, 216, 85, 133, 246, 5, + 130, 177, 220, 6, 210, 34, 164, 15, 207, 125, 223, 42, 190, 77, 109, 69, 224, 132, 147, 115, 110, 39, 205, + 112, 140, 44, 215, 43, 252, 206, 89, 55, 161, 210, 166, 234, 223, 0, 198, 24, 70, 158, 56, 78, 23, 76, 249, + 86, 198, 95, 207, 53, 220, 75, 246, 91, 138, 99, 193, 186, 97, 57, 207, 115, 14, 1, 251, 111, 180, 121, 41, + 132, 254, 82, 109, 66, 202, 11, 20, 14, 31, 242, 55, 225, 112, 210, 220, 229, 155, 152, 202, 92, 54, 223, + 38, 153, 248, 173, 168, 180, 70, 146, 219, 186, 166, 251, 234, 149, 41, 18, 61, 227, 148, 13, 141, 229, 1, + 49, 212, 128, 67, 225, 120, 7, 122, 41, 102, 241, 223, 249, 198, 117, 89, 37, 177, 142, 85, 24, 136, 230, + 160, 136, 43, 89, 66, 41, 220, 85, 85, 2, 3, 1, 0, 1, 163, 111, 48, 109, 48, 14, 6, 3, 85, 29, 15, 1, 1, + 255, 4, 4, 3, 2, 5, 160, 48, 41, 6, 3, 85, 29, 17, 4, 34, 48, 32, 160, 30, 6, 10, 43, 6, 1, 4, 1, 130, 55, + 20, 2, 3, 160, 16, 12, 14, 115, 55, 64, 100, 97, 116, 97, 97, 110, 115, 46, 99, 111, 109, 48, 19, 6, 3, 85, + 29, 37, 4, 12, 48, 10, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2, 48, 27, 6, 9, 43, 6, 1, 4, 1, 130, 55, 21, 10, 4, 14, + 48, 12, 48, 10, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, + 130, 1, 1, 0, 71, 217, 65, 65, 121, 161, 60, 132, 114, 210, 31, 169, 34, 170, 87, 169, 50, 137, 52, 187, + 116, 98, 61, 8, 255, 89, 197, 131, 73, 33, 17, 136, 188, 42, 180, 22, 239, 101, 126, 28, 138, 35, 108, 101, + 138, 50, 54, 5, 105, 17, 85, 172, 239, 78, 21, 202, 246, 237, 51, 210, 17, 184, 39, 190, 135, 109, 73, 210, + 243, 138, 142, 72, 67, 206, 58, 129, 133, 215, 161, 103, 57, 97, 99, 131, 85, 45, 160, 129, 144, 5, 184, + 191, 7, 114, 24, 7, 237, 81, 246, 242, 94, 232, 161, 230, 108, 97, 184, 185, 182, 200, 178, 44, 7, 76, 10, + 47, 156, 88, 110, 198, 193, 125, 190, 84, 225, 93, 53, 87, 183, 14, 49, 118, 233, 217, 171, 139, 75, 131, + 8, 222, 241, 87, 3, 146, 243, 55, 69, 62, 204, 146, 92, 118, 241, 104, 209, 178, 228, 246, 199, 220, 104, + 32, 189, 125, 84, 82, 250, 215, 218, 10, 9, 21, 185, 251, 180, 51, 254, 67, 144, 78, 230, 201, 78, 127, 92, + 159, 26, 51, 223, 195, 192, 177, 251, 137, 234, 64, 37, 65, 76, 246, 118, 216, 224, 83, 152, 110, 67, 117, + 201, 2, 253, 173, 128, 73, 76, 26, 179, 93, 24, 227, 242, 121, 254, 170, 226, 31, 88, 196, 194, 58, 86, + 255, 192, 36, 221, 100, 20, 198, 221, 242, 249, 196, 211, 98, 111, 198, 220, 135, 239, 82, 74, 139, 243, 2, + 25, 215, + ]) + .unwrap(); + + assert!(validate_client_p2p_certificate(&certificate)); + } +} diff --git a/src/sspi/pku2u/config.rs b/src/sspi/pku2u/config.rs new file mode 100644 index 00000000..d6d2fb83 --- /dev/null +++ b/src/sspi/pku2u/config.rs @@ -0,0 +1,44 @@ +use picky::key::PrivateKey; +use picky_asn1_x509::Certificate; + +use crate::negotiate::{NegotiatedProtocol, ProtocolConfig}; +use crate::{Pku2u, Result}; + +#[derive(Debug, Clone)] +pub struct Pku2uConfig { + pub p2p_certificate: Certificate, + pub private_key: PrivateKey, +} + +impl Pku2uConfig { + pub fn new(p2p_certificate: Certificate, private_key: PrivateKey) -> Self { + Self { + p2p_certificate, + private_key, + } + } + + #[cfg(target_os = "windows")] + pub fn default_client_config() -> Result { + use super::cert_utils::extraction::extract_client_p2p_cert_and_key; + + let (p2p_certificate, private_key) = extract_client_p2p_cert_and_key()?; + + Ok(Self { + p2p_certificate, + private_key, + }) + } +} + +impl ProtocolConfig for Pku2uConfig { + fn new_client(&self) -> Result { + Ok(NegotiatedProtocol::Pku2u(Pku2u::new_client_from_config(Clone::clone( + self, + ))?)) + } + + fn clone(&self) -> Box { + Box::new(Clone::clone(self)) + } +} diff --git a/src/sspi/pku2u/extractors.rs b/src/sspi/pku2u/extractors.rs new file mode 100644 index 00000000..1af0e8ad --- /dev/null +++ b/src/sspi/pku2u/extractors.rs @@ -0,0 +1,119 @@ +use std::convert::{TryFrom, TryInto}; + +use oid::ObjectIdentifier; +use picky_asn1::wrapper::IntegerAsn1; +use picky_asn1_der::application_tag::ApplicationTag; +use picky_asn1_der::Asn1RawDer; +use picky_asn1_x509::content_info::ContentValue; +use picky_asn1_x509::oids::PKINIT_DH_KEY_DATA; +use picky_asn1_x509::signed_data::SignedData; +use picky_krb::constants::key_usages::AS_REP_ENC; +use picky_krb::constants::types::PA_PK_AS_REP; +use picky_krb::messages::{AsRep, EncAsRepPart}; +use picky_krb::pkinit::{DhRepInfo, KdcDhKeyInfo, PaPkAsRep}; +use serde::Deserialize; + +use super::generators::DH_NONCE_LEN; +use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE}; +use crate::{Error, ErrorKind, Result}; + +pub fn extract_krb_rep<'a, T: Deserialize<'a>>(mut data: &'a [u8]) -> Result<(T, &'a [u8])> { + let _oid: ApplicationTag = picky_asn1_der::from_reader(&mut data)?; + + Ok((picky_asn1_der::from_bytes(data)?, data)) +} + +pub fn extract_pa_pk_as_rep(as_rep: &AsRep) -> Result { + Ok(picky_asn1_der::from_bytes( + &as_rep + .0 + .padata + .0 + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "pa-datas is not present in as-rep".into()))? + .iter() + .find(|pa_data| pa_data.padata_type.0 .0 == PA_PK_AS_REP) + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidToken, + "PA_PK_AS_REP is not present in pa-datas of the as-rep".into(), + ) + })? + .padata_data + .0 + .0, + )?) +} + +pub fn extract_server_nonce(dh_rep_info: &DhRepInfo) -> Result<[u8; DH_NONCE_LEN]> { + let nonce = dh_rep_info + .server_dh_nonce + .0 + .as_ref() + .map(|nonce| nonce.0 .0.clone()) + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "DH server nonce is not present".into()))?; + + if nonce.len() != DH_NONCE_LEN { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid server dh nonce length: {}. Expected: {}", + nonce.len(), + DH_NONCE_LEN + ), + )); + } + + Ok(nonce.try_into().unwrap()) +} + +pub fn extract_server_dh_public_key(signed_data: &SignedData) -> Result> { + let pkinit_dh_key_data = ObjectIdentifier::try_from(PKINIT_DH_KEY_DATA).unwrap(); + if signed_data.content_info.content_type.0 != pkinit_dh_key_data { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "Invalid content info identifier: {:?}. Expected: {:?}", + signed_data.content_info.content_type.0, pkinit_dh_key_data + ), + )); + } + + let dh_key_info_data = match &signed_data + .content_info + .content + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "content info is not present".into()))? + .0 + { + ContentValue::OctetString(data) => &data.0, + _ => return Err(Error::new(ErrorKind::InvalidToken, "unexpected content info".into())), + }; + + let dh_key_info: KdcDhKeyInfo = picky_asn1_der::from_bytes(dh_key_info_data)?; + + if dh_key_info.nonce.0 != vec![0] { + return Err(Error::new( + ErrorKind::InvalidToken, + format!("DH key nonce must be 0. Got: {:?}", dh_key_info.nonce.0), + )); + } + + let key: IntegerAsn1 = picky_asn1_der::from_bytes(dh_key_info.subject_public_key.0.payload_view())?; + + Ok(key.as_unsigned_bytes_be().to_vec()) +} + +pub fn extract_session_key_from_as_rep(as_rep: &AsRep, key: &[u8], enc_params: &EncryptionParams) -> Result> { + let cipher = enc_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher(); + + let enc_data = cipher.decrypt(key, AS_REP_ENC, &as_rep.0.enc_part.0.cipher.0 .0)?; + + let enc_as_rep_part: EncAsRepPart = picky_asn1_der::from_bytes(&enc_data)?; + + Ok(enc_as_rep_part.0.key.0.key_value.0.to_vec()) +} diff --git a/src/sspi/pku2u/generators.rs b/src/sspi/pku2u/generators.rs new file mode 100644 index 00000000..67a832e4 --- /dev/null +++ b/src/sspi/pku2u/generators.rs @@ -0,0 +1,496 @@ +use std::fmt::Debug; +use std::str::FromStr; + +use chrono::Utc; +use picky::hash::HashAlgorithm; +use picky::key::PrivateKey; +use picky::signature::SignatureAlgorithm; +use picky_asn1::bit_string::BitString; +use picky_asn1::date::GeneralizedTime; +use picky_asn1::restricted_string::IA5String; +use picky_asn1::wrapper::{ + Asn1SequenceOf, Asn1SetOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, + ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7, + ExplicitContextTag8, ImplicitContextTag0, IntegerAsn1, ObjectIdentifierAsn1, OctetStringAsn1, Optional, +}; +use picky_asn1_der::application_tag::ApplicationTag; +use picky_asn1_der::Asn1RawDer; +use picky_asn1_x509::cmsversion::CmsVersion; +use picky_asn1_x509::content_info::EncapsulatedContentInfo; +use picky_asn1_x509::signed_data::{ + CertificateChoices, CertificateSet, DigestAlgorithmIdentifiers, SignedData, SignersInfos, +}; +use picky_asn1_x509::signer_info::{ + Attributes, CertificateSerialNumber, DigestAlgorithmIdentifier, IssuerAndSerialNumber, + SignatureAlgorithmIdentifier, SignatureValue, SignerIdentifier, SignerInfo, UnsignedAttributes, +}; +use picky_asn1_x509::{ + oids, AlgorithmIdentifier, Attribute, AttributeTypeAndValueParameters, AttributeValues, Certificate, ShaVariant, +}; +use picky_krb::constants::gss_api::{ACCEPT_INCOMPLETE, AUTHENTICATOR_CHECKSUM_TYPE}; +use picky_krb::constants::key_usages::KEY_USAGE_FINISHED; +use picky_krb::constants::types::{NT_SRV_INST, PA_PK_AS_REQ}; +use picky_krb::crypto::diffie_hellman::{compute_public_key, generate_private_key}; +use picky_krb::crypto::ChecksumSuite; +use picky_krb::data_types::{ + Authenticator, AuthenticatorInner, AuthorizationData, AuthorizationDataInner, Checksum, EncryptionKey, + KerbAdRestrictionEntry, KerberosStringAsn1, KerberosTime, LsapTokenInfoIntegrity, PaData, PrincipalName, Realm, +}; +use picky_krb::gss_api::{ + ApplicationTag0, GssApiNegInit, KrbMessage, MechType, MechTypeList, NegTokenInit, NegTokenTarg, +}; +use picky_krb::messages::KdcReqBody; +use picky_krb::negoex::RANDOM_ARRAY_SIZE; +use picky_krb::pkinit::{ + AuthPack, DhDomainParameters, DhReqInfo, DhReqKeyInfo, KrbFinished, PaPkAsReq, PkAuthenticator, Pku2uNegoBody, + Pku2uNegoReq, Pku2uNegoReqMetadata, +}; +use rand::rngs::OsRng; +use rand::Rng; +use sha1::{Digest, Sha1}; + +use super::{DhParameters, Pku2uConfig}; +use crate::crypto::compute_md5_channel_bindings_hash; +use crate::kerberos::client::generators::{ + AuthenticatorChecksumExtension, ChecksumOptions, EncKey, GenerateAuthenticatorOptions, MAX_MICROSECONDS_IN_SECOND, +}; +use crate::{Error, ErrorKind, Result, KERBEROS_VERSION}; + +/// [The PKU2U Realm Name](https://datatracker.ietf.org/doc/html/draft-zhu-pku2u-09#section-3) +/// The PKU2U realm name is defined as a reserved Kerberos realm name, and it has the value of "WELLKNOWN:PKU2U". +pub const WELLKNOWN_REALM: &str = "WELLKNOWN:PKU2U"; + +/// [Generation of Client Request](https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.1) +/// 9. This nonce string MUST be as long as the longest key length of the symmetric key types that the client supports. +/// Key length of Aes256 is equal to 32 +pub const DH_NONCE_LEN: usize = 32; + +/// [The GSS-API Binding for PKU2U](https://datatracker.ietf.org/doc/html/draft-zhu-pku2u-04#section-6) +/// The type for the checksum extension. +/// GSS_EXTS_FINISHED 2 +const GSS_EXTS_FINISHED: u32 = 2; + +/// [2.2.5 LSAP_TOKEN_INFO_INTEGRITY](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-KILE/%5bMS-KILE%5d.pdf) +/// indicating the token information type +/// 0x00000001 = User Account Control (UAC) restricted token +const LSAP_TOKEN_INFO_INTEGRITY_FLAG: u32 = 1; +/// [2.2.5 LSAP_TOKEN_INFO_INTEGRITY](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-KILE/%5bMS-KILE%5d.pdf) +/// indicating the integrity level of the calling process +/// 0x00002000 = Medium. +const LSAP_TOKEN_INFO_INTEGRITY_TOKEN_IL: u32 = 0x00002000; +/// [3.1.1.4 Machine ID](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-KILE/%5bMS-KILE%5d.pdf) +/// KILE implements a 32-byte binary random string machine ID. +const MACHINE_ID: [u8; 32] = [ + 92, 95, 64, 72, 191, 160, 228, 23, 98, 35, 78, 151, 207, 227, 96, 126, 97, 180, 15, 98, 127, 211, 90, 177, 119, + 132, 45, 113, 206, 90, 169, 124, +]; + +// returns supported authentication types +pub fn get_mech_list() -> MechTypeList { + MechTypeList::from(vec![MechType::from(oids::negoex()), MechType::from(oids::ntlm_ssp())]) +} + +pub fn generate_pku2u_nego_req(service_names: Vec<&str>, config: &Pku2uConfig) -> Result { + let mut snames = Vec::with_capacity(service_names.len()); + for sname in service_names { + snames.push(KerberosStringAsn1::from(IA5String::from_str(sname)?)); + } + + Ok(Pku2uNegoReq { + metadata: ExplicitContextTag0::from(Asn1SequenceOf::from(vec![Pku2uNegoReqMetadata { + inner: ImplicitContextTag0::from(OctetStringAsn1::from(picky_asn1_der::to_vec( + &config.p2p_certificate.tbs_certificate.issuer, + )?)), + }])), + body: ExplicitContextTag1::from(Pku2uNegoBody { + realm: ExplicitContextTag0::from(Realm::from(IA5String::from_str(WELLKNOWN_REALM)?)), + sname: ExplicitContextTag1::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(snames)), + }), + }), + }) +} + +pub fn generate_neg_token_init(mech_token: Vec) -> Result> { + Ok(ApplicationTag0(GssApiNegInit { + oid: ObjectIdentifierAsn1::from(oids::spnego()), + neg_token_init: ExplicitContextTag0::from(NegTokenInit { + mech_types: Optional::from(Some(ExplicitContextTag0::from(get_mech_list()))), + req_flags: Optional::from(None), + mech_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(mech_token)))), + mech_list_mic: Optional::from(None), + }), + })) +} + +pub fn generate_neg_token_targ(token: Vec) -> Result> { + Ok(ExplicitContextTag1::from(NegTokenTarg { + neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))), + supported_mech: Optional::from(None), + response_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(token)))), + mech_list_mic: Optional::from(None), + })) +} + +pub fn generate_signer_info(p2p_cert: &Certificate, digest: Vec, private_key: &PrivateKey) -> Result { + let signed_attributes = Asn1SetOf::from(vec![ + Attribute { + ty: ObjectIdentifierAsn1::from(oids::content_type()), + value: AttributeValues::ContentType(Asn1SetOf::from(vec![ObjectIdentifierAsn1::from( + oids::pkinit_auth_data(), + )])), + }, + Attribute { + ty: ObjectIdentifierAsn1::from(oids::message_digest()), + value: AttributeValues::MessageDigest(Asn1SetOf::from(vec![OctetStringAsn1::from(digest)])), + }, + ]); + + let encoded_signed_attributes = picky_asn1_der::to_vec(&signed_attributes)?; + + let mut sha1 = Sha1::new(); + sha1.update(&encoded_signed_attributes); + + let hashed_signed_attributes = sha1.finalize().to_vec(); + + let signature = SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) + .sign(&hashed_signed_attributes, private_key) + .map_err(|err| { + Error::new( + ErrorKind::InternalError, + format!("Cannot calculate signer info signature: {:?}", err), + ) + })?; + + Ok(SignerInfo { + version: CmsVersion::V1, + sid: SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { + issuer: p2p_cert.tbs_certificate.issuer.clone(), + serial_number: CertificateSerialNumber(p2p_cert.tbs_certificate.serial_number.clone()), + }), + digest_algorithm: DigestAlgorithmIdentifier(AlgorithmIdentifier::new_sha(ShaVariant::SHA1)), + signed_attrs: Optional::from(Attributes(Asn1SequenceOf::from(signed_attributes.0))), + signature_algorithm: SignatureAlgorithmIdentifier(AlgorithmIdentifier::new_rsa_encryption()), + signature: SignatureValue(OctetStringAsn1::from(signature)), + unsigned_attrs: Optional::from(UnsignedAttributes(Vec::new())), + }) +} + +/// returns (p, g, q) +pub fn get_default_parameters() -> (Vec, Vec, Vec) { + ( + vec![ + 0, 255, 255, 255, 255, 255, 255, 255, 255, 201, 15, 218, 162, 33, 104, 194, 52, 196, 198, 98, 139, 128, + 220, 28, 209, 41, 2, 78, 8, 138, 103, 204, 116, 2, 11, 190, 166, 59, 19, 155, 34, 81, 74, 8, 121, 142, 52, + 4, 221, 239, 149, 25, 179, 205, 58, 67, 27, 48, 43, 10, 109, 242, 95, 20, 55, 79, 225, 53, 109, 109, 81, + 194, 69, 228, 133, 181, 118, 98, 94, 126, 198, 244, 76, 66, 233, 166, 55, 237, 107, 11, 255, 92, 182, 244, + 6, 183, 237, 238, 56, 107, 251, 90, 137, 159, 165, 174, 159, 36, 17, 124, 75, 31, 230, 73, 40, 102, 81, + 236, 230, 83, 129, 255, 255, 255, 255, 255, 255, 255, 255, + ], + vec![2], + vec![ + 127, 255, 255, 255, 255, 255, 255, 255, 228, 135, 237, 81, 16, 180, 97, 26, 98, 99, 49, 69, 192, 110, 14, + 104, 148, 129, 39, 4, 69, 51, 230, 58, 1, 5, 223, 83, 29, 137, 205, 145, 40, 165, 4, 60, 199, 26, 2, 110, + 247, 202, 140, 217, 230, 157, 33, 141, 152, 21, 133, 54, 249, 47, 138, 27, 167, 240, 154, 182, 182, 168, + 225, 34, 242, 66, 218, 187, 49, 47, 63, 99, 122, 38, 33, 116, 211, 27, 246, 181, 133, 255, 174, 91, 122, 3, + 91, 246, 247, 28, 53, 253, 173, 68, 207, 210, 215, 79, 146, 8, 190, 37, 143, 243, 36, 148, 51, 40, 246, + 115, 41, 192, 255, 255, 255, 255, 255, 255, 255, 255, + ], + ) +} + +pub fn generate_server_dh_parameters(rng: &mut OsRng) -> Result { + Ok(DhParameters { + base: Vec::new(), + modulus: Vec::new(), + q: Vec::new(), + private_key: Vec::new(), + other_public_key: None, + server_nonce: Some(rng.gen::<[u8; RANDOM_ARRAY_SIZE]>()), + client_nonce: None, + }) +} + +pub fn generate_client_dh_parameters(rng: &mut OsRng) -> Result { + let (p, g, q) = get_default_parameters(); + + let private_key = generate_private_key(&q, rng); + + Ok(DhParameters { + base: g, + modulus: p, + q, + private_key, + other_public_key: None, + client_nonce: Some(rng.gen::<[u8; RANDOM_ARRAY_SIZE]>()), + server_nonce: None, + }) +} + +pub fn generate_pa_datas_for_as_req( + p2p_cert: &Certificate, + kdc_req_body: &KdcReqBody, + dh_parameters: &DhParameters, + private_key: &PrivateKey, +) -> Result> { + let current_date = Utc::now(); + let mut microseconds = current_date.timestamp_subsec_micros(); + if microseconds > MAX_MICROSECONDS_IN_SECOND { + microseconds = MAX_MICROSECONDS_IN_SECOND; + } + + // [Generation of Client Request](https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.1) + // paChecksum: Contains the SHA1 checksum, performed over KDC-REQ-BODY. + let encoded_kdc_req_body = picky_asn1_der::to_vec(&kdc_req_body)?; + + let mut sha1 = Sha1::new(); + sha1.update(&encoded_kdc_req_body); + + let kdc_req_body_sha1_hash = sha1.finalize().to_vec(); + + let public_value = compute_public_key(&dh_parameters.private_key, &dh_parameters.modulus, &dh_parameters.base); + + let auth_pack = AuthPack { + pk_authenticator: ExplicitContextTag0::from(PkAuthenticator { + cusec: ExplicitContextTag0::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())), + ctime: ExplicitContextTag1::from(KerberosTime::from(GeneralizedTime::from(current_date))), + // always 0 in Pku2u + nonce: ExplicitContextTag2::from(IntegerAsn1::from(vec![0])), + pa_checksum: Optional::from(Some(ExplicitContextTag3::from(OctetStringAsn1::from( + kdc_req_body_sha1_hash, + )))), + }), + client_public_value: Optional::from(Some(ExplicitContextTag1::from(DhReqInfo { + key_info: DhReqKeyInfo { + identifier: ObjectIdentifierAsn1::from(oids::diffie_hellman()), + key_info: DhDomainParameters { + p: IntegerAsn1::from(dh_parameters.modulus.clone()), + g: IntegerAsn1::from(dh_parameters.base.clone()), + q: IntegerAsn1::from(dh_parameters.q.clone()), + j: Optional::from(None), + validation_params: Optional::from(None), + }, + }, + key_value: BitStringAsn1::from(BitString::with_bytes(picky_asn1_der::to_vec(&IntegerAsn1::from( + public_value, + ))?)), + }))), + supported_cms_types: Optional::from(None), + client_dh_nonce: Optional::from( + dh_parameters + .client_nonce + .as_ref() + .map(|nonce| ExplicitContextTag3::from(OctetStringAsn1::from(nonce.to_vec()))), + ), + }; + + let encoded_auth_pack = picky_asn1_der::to_vec(&auth_pack)?; + + let mut sha1 = Sha1::new(); + sha1.update(&encoded_auth_pack); + + let digest = sha1.finalize().to_vec(); + + let signed_data = SignedData { + version: CmsVersion::V3, + digest_algorithms: DigestAlgorithmIdentifiers(Asn1SetOf::from(vec![AlgorithmIdentifier::new_sha1()])), + content_info: EncapsulatedContentInfo::new(oids::pkinit_auth_data(), Some(encoded_auth_pack)), + certificates: Optional::from(CertificateSet(vec![CertificateChoices::Certificate(Asn1RawDer( + picky_asn1_der::to_vec(p2p_cert)?, + ))])), + crls: None, + signers_infos: SignersInfos(Asn1SetOf::from(vec![generate_signer_info( + p2p_cert, + digest, + private_key, + )?])), + }; + + let pa_pk_as_req = PaPkAsReq { + signed_auth_pack: ImplicitContextTag0::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&signed_data)?)), + trusted_certifiers: Optional::from(None), + kdc_pk_id: Optional::from(None), + }; + + Ok(vec![PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_PK_AS_REQ.to_vec())), + padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&pa_pk_as_req)?)), + }]) +} + +pub fn generate_neg( + krb_msg: T, + krb5_token_id: [u8; 2], +) -> ApplicationTag, 0> { + ApplicationTag::from(KrbMessage { + krb5_oid: ObjectIdentifierAsn1::from(oids::gss_pku2u()), + krb5_token_id, + krb_msg, + }) +} + +pub fn generate_authenticator_extension(key: &[u8], payload: &[u8]) -> Result { + let hasher = ChecksumSuite::HmacSha196Aes256.hasher(); + + let krb_finished = KrbFinished { + gss_mic: ExplicitContextTag1::from(Checksum { + cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(vec![ChecksumSuite::HmacSha196Aes256.into()])), + checksum: ExplicitContextTag1::from(OctetStringAsn1::from(hasher.checksum( + key, + KEY_USAGE_FINISHED, + payload, + )?)), + }), + }; + + Ok(AuthenticatorChecksumExtension { + extension_type: GSS_EXTS_FINISHED, + extension_value: picky_asn1_der::to_vec(&krb_finished)?, + }) +} + +pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result { + let GenerateAuthenticatorOptions { + kdc_rep, + seq_num, + sub_key, + checksum, + channel_bindings, + extensions, + } = options; + + let current_date = Utc::now(); + let mut microseconds = current_date.timestamp_subsec_micros(); + if microseconds > MAX_MICROSECONDS_IN_SECOND { + microseconds = MAX_MICROSECONDS_IN_SECOND; + } + + let lsap_token = LsapTokenInfoIntegrity { + flags: LSAP_TOKEN_INFO_INTEGRITY_FLAG, + token_il: LSAP_TOKEN_INFO_INTEGRITY_TOKEN_IL, + machine_id: MACHINE_ID, + }; + + let mut encoded_lsap_token = Vec::with_capacity(40); + lsap_token.encode(&mut encoded_lsap_token)?; + + let restriction_entry = KerbAdRestrictionEntry { + restriction_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0])), + restriction: ExplicitContextTag1::from(OctetStringAsn1::from(encoded_lsap_token)), + }; + + let authorization_data = Optional::from(Some(ExplicitContextTag8::from(AuthorizationData::from(vec![ + AuthorizationDataInner { + ad_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0x01])), + ad_data: ExplicitContextTag1::from(OctetStringAsn1::from(picky_asn1_der::to_vec(&Asn1SequenceOf::from( + vec![AuthorizationDataInner { + ad_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0x00, 0x8d])), + ad_data: ExplicitContextTag1::from(OctetStringAsn1::from(picky_asn1_der::to_vec( + &Asn1SequenceOf::from(vec![restriction_entry]), + )?)), + }], + ))?)), + }, + ])))); + + let cksum = if let Some(ChecksumOptions { + checksum_type, + mut checksum_value, + }) = checksum + { + if checksum_type == AUTHENTICATOR_CHECKSUM_TYPE && channel_bindings.is_some() { + if checksum_value.len() < 20 { + return Err(Error::new( + ErrorKind::InternalError, + format!( + "Invalid authenticator checksum length: expected >= 20 but got {}. ", + checksum_value.len() + ), + )); + } + // [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1) + // 4..19 - Channel binding information (19 inclusive). + checksum_value[4..20] + .copy_from_slice(&compute_md5_channel_bindings_hash(channel_bindings.as_ref().unwrap())); + } + + for extension in extensions { + checksum_value.extend_from_slice(&extension.extension_type.to_be_bytes()); + checksum_value.extend_from_slice(&(extension.extension_value.len() as u32).to_be_bytes()); + checksum_value.extend_from_slice(&extension.extension_value); + } + + Optional::from(Some(ExplicitContextTag3::from(Checksum { + cksumtype: ExplicitContextTag0::from(IntegerAsn1::from(checksum_type)), + checksum: ExplicitContextTag1::from(OctetStringAsn1::from(checksum_value)), + }))) + } else { + Optional::from(None) + }; + + Ok(Authenticator::from(AuthenticatorInner { + authenticator_bno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + crealm: ExplicitContextTag1::from(kdc_rep.crealm.0.clone()), + cname: ExplicitContextTag2::from(kdc_rep.cname.0.clone()), + cksum, + cusec: ExplicitContextTag4::from(IntegerAsn1::from(microseconds.to_be_bytes().to_vec())), + ctime: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(current_date))), + subkey: Optional::from(sub_key.map(|EncKey { key_type, key_value }| { + ExplicitContextTag6::from(EncryptionKey { + key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![key_type.into()])), + key_value: ExplicitContextTag1::from(OctetStringAsn1::from(key_value)), + }) + })), + seq_number: Optional::from(seq_num.map(|seq_num| { + ExplicitContextTag7::from(IntegerAsn1::from_bytes_be_unsigned(seq_num.to_be_bytes().to_vec())) + })), + authorization_data, + })) +} + +pub fn generate_as_req_username_from_certificate(certificate: &Certificate) -> Result { + let mut username = "AzureAD\\".to_owned(); + + let mut issuer = false; + for attr_type_and_value in certificate.tbs_certificate.issuer.0 .0.iter() { + for v in attr_type_and_value.0.iter() { + if v.ty.0 == oids::at_common_name() { + if let AttributeTypeAndValueParameters::CommonName(name) = &v.value { + issuer = true; + username.push_str(&name.to_utf8_lossy()); + } + } + } + } + + if !issuer { + return Err(Error::new( + ErrorKind::InternalError, + "Bad client certificate: cannot find common name of the issuer".into(), + )); + } + + username.push('\\'); + + let mut subject = false; + for attr_type_and_value in certificate.tbs_certificate.subject.0 .0.iter() { + for v in attr_type_and_value.0.iter() { + if v.ty.0 == oids::at_common_name() { + if let AttributeTypeAndValueParameters::CommonName(name) = &v.value { + subject = true; + username.push_str(&name.to_utf8_lossy()); + } + } + } + } + + if !subject { + return Err(Error::new( + ErrorKind::InternalError, + "Bad client certificate: cannot find appropriate common name of the subject".into(), + )); + } + + Ok(username) +} diff --git a/src/sspi/pku2u/macros.rs b/src/sspi/pku2u/macros.rs new file mode 100644 index 00000000..a5a9f9ce --- /dev/null +++ b/src/sspi/pku2u/macros.rs @@ -0,0 +1,52 @@ +macro_rules! check_conversation_id { + ($actual:expr, $expected:expr) => { + if $actual != $expected { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "Server sent invalid conversation id. Got {:?} but expected {:?}.", + $actual, $expected + ), + )); + } + }; +} + +macro_rules! check_auth_scheme { + ($actual:expr, $expected:expr) => { + if $expected.is_none() { + return Err(Error::new(ErrorKind::InternalError, "auth scheme id is not set".into())); + } + + if $actual != $expected.unwrap() { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "Server sent invalid conversation id. Got {:?} but expected {:?}.", + $actual, + $expected.unwrap() + ), + )); + } + }; +} + +macro_rules! check_sequence_number { + ($actual:expr, $expected:expr) => { + if $actual != $expected { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!( + "Server sent invalid sequence number. Got {:?} but expected {:?}.", + $actual, $expected + ), + )); + } + }; +} + +macro_rules! check_if_empty { + ($actual:expr, $msg:expr) => { + $actual.ok_or_else(|| Error::new(ErrorKind::InternalError, $msg.into()))? + }; +} diff --git a/src/sspi/pku2u/validate.rs b/src/sspi/pku2u/validate.rs new file mode 100644 index 00000000..a9f2a878 --- /dev/null +++ b/src/sspi/pku2u/validate.rs @@ -0,0 +1,27 @@ +use picky::hash::HashAlgorithm; +use picky::key::PublicKey as RsaPublicKey; +use picky::signature::SignatureAlgorithm; +use picky_asn1::wrapper::Asn1SetOf; +use picky_asn1_x509::signed_data::SignedData; +use sha1::{Digest, Sha1}; + +use crate::{Error, ErrorKind, Result}; + +pub fn validate_signed_data(signed_data: &SignedData, rsa_public_key: &RsaPublicKey) -> Result<()> { + let signer_info = signed_data + .signers_infos + .0 + .0 + .get(0) + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Missing signers_infos in signed data".into()))?; + + let signed_attributes = Asn1SetOf::from(signer_info.signed_attrs.0 .0 .0.clone()); + + let mut sha1 = Sha1::new(); + sha1.update(&picky_asn1_der::to_vec(&signed_attributes)?); + let hashed_signed_attributes = sha1.finalize().to_vec(); + + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) + .verify(rsa_public_key, &hashed_signed_attributes, &signer_info.signature.0 .0) + .map_err(|_| Error::new(ErrorKind::InvalidToken, "Invalid signed data signature".into())) +} diff --git a/src/utils.rs b/src/utils.rs index 176736e9..3afe5c23 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,9 @@ use byteorder::{LittleEndian, ReadBytesExt}; +use picky_krb::crypto::CipherSuite; +use rand::rngs::OsRng; +use rand::Rng; + +use crate::sspi::pku2u::AZURE_AD_DOMAIN; pub fn string_to_utf16(value: &str) -> Vec { value @@ -15,3 +20,28 @@ pub fn bytes_to_utf16_string(mut value: &[u8]) -> String { String::from_utf16_lossy(value_u16.as_ref()) } + +pub fn is_azure_ad_domain(domain: &str) -> bool { + domain == AZURE_AD_DOMAIN +} + +pub fn utf16_bytes_to_utf8_string(data: &[u8]) -> String { + debug_assert_eq!(data.len() % 2, 0); + String::from_utf16_lossy( + &data + .chunks(2) + .map(|c| u16::from_le_bytes(c.try_into().unwrap())) + .collect::>(), + ) +} + +pub fn generate_random_symmetric_key(cipher: &CipherSuite, rnd: &mut OsRng) -> Vec { + let key_size = cipher.cipher().key_size(); + let mut key = Vec::with_capacity(key_size); + + for _ in 0..key_size { + key.push(rnd.gen()); + } + + key +}