From 7ec5e1ad90ca9b8995513bfd8e4c53907e0f11a4 Mon Sep 17 00:00:00 2001 From: restitux Date: Fri, 27 Jun 2025 00:34:46 -0600 Subject: [PATCH] More progress and cleanup --- gamestream-webtransport-proxy/src/pair.rs | 276 ++++++++++++++++------ 1 file changed, 205 insertions(+), 71 deletions(-) diff --git a/gamestream-webtransport-proxy/src/pair.rs b/gamestream-webtransport-proxy/src/pair.rs index 2ef1b28..93ed17d 100644 --- a/gamestream-webtransport-proxy/src/pair.rs +++ b/gamestream-webtransport-proxy/src/pair.rs @@ -1,19 +1,16 @@ -use axum::Json; use axum::extract::Path; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use openssl::hash::MessageDigest; use openssl::sha::Sha256; -use openssl::symm::Cipher; -use rand::Rng; -use reqwest::Identity; use serde::{Deserialize, Serialize}; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; -use openssl::x509::X509; +use openssl::x509::{self, X509}; use anyhow::Result; +use url_constructor::UrlConstructor; #[derive(Debug, Deserialize)] struct ServerCertResponse { @@ -32,63 +29,95 @@ pub struct ServerChallengeResponseResponse { pairingsecret: String, } +#[derive(Debug)] +struct ServerCert { + cert: Vec, +} + +#[derive(Debug)] +pub struct ServerPairingSecret { + pairing_secret: Vec, + signature: Vec, +} + fn get_cert_and_private_key() -> Result<(X509, PKey)> { let rsa = Rsa::generate(2048)?; let key_pair = PKey::from_rsa(rsa)?; + let mut cert_builder = X509::builder()?; + + let serial_number_u32 = openssl::bn::BigNum::from_u32(0)?; + let serial_number = openssl::asn1::Asn1Integer::from_bn(&serial_number_u32)?; + + let now_unix = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + - 1000000; + let now = openssl::asn1::Asn1Time::from_unix(now_unix as i64)?; + let ten_years_from_now = openssl::asn1::Asn1Time::days_from_now(365 * 10)?; + + let mut x509_name_builder = openssl::x509::X509NameBuilder::new()?; + x509_name_builder.append_entry_by_text("CN", "NVIDIA GameStream Client")?; + let x509_name = x509_name_builder.build(); + cert_builder.set_version(2)?; + cert_builder.set_not_before(&now)?; + cert_builder.set_not_after(&ten_years_from_now)?; cert_builder.set_pubkey(&key_pair)?; + cert_builder.set_serial_number(&serial_number)?; + cert_builder.set_issuer_name(&x509_name)?; + cert_builder.set_subject_name(&x509_name)?; cert_builder.sign(&key_pair, MessageDigest::sha256())?; let cert = cert_builder.build(); Ok((cert, key_pair)) } +async fn get_url(base_url: &mut url_constructor::UrlConstructor) -> Result { + let mut uuidv2 = [0u8; 16]; + openssl::rand::rand_bytes(&mut uuidv2)?; + let uuidv2_hex = hex::encode(uuidv2); + + let url = base_url.param("uuid", uuidv2_hex).build(); + println!("Getting url: {url}"); + + let mut http_builder = reqwest::Client::builder(); + http_builder = http_builder.user_agent("Mozilla/5.0"); + let client = http_builder.build().unwrap(); + + let resp = client.get(url).send().await?; + let text = resp.text().await?; + Ok(text) +} + async fn get_server_cert( mut base_url: url_constructor::UrlConstructor, salt_hex: String, cert_hex: String, -) -> Result { +) -> Result { // Generate pairing url let url = base_url .param("phrase", "getservercert") .param("salt", &salt_hex) - .param("clientcert", &cert_hex) - .build(); - //let url = format!( - // "http://{}/pair?uniqueid={}&uuid={}&devicename=roth&updateState=1&phrase=getservercert&salt={}&clientcert={}", - // host, "0123456789ABCDEF", "23060a17e4fc40f5a83fee298995dd18", salt_hex, cert_hex, - //); - println!("url: {url}"); + .param("clientcert", &cert_hex); - //let mut identity_data = private_key.private_key_to_pem_pkcs8().unwrap(); - //identity_data.extend_from_slice(&cert_pem); + let text = get_url(url).await?; + let server_cert: ServerCertResponse = serde_xml_rs::from_str(&text)?; - //let identity = Identity::from_pem(&identity_data).unwrap(); - //let identity = - // Identity::from_pkcs8_pem(&cert_pem, &private_key.private_key_to_pem_pkcs8().unwrap()) - // .unwrap(); + let server_cert_bytes = hex::decode(server_cert.plaincert)?; - let mut http_builder = reqwest::Client::builder(); - http_builder = http_builder.user_agent("gamestream-webtransport-proxy"); - //http_builder = http_builder.identity(identity); - let client = http_builder.build().unwrap(); - - let resp = client.get(url).send().await?; - let text = resp.text().await.unwrap(); - let server_response: ServerCertResponse = serde_xml_rs::from_str(&text).unwrap(); - println!("{server_response:?}"); - Ok(server_response) + Ok(ServerCert { + cert: server_cert_bytes, + }) } async fn get_server_challenge( mut base_url: url_constructor::UrlConstructor, aes_key: &[u8], ) -> Result { - // 3. Generate random challenge let mut challenge_data = [0u8; 16]; openssl::rand::rand_bytes(&mut challenge_data)?; - // 4. Encrypt challenge let mut cipher_ctx = openssl::cipher_ctx::CipherCtx::new()?; cipher_ctx.encrypt_init( Some(openssl::cipher::Cipher::aes_128_ecb()), @@ -101,23 +130,17 @@ async fn get_server_challenge( cipher_ctx.cipher_update_vec(&challenge_data, &mut challenge_enc)?; cipher_ctx.cipher_final(&mut challenge_enc)?; - // 5. Convert to hex let challenge_hex = hex::encode(challenge_enc); - let url = base_url.param("clientchallenge", challenge_hex).build(); - let mut http_builder = reqwest::Client::builder(); - http_builder = http_builder.user_agent("gamestream-webtransport-proxy"); - //http_builder = http_builder.identity(identity); - let client = http_builder.build().unwrap(); - - let resp = client.get(url).send().await?; - let text = resp.text().await?; + let url = base_url.param("clientchallenge", challenge_hex); + let text = get_url(url).await?; Ok(serde_xml_rs::from_str(&text)?) } fn generate_challenge_response( client_challenge_response: String, + client_secret_data: &[u8; 16], aes_key: &[u8], cert: &X509, ) -> Result { @@ -145,9 +168,6 @@ fn generate_challenge_response( )?; cipher_ctx.cipher_final(&mut client_challenge_response_data)?; - let mut client_secret_data = [0u8; 16]; - openssl::rand::rand_bytes(&mut client_secret_data)?; - // Extract ASN.1 signature from certificate let asn_signature = cert.signature(); let signature_data = asn_signature.as_slice(); @@ -157,7 +177,7 @@ fn generate_challenge_response( Vec::with_capacity(16 + signature_data.len() + client_secret_data.len()); challenge_response.extend_from_slice(&client_challenge_response_data[32..32 + 16]); challenge_response.extend_from_slice(signature_data); - challenge_response.extend_from_slice(&client_secret_data); + challenge_response.extend_from_slice(client_secret_data); let mut hasher = Sha256::new(); hasher.update(&challenge_response); @@ -184,18 +204,9 @@ async fn send_server_challenge_response( mut base_url: url_constructor::UrlConstructor, server_challenge_response: String, ) -> Result { - let url = base_url - .param("serverchallengeresp", server_challenge_response) - .build(); + let url = base_url.param("serverchallengeresp", server_challenge_response); - println!("url: {url}"); - let mut http_builder = reqwest::Client::builder(); - http_builder = http_builder.user_agent("gamestream-webtransport-proxy"); - //http_builder = http_builder.identity(identity); - let client = http_builder.build().unwrap(); - - let resp = client.get(url).send().await?; - let text = resp.text().await?; + let text = get_url(url).await?; Ok(serde_xml_rs::from_str(&text)?) } @@ -211,31 +222,38 @@ async fn generate_aes_key(salt: [u8; 16], pin: [u8; 4]) -> Vec { async fn do_challenge( base_url: url_constructor::UrlConstructor, + client_secret_data: &[u8; 16], pin: [u8; 4], salt: [u8; 16], cert: &X509, -) -> Result<()> { +) -> Result { let aes_key = generate_aes_key(salt, pin).await; let client_challenge_response = get_server_challenge(base_url.clone(), &aes_key).await?; println!("{client_challenge_response:?}"); - let challenge_response = - generate_challenge_response(client_challenge_response.challengeresponse, &aes_key, cert)?; - println!("{challenge_response:?}"); + let challenge_response = generate_challenge_response( + client_challenge_response.challengeresponse, + client_secret_data, + &aes_key, + cert, + )?; let server_challenge_response_response = send_server_challenge_response(base_url, challenge_response).await?; - println!("{server_challenge_response_response:?}"); + let pairing_secret_bytes = hex::decode(server_challenge_response_response.pairingsecret)?; - Ok(()) + Ok(ServerPairingSecret { + pairing_secret: pairing_secret_bytes[0..16].into(), + signature: pairing_secret_bytes[16..].into(), + }) } -// Do a pairing operation -pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response { - let unique_id = "0123456789ABCDEF"; - let uuidv2 = "23060a17e4fc40f5a83fee298995dd18"; - +pub async fn get_base_url( + host: String, + port: u16, + unique_id: String, +) -> url_constructor::UrlConstructor { let mut base_url = url_constructor::UrlConstructor::new(); base_url .scheme("http") @@ -243,14 +261,17 @@ pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response { .port(port) .subdir("pair") .param("uniqueid", unique_id) - .param("uuid", uuidv2) - .param("devicename", "roth") + .param("devicename", "roth") // TODO: what is this roth thing? .param("updateState", "1"); + base_url +} +pub async fn generate_pin() -> [u8; 4] { let mut pin = [0u8; 4]; { print!("pairing pin: "); + // TODO: reenable real RNG let mut rng = rand::rng(); for i in 0..pin.len() { pin[i] = 5; @@ -261,6 +282,83 @@ pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response { // Print as a four-digit, zero-padded integer println!(""); } + pin +} + +pub async fn verify_signature(secret: Vec, signature: Vec, cert: Vec) -> Result<()> { + let cert_x509 = openssl::x509::X509::from_pem(&cert)?; + let cert_pubkey = cert_x509.public_key()?; + + let md = openssl::md::Md::sha256(); + let mut mdctx = openssl::md_ctx::MdCtx::new()?; + + mdctx.digest_verify_init(Some(md), &cert_pubkey)?; + mdctx.digest_verify_update(&secret)?; + match mdctx.digest_verify_final(&signature)? { + true => Ok(()), + false => Err(anyhow::anyhow!( + "Signature failed to verify. Are we being MITMed?" + )), + } +} + +pub async fn create_signature(data: &[u8; 16], private_key: &PKey) -> Result> { + let mut signature = Vec::new(); + + let md = openssl::md::Md::sha256(); + let mut mdctx = openssl::md_ctx::MdCtx::new()?; + + mdctx.digest_sign_init(Some(md), private_key)?; + mdctx.digest_sign_update(data)?; + + mdctx.digest_sign_final_to_vec(&mut signature)?; + + Ok(signature) +} + +pub async fn send_client_pairing_secret( + mut base_url: url_constructor::UrlConstructor, + client_secret_data: &[u8; 16], + private_key: &PKey, +) -> Result<()> { + let signature = create_signature(client_secret_data, private_key).await?; + + let mut client_secret = Vec::with_capacity(client_secret_data.len() + signature.len()); + client_secret.extend_from_slice(client_secret_data); + client_secret.extend_from_slice(&signature); + + let client_secret_hex = hex::encode(&client_secret); + + let url = base_url.param("clientpairingsecret", client_secret_hex); + + let text = get_url(url).await?; + println!("{text:?}"); + + Ok(()) +} + +async fn get_unique_id() -> Result { + let mut bytes = [0u8; 8]; + openssl::rand::rand_bytes(&mut bytes)?; + Ok(hex::encode(bytes)) +} + +// Do a pairing operation +pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response { + //let unique_id = "0123456789ABCDEF".to_string(); + //let uuidv2 = "23060a17e4fc40f5a83fee298995dd18".to_string(); + + let unique_id = get_unique_id().await.unwrap(); + + let base_url = get_base_url(host, port, unique_id).await; + + let pin = generate_pin().await; + + let mut client_secret_data = [0u8; 16]; + if let Err(e) = openssl::rand::rand_bytes(&mut client_secret_data) { + println!("Could not generate client secret data: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } // Generate certificates let (cert, private_key) = match get_cert_and_private_key() { @@ -280,10 +378,46 @@ pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response { openssl::rand::rand_bytes(&mut salt).unwrap(); let salt_hex = hex::encode(salt); - let server_cert_response = get_server_cert(base_url.clone(), salt_hex, cert_hex).await; + // Get the server certificate and start the pairing process + // This returns once the user has submitted the pin to the + // server out of band. + let server_cert = match get_server_cert(base_url.clone(), salt_hex, cert_hex).await { + Ok(s) => s, + Err(e) => { + println!("could not get server cert: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; + println!("{server_cert:?}"); - if let Err(e) = do_challenge(base_url.clone(), pin, salt, &cert).await { - println!("could not do challenge: {e}"); + // Do the challenge response process + // This returns the pairing secret + let server_pairing_secret = + match do_challenge(base_url.clone(), &client_secret_data, pin, salt, &cert).await { + Ok(s) => s, + Err(e) => { + println!("could not do challenge: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; + println!("{server_pairing_secret:?}"); + + // Verify the pairing_secret signature + if let Err(e) = verify_signature( + server_pairing_secret.pairing_secret, + server_pairing_secret.signature, + server_cert.cert, + ) + .await + { + println!("Could not verify signature: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + if let Err(e) = + send_client_pairing_secret(base_url.clone(), &client_secret_data, &private_key).await + { + println!("Could not send client pairing secret: {e}"); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); };