From fb357b59ebb00f113a2c43db67aad6f999bdc51d Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 08:10:50 -0500 Subject: [PATCH] Support base64 encoded certificate hashes, remove ring as mandatory dep for scgi_srv Removing ring also means switching out some of the code around user password hashes so that's what that is, if you're wondering. Also, the sunrise today was absolutely beautiful. I haven't been awake for a sunrise in a long time, so I guess that's one upside to not sleeping. Like, melencholy sunsets get a lot of love, but man, nothing fills you with hope and positivity like a sunrise. --- Cargo.toml | 7 +++--- src/lib.rs | 8 +++++++ src/types/request.rs | 47 ++++++++++++++++++++++++++++++------- src/user_management/user.rs | 38 ++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94452ce..142d3d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] certgen = ["rcgen", "gemini_srv"] -gemini_srv = ["tokio-rustls", "webpki", "rustls"] -scgi_srv = [] +gemini_srv = ["tokio-rustls", "webpki", "rustls", "ring"] +scgi_srv = ["base64"] [dependencies] anyhow = "1.0.33" @@ -28,7 +28,8 @@ tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" -ring = "0.16.15" +ring = { version = "0.16.15", optional = true } +base64 = { version = "0.13.0", optional = true } lazy_static = { version = "1.4.0", optional = true } rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} webpki = { version = "0.21.0", optional = true} diff --git a/src/lib.rs b/src/lib.rs index 31d020d..2ce7630 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,14 @@ //! * `dashmap` - Enables some minor optimizations within the `user_management_routes` //! feature. Automatically enabled by `ratelimiting`. //! +//! * `ring` - When using `user_management_advanced` with `scgi_srv`, salts are calculated +//! based off of a simple PRNG and the system time. This should be plenty secure enough, +//! especially since we're using a good number argon2 rounds, but for bonus paranoia +//! points, you can add ring as a dependency to source secure random. This is enabled +//! automatically on `gemini_srv`, since `ring` is added as a dependency for certificate +//! processing. When not using the `user_management_advanced` feature, this does nothing +//! but increase your build time & size. +//! //! * `gemini_srv`/`scgi_srv` - Switches between serving content using SCGI and serving //! content as a raw gemini server. One and only one of these features must be enabled, //! and compilation will fail if both are enabled. See below for more information. diff --git a/src/types/request.rs b/src/types/request.rs index 34fddf9..f3e3801 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,4 +1,5 @@ use std::ops; +#[cfg(feature = "gemini_srv")] use std::convert::TryInto; #[cfg(feature = "scgi_srv")] use std::{ @@ -41,6 +42,7 @@ impl Request { manager: UserManager, ) -> Result { #[cfg(feature = "scgi_srv")] + #[allow(clippy::or_fun_call)] // Lay off it's a macro let (mut uri, certificate, script_path) = ( URIReference::try_from( format!( @@ -55,14 +57,8 @@ impl Request { ) .context("Request URI is invalid")? .into_owned(), - headers.get("TLS_CLIENT_HASH") - .map(|hsh| { - ring::test::from_hex(hsh.as_str()) - .expect("Received invalid certificate fingerprint from upstream") - .as_slice() - .try_into() - .expect("Received certificate fingerprint of invalid lenght from upstream") - }), + headers.get("TLS_CLIENT_HASH").map(hash_decode) + .ok_or(anyhow!("Received malformed TLS client hash from upstream. Expected 256 bit hex or b64 encoded"))?, headers.get("SCRIPT_PATH") .or_else(|| headers.get("SCRIPT_NAME")) .cloned() @@ -245,6 +241,41 @@ impl Request { } } +#[allow(clippy::ptr_arg)] // This is a single use function that expects a &String +/// Attempt to decode a 256 bit hash +/// +/// Will attempt to decode first as hexadecimal, and then as base64. If both fail, return +/// [`None`] +fn hash_decode(hash: &String) -> Option<[u8; 32]> { + let mut buffer = [0u8; 32]; + if hash.len() == 64 { // Looks like a hex + // Lifted (lightly modified) from ring::test::from_hex + for (i, digits) in hash.as_bytes().chunks(2).enumerate() { + let hi = from_hex_digit(digits[0])?; + let lo = from_hex_digit(digits[1])?; + buffer[i] = (hi * 0x10) | lo; + } + Some(buffer) + } else if hash.len() == 44 { // Look like base64 + base64::decode_config_slice(hash, base64::STANDARD, &mut buffer).ok()?; + Some(buffer) + } else { + None + } +} + +/// Attempt to decode a hex encoded nibble to u8 +/// +/// Returns [`None`] if not a valid hex character +fn from_hex_digit(d: u8) -> Option { + match d { + b'0'..=b'9' => Some(d - b'0'), + b'a'..=b'f' => Some(d - b'a' + 10u8), + b'A'..=b'F' => Some(d - b'A' + 10u8), + _ => None, + } +} + impl ops::Deref for Request { type Target = URIReference<'static>; diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 043c1ff..bdfa85a 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -16,6 +16,9 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; +#[cfg(not(feature = "ring"))] +use std::time::{SystemTime, UNIX_EPOCH}; + use crate::user_management::UserManager; use crate::user_management::Result; @@ -32,7 +35,7 @@ const ARGON2_CONFIG: argon2::Config = argon2::Config { version: argon2::Version::Version13, }; -#[cfg(feature = "user_management_advanced")] +#[cfg(all(feature = "user_management_advanced", feature = "ring"))] lazy_static::lazy_static! { static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new(); } @@ -287,9 +290,24 @@ impl RegisteredUser { &mut self, password: impl AsRef<[u8]>, ) -> Result<()> { - let salt: [u8; 32] = ring::rand::generate(&*RANDOM) - .expect("Error generating random salt") - .expose(); + #[cfg_attr(feature = "ring", allow(unused_mut))] + let mut salt: [u8; 32]; + + // For a simple salt, system time nanos and a bit of PCG is plenty secure enough, + // but if we have ring anyway, may as well use it + #[cfg(feature = "ring")] { + salt = ring::rand::generate(&*RANDOM) + .expect("Error generating random salt") + .expose(); + } + + #[cfg(not(feature = "ring"))] { + let mut random = (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() | 0xffff) as u16; + random = random.wrapping_mul(0xd09d); + salt = [0; 32]; + for byte in salt.as_mut() { *byte = pcg8(&mut random) } + } + self.inner.pass_hash = Some(( argon2::hash_raw( password.as_ref(), @@ -396,3 +414,15 @@ impl AsMut for RegisteredUser< self.mut_data() } } + +#[cfg(all(feature = "user_management_advanced", not(feature = "ring")))] +/// Inexpensive but low quality random +fn pcg8(state: &mut u16) -> u8 { + const MUL: u16 = 0xfb85; + const ADD: u16 = 0xfabb; + let mut x = *state; + *state = state.wrapping_mul(MUL).wrapping_add(ADD); + let count = x >> 13; + x ^= x >> 5; + ((x >> 5) as u8).rotate_right(count as u32) +}