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.
This commit is contained in:
Emi Tatsuo 2020-12-03 08:10:50 -05:00
parent d71c3f952d
commit fb357b59eb
Signed by: Emi
GPG Key ID: 68FAB2E2E6DFC98B
4 changed files with 85 additions and 15 deletions

View File

@ -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}

View File

@ -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.

View File

@ -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<Self> {
#[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<u8> {
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>;

View File

@ -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<UserData: Serialize + DeserializeOwned> RegisteredUser<UserData> {
&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<UserData: Serialize + DeserializeOwned> AsMut<UserData> 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)
}