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:
parent
d71c3f952d
commit
fb357b59eb
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue