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"]
|
serve_dir = ["mime_guess", "tokio/fs"]
|
||||||
ratelimiting = ["dashmap"]
|
ratelimiting = ["dashmap"]
|
||||||
certgen = ["rcgen", "gemini_srv"]
|
certgen = ["rcgen", "gemini_srv"]
|
||||||
gemini_srv = ["tokio-rustls", "webpki", "rustls"]
|
gemini_srv = ["tokio-rustls", "webpki", "rustls", "ring"]
|
||||||
scgi_srv = []
|
scgi_srv = ["base64"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
|
@ -28,7 +28,8 @@ tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] }
|
||||||
uriparse = "0.6.3"
|
uriparse = "0.6.3"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
log = "0.4.11"
|
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 }
|
lazy_static = { version = "1.4.0", optional = true }
|
||||||
rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true}
|
rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true}
|
||||||
webpki = { version = "0.21.0", optional = true}
|
webpki = { version = "0.21.0", optional = true}
|
||||||
|
|
|
@ -57,6 +57,14 @@
|
||||||
//! * `dashmap` - Enables some minor optimizations within the `user_management_routes`
|
//! * `dashmap` - Enables some minor optimizations within the `user_management_routes`
|
||||||
//! feature. Automatically enabled by `ratelimiting`.
|
//! 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
|
//! * `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,
|
//! 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.
|
//! and compilation will fail if both are enabled. See below for more information.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
#[cfg(feature = "gemini_srv")]
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
#[cfg(feature = "scgi_srv")]
|
#[cfg(feature = "scgi_srv")]
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -41,6 +42,7 @@ impl Request {
|
||||||
manager: UserManager,
|
manager: UserManager,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
#[cfg(feature = "scgi_srv")]
|
#[cfg(feature = "scgi_srv")]
|
||||||
|
#[allow(clippy::or_fun_call)] // Lay off it's a macro
|
||||||
let (mut uri, certificate, script_path) = (
|
let (mut uri, certificate, script_path) = (
|
||||||
URIReference::try_from(
|
URIReference::try_from(
|
||||||
format!(
|
format!(
|
||||||
|
@ -55,14 +57,8 @@ impl Request {
|
||||||
)
|
)
|
||||||
.context("Request URI is invalid")?
|
.context("Request URI is invalid")?
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
headers.get("TLS_CLIENT_HASH")
|
headers.get("TLS_CLIENT_HASH").map(hash_decode)
|
||||||
.map(|hsh| {
|
.ok_or(anyhow!("Received malformed TLS client hash from upstream. Expected 256 bit hex or b64 encoded"))?,
|
||||||
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("SCRIPT_PATH")
|
headers.get("SCRIPT_PATH")
|
||||||
.or_else(|| headers.get("SCRIPT_NAME"))
|
.or_else(|| headers.get("SCRIPT_NAME"))
|
||||||
.cloned()
|
.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 {
|
impl ops::Deref for Request {
|
||||||
type Target = URIReference<'static>;
|
type Target = URIReference<'static>;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
use sled::Transactional;
|
use sled::Transactional;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ring"))]
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::user_management::UserManager;
|
use crate::user_management::UserManager;
|
||||||
use crate::user_management::Result;
|
use crate::user_management::Result;
|
||||||
|
|
||||||
|
@ -32,7 +35,7 @@ const ARGON2_CONFIG: argon2::Config = argon2::Config {
|
||||||
version: argon2::Version::Version13,
|
version: argon2::Version::Version13,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "user_management_advanced")]
|
#[cfg(all(feature = "user_management_advanced", feature = "ring"))]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new();
|
static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new();
|
||||||
}
|
}
|
||||||
|
@ -287,9 +290,24 @@ impl<UserData: Serialize + DeserializeOwned> RegisteredUser<UserData> {
|
||||||
&mut self,
|
&mut self,
|
||||||
password: impl AsRef<[u8]>,
|
password: impl AsRef<[u8]>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let salt: [u8; 32] = ring::rand::generate(&*RANDOM)
|
#[cfg_attr(feature = "ring", allow(unused_mut))]
|
||||||
.expect("Error generating random salt")
|
let mut salt: [u8; 32];
|
||||||
.expose();
|
|
||||||
|
// 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((
|
self.inner.pass_hash = Some((
|
||||||
argon2::hash_raw(
|
argon2::hash_raw(
|
||||||
password.as_ref(),
|
password.as_ref(),
|
||||||
|
@ -396,3 +414,15 @@ impl<UserData: Serialize + DeserializeOwned> AsMut<UserData> for RegisteredUser<
|
||||||
self.mut_data()
|
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 New Issue