From 4864fb83a8605c136b7123561c93b57bf7f85faa Mon Sep 17 00:00:00 2001 From: Maple Date: Mon, 27 Apr 2026 14:16:13 +0200 Subject: [PATCH] add certificate verification --- Cargo.lock | 213 +++++++++++++++++++++++++++++++++++- Cargo.toml | 7 ++ grpc-protobufs | 2 +- src/account/account.rs | 238 ++++++++++++++++++++++++++++++----------- 4 files changed, 390 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7607ea..c3b6723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,21 +10,27 @@ dependencies = [ "argon2", "base64", "bcrypt", + "binrw", "bytemuck", "cbc", "chrono", "crc32fast", "dotenvy", + "dsa", + "ecdsa", "gxhash", "hex", "hmac", "juniper", "juniper_rocket", + "k256", "lettre", "log", "md-5", "mii", "once_cell", + "openssl", + "p256", "prost", "quick-xml", "rand 0.8.5", @@ -35,6 +41,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sha256", "sqlx", "thiserror", "tokio", @@ -280,6 +287,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "async-stream" version = "0.3.6" @@ -423,6 +436,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -454,6 +473,30 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "binrw" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53195f985e88ab94d1cc87e80049dd2929fd39e4a772c5ae96a7e5c4aad3642" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5910da05ee556b789032c8ff5a61fb99239580aa3fd0bfaa8f4d094b2aee00ad" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bitflags" version = "2.8.0" @@ -696,6 +739,18 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -830,6 +885,36 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.13.0" @@ -839,6 +924,26 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.4.1" @@ -908,6 +1013,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "figment" version = "0.10.19" @@ -1108,6 +1223,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1145,6 +1261,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gxhash" version = "3.5.0" @@ -1770,6 +1897,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2101,9 +2242,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags", "cfg-if", @@ -2133,9 +2274,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -2160,6 +2301,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -2332,6 +2491,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -2623,6 +2791,16 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.11" @@ -2856,6 +3034,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -3079,6 +3271,19 @@ dependencies = [ "digest", ] +[[package]] +name = "sha256" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 1a13e1d..dd0749a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,13 @@ prost = "0.13.4" lettre = "0.11.15" rand = "0.8.5" reqwest = "0.12.12" +binrw = "0.15.1" +ecdsa = { version = "0.16.9", features = ["pem", "std", "verifying"] } +sha256 = "1.6.0" +p256 = "0.13.2" +k256 = "0.13.4" +dsa = "0.6.3" +openssl = "0.10.78" diff --git a/grpc-protobufs b/grpc-protobufs index 405fe9b..4101111 160000 --- a/grpc-protobufs +++ b/grpc-protobufs @@ -1 +1 @@ -Subproject commit 405fe9b47b416e76b21d7087b2ed11606deccfcf +Subproject commit 410111190ec9f540d60108b70d55a437a6caf68e diff --git a/src/account/account.rs b/src/account/account.rs index 1640cad..df198bc 100644 --- a/src/account/account.rs +++ b/src/account/account.rs @@ -1,38 +1,39 @@ -use std::io::Write; +use std::io::{Cursor, Write}; use std::ops::{Deref, DerefMut}; // Don't import until required. // use argon2::{Algorithm, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; // use argon2::password_hash::rand_core::OsRng; // use argon2::password_hash::SaltString; -use base64::Engine; -use base64::prelude::BASE64_STANDARD; -use bytemuck::bytes_of; -use chrono::{NaiveDate, NaiveDateTime, Utc}; -use rocket::http::Status; -use rocket::{async_trait, Request}; -use rocket::request::{FromRequest, Outcome}; -use sha2::{Digest, Sha256}; +use crate::Pool; use crate::error::{Error, Errors}; use crate::nnid::oauth::TokenData; -use crate::Pool; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; +use binrw::{BinRead, binread}; +use bytemuck::bytes_of; +use chrono::{NaiveDate, NaiveDateTime, Utc}; +use openssl::bn::BigNum; +use openssl::ecdsa::EcdsaSig; use rand::Rng; +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome}; +use rocket::{Request, async_trait}; +use sha2::{Digest, Sha256}; macro_rules! request_try { ($expression:expr) => { - match $expression{ + match $expression { Ok(v) => v, - Err(e) => return Outcome::Error((Status::BadRequest, e)) + Err(e) => return Outcome::Error((Status::BadRequest, e)), } }; } -const INVALID_TOKEN_ERRORS: Errors<'static> = Errors{ - error: &[ - Error{ - message: "Invalid access token", - code: "0005" - } - ] +const INVALID_TOKEN_ERRORS: Errors<'static> = Errors { + error: &[Error { + message: "Invalid access token", + code: "0005", + }], }; // optimization note: add token caching @@ -56,52 +57,48 @@ pub struct User { pub creation_date: NaiveDateTime, pub updated: NaiveDateTime, pub nex_password: String, - pub verification_code: Option + pub verification_code: Option, } -fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{ +fn generate_nintendo_hash(pid: i32, text_password: &str) -> String { let mut sha = Sha256::new(); sha.write_all(&bytes_of(&pid)).unwrap(); - sha.write_all(&[0x02, 0x65, 0x43 ,0x46]).unwrap(); + sha.write_all(&[0x02, 0x65, 0x43, 0x46]).unwrap(); sha.write_all(text_password.as_bytes()).unwrap(); hex::encode(&sha.finalize()[..]) } -impl User{ - pub async fn get_by_username(name: &str, pool: &Pool) -> Option{ - sqlx::query_as!( - Self, - "SELECT * FROM users WHERE username = $1", - name - ).fetch_one(pool) +impl User { + pub async fn get_by_username(name: &str, pool: &Pool) -> Option { + sqlx::query_as!(Self, "SELECT * FROM users WHERE username = $1", name) + .fetch_one(pool) .await .ok() } - fn generate_nintendo_hash(&self, text_password: &str) -> String{ + fn generate_nintendo_hash(&self, text_password: &str) -> String { generate_nintendo_hash(self.pid, text_password) } - pub fn verify_cleartext_password(&self, cleartext_password: &str) -> Option{ + pub fn verify_cleartext_password(&self, cleartext_password: &str) -> Option { let nintendo_hash = self.generate_nintendo_hash(cleartext_password); self.verify_hashed_password(&nintendo_hash) } - pub fn verify_hashed_password(&self, hashed_password: &str) -> Option{ + pub fn verify_hashed_password(&self, hashed_password: &str) -> Option { bcrypt::verify(hashed_password, &self.password).ok() } } -pub fn generate_password(pid: i32, cleartext_password: &str) -> Option{ +pub fn generate_password(pid: i32, cleartext_password: &str) -> Option { let password = generate_nintendo_hash(pid, cleartext_password); bcrypt::hash(password, 10).ok() } - pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option { let data = match BASE64_STANDARD.decode(&token) { Ok(d) => d, @@ -131,7 +128,9 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option u, @@ -150,26 +149,27 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option Option { let data = TokenData::decode(token)?; - let token_info = - sqlx::query!( - "select * from tokens where pid = $1 and token_id = $2 and random =$3", - data.pid, data.token_id, data.random - ). - fetch_one(connection).await.ok()?; + let token_info = sqlx::query!( + "select * from tokens where pid = $1 and token_id = $2 and random =$3", + data.pid, + data.token_id, + data.random + ) + .fetch_one(connection) + .await + .ok()?; - if token_info.expires.and_utc() < Utc::now(){ - return None + if token_info.expires.and_utc() < Utc::now() { + return None; } - let user = sqlx::query_as!( - User, - "SELECT * FROM users WHERE pid = $1", - token_info.pid - ).fetch_one(connection).await.ok()?; + let user = sqlx::query_as!(User, "SELECT * FROM users WHERE pid = $1", token_info.pid) + .fetch_one(connection) + .await + .ok()?; Some(user) } @@ -180,7 +180,7 @@ pub fn generate_nex_password() -> String { while output.len() < 16 { let offset: u8 = rng.gen_range(0..62); - + let character = if offset < 10 { (offset + b'0') as char } else if offset < 36 { @@ -188,59 +188,77 @@ pub fn generate_nex_password() -> String { } else { (offset + 61) as char }; - + output.push(character); } output } -pub struct Auth(pub User); +pub struct Auth( + pub User, +); -impl AsRef for Auth{ +impl AsRef + for Auth +{ fn as_ref(&self) -> &User { &self.0 } } -impl AsMut for Auth{ +impl AsMut + for Auth +{ fn as_mut(&mut self) -> &mut User { &mut self.0 } } -impl Deref for Auth{ +impl Deref + for Auth +{ type Target = User; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for Auth{ +impl DerefMut + for Auth +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl Into for Auth{ +impl Into + for Auth +{ fn into(self) -> User { self.0 } } - - #[async_trait] -impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth{ +impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r> + for Auth +{ type Error = Errors<'static>; async fn from_request(request: &'r Request<'_>) -> Outcome { let pool: &Pool = request.rocket().state().unwrap(); - let auth = request_try!(request.headers().get("Authorization").next().ok_or(INVALID_TOKEN_ERRORS)); + let auth = request_try!( + request + .headers() + .get("Authorization") + .next() + .ok_or(INVALID_TOKEN_ERRORS) + ); let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS)); - let user = match auth_type{ + let user = match auth_type { "Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await, "Bearer" => read_bearer_auth_token(pool, token).await, _ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)), @@ -250,10 +268,24 @@ impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth16}", user.nex_password), // ..user @@ -261,4 +293,80 @@ impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth Option { + let data = BASE64_STANDARD.decode(data).unwrap(); + + let cert = OuterCertificate::read(&mut Cursor::new(&data)).ok()?; + + println!("key"); + + let key = openssl::ec::EcKey::public_key_from_pem(PUB_PEM).ok()?; + + let sig_components = read_p1363(&cert.signature)?; + + let sig = EcdsaSig::from_private_components(sig_components.0, sig_components.1).unwrap(); + + let mut hasher = openssl::sha::Sha256::new(); + + hasher.update(&cert.data); + + let hash = hasher.finish(); + + if !sig.verify(&hash[..], &key).ok()? { + return None; + } + + Certificate::read(&mut Cursor::new(cert.data)).ok() + } + + pub fn hash(&self) -> [u8; 32] { + let mut hasher = openssl::sha::Sha256::new(); + + hasher.update(&self.issuer[..]); + hasher.update(bytes_of(&self.key_id)); + hasher.update(&self.cert_name[..]); + hasher.update(bytes_of(&self.key_type)); + hasher.update(&self.pubkey_data[..]); + + hasher.finish() + } +} + +fn read_p1363(data: &[u8]) -> Option<(BigNum, BigNum)> { + if data.len() % 2 != 0 { + return None; + } + let half_len = data.len() / 2; + Some(( + BigNum::from_slice(&data[..half_len]).ok()?, + BigNum::from_slice(&data[half_len..]).ok()?, + )) +}