random progress
parent
eac80b68a2
commit
cac70ae717
File diff suppressed because it is too large
Load Diff
44
Cargo.toml
44
Cargo.toml
|
@ -7,7 +7,49 @@ license = "EUPL-1.2"
|
|||
readme = "README.md"
|
||||
keywords = ["sso", "oidc", "oauth", "oauth2"]
|
||||
edition = "2021"
|
||||
rust-version = "1.64"
|
||||
rust-version = "1.65"
|
||||
exclude = ["doc/", "example/"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
argon2 = { version = "0.4.1", features = ["std"] }
|
||||
axum = { version = "0.6.1", features = ["http2"] }
|
||||
axum-extra = { version = "0.4.2", features = ["cookie"] }
|
||||
base16ct = "0.1.1"
|
||||
base64ct = "1.5.3"
|
||||
blake2 = "0.10.5"
|
||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||
clap = { version = "4.0.29", features = ["derive"] }
|
||||
password-hash = "0.4.2"
|
||||
rand = { version = "0.8.5", default-features = false, features = ["getrandom"] }
|
||||
rsa = "0.7.2"
|
||||
rusqlite = { version = "0.28.0", features = ["serde_json", "time", "uuid"] }
|
||||
rustls = "0.20.7"
|
||||
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
sha2 = { version = "0.10.6", features = ["oid"] }
|
||||
subtle = "2.4.1"
|
||||
tokio = { version = "1.22.0", features = ["rt-multi-thread", "fs", "io-util", "io-std", "macros", "net", "signal", "sync", "time"] }
|
||||
toml = "0.5.9"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["json"] }
|
||||
uuid = { version = "1.2.2", features = ["v4"] }
|
||||
zeroize = { version = "1.5.7", features = ["std", "derive"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
incremental = false
|
||||
lto = "fat"
|
||||
panic = "abort"
|
||||
strip = "debuginfo"
|
||||
|
||||
[profile.smaller]
|
||||
inherits = "release"
|
||||
opt-level = "s"
|
||||
|
||||
[features]
|
||||
bundled = ["rusqlite/bundled"]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# ChibiAuth
|
||||
|
||||
Naive OpenID Provider.
|
||||
Naive IAM server.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 9909
|
||||
database = "example/database.db"
|
||||
base-url = "https://example.com"
|
||||
|
||||
[server.tls]
|
||||
offload = false
|
||||
certificate = "example/cert.pem"
|
||||
private-key = "example/priv.pem"
|
||||
|
||||
[server.mtls]
|
||||
enabled = false
|
|
@ -0,0 +1,59 @@
|
|||
/* API Tokens */
|
||||
CREATE TABLE IF NOT EXISTS api_token
|
||||
( selector TEXT PRIMARY KEY
|
||||
, verifier BLOB NOT NULL
|
||||
, expiration INTEGER NOT NULL /* TAI64N */
|
||||
, description TEXT
|
||||
) STRICT;
|
||||
|
||||
/* General User Management */
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
( uuid BLOB PRIMARY KEY
|
||||
, username TEXT NOT NULL
|
||||
, passhash TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pass_reset
|
||||
( selector TEXT PRIMARY KEY
|
||||
, verifier BLOB NOT NULL
|
||||
, expiration INTEGER NOT NULL
|
||||
, uuid BLOB NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
/* OAuth & OIDC */
|
||||
CREATE TABLE IF NOT EXISTS oidc_rs256
|
||||
( kid TEXT UNIQUE NOT NULL
|
||||
, keyblob BLOB NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_hs256
|
||||
( kid TEXT UNIQUE NOT NULL
|
||||
, keyblob BLOB NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_generation
|
||||
( tokentype TEXT NOT NULL
|
||||
, not_before INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_clients
|
||||
( uuid BLOB PRIMARY KEY
|
||||
, name TEXT NOT NULL
|
||||
, subdomains TEXT NOT NULL
|
||||
, kid TEXT NOT NULL
|
||||
, is_secret INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_session_tokens
|
||||
( tokenhash BLOB PRIMARY KEY
|
||||
, uuid BLOB REFERENCES users ON DELETE CASCADE,
|
||||
, generation INTEGER NOT NULL
|
||||
, expiration INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_refresh_tokens
|
||||
( tokenhash BLOB PRIMARY KEY
|
||||
, uuid BLOB REFERENCES users ON DELETE CASCADE
|
||||
, generation INTEGER NOT NULL
|
||||
, expiration INTEGER NOT NULL
|
||||
) STRICT;
|
|
@ -0,0 +1,2 @@
|
|||
mod oidc;
|
||||
mod api;
|
|
@ -0,0 +1,25 @@
|
|||
use anyhow::Result;
|
||||
use blake2::{Blake2b512, Blake2s256, Digest};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use rusqlite::Connection;
|
||||
use secrecy::{ExposeSecret, Secret, SecretString};
|
||||
|
||||
pub struct ApiKey {
|
||||
selector: String,
|
||||
verifier: String,
|
||||
}
|
||||
|
||||
impl ApiKey {
|
||||
pub fn new() -> Result<SecretString, ApiKey> {
|
||||
let mut raw_key = [0u8; 32];
|
||||
|
||||
OsRng.try_fill_bytes(&mut key)?;
|
||||
|
||||
let key = base16ct::upper::encode_string(raw_key);
|
||||
|
||||
let mut h = Blake2b512::new();
|
||||
|
||||
h.update(b"");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//! OIDC
|
||||
//!
|
||||
//! # JWK
|
||||
//! Only `HS256` and `RS256` are supported.
|
||||
|
||||
mod hs256;
|
||||
mod rs256;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64ct::{Base64Url, Encoding};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
pub use hs256::Hs256;
|
||||
pub use rs256::Rs256;
|
||||
|
||||
pub struct OidcAuthnRequest {
|
||||
pub user: String,
|
||||
pub pass: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
pub struct OidcDiscovery {
|
||||
pub jwks: String,
|
||||
}
|
||||
|
||||
pub struct OidcKeyEngine {
|
||||
discovery: OidcDiscovery,
|
||||
}
|
||||
|
||||
/// Generates a 192-bit random key ID.
|
||||
#[inline(always)]
|
||||
fn generate_kid() -> Result<String> {
|
||||
let mut kid_bytes = [0u8; 24];
|
||||
|
||||
OsRng.try_fill_bytes(&mut kid_bytes)?;
|
||||
|
||||
Ok(Base64Url::encode_string(&kid_bytes))
|
||||
}
|
||||
|
||||
impl OidcDiscovery {}
|
|
@ -0,0 +1,24 @@
|
|||
use anyhow::Result;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use secrecy::Secret;
|
||||
|
||||
use crate::auth::oidc::generate_kid;
|
||||
|
||||
pub struct Hs256 {
|
||||
kid: String,
|
||||
key: Secret<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl Hs256 {
|
||||
pub fn new() -> Result<Hs256> {
|
||||
let mut key = [0u8; 32];
|
||||
|
||||
OsRng.try_fill_bytes(&mut key)?;
|
||||
|
||||
Ok(Hs256 {
|
||||
kid: generate_kid()?,
|
||||
key: Secret::new(key),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use anyhow::Result;
|
||||
use base64ct::{Base64Url, Encoding};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use rsa::pkcs1v15::{SigningKey, VerifyingKey};
|
||||
use rsa::{PublicKeyParts, RsaPrivateKey, RsaPublicKey};
|
||||
use serde_json::json;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::auth::oidc::generate_kid;
|
||||
|
||||
static RSA_BITS: usize = 2048;
|
||||
|
||||
/// RSASSA-PKCS1v1.5 with SHA-2-256
|
||||
///
|
||||
/// Only 2048-bit keys are used.
|
||||
pub struct Rs256 {
|
||||
kid: String,
|
||||
signing_key: SigningKey<Sha256>,
|
||||
verifying_key: VerifyingKey<Sha256>,
|
||||
}
|
||||
|
||||
impl Rs256 {
|
||||
pub fn new() -> Result<Rs256> {
|
||||
let kid = generate_kid()?;
|
||||
|
||||
let p = RsaPrivateKey::new(&mut OsRng, RSA_BITS)?;
|
||||
|
||||
let signing_key = SigningKey::new_with_prefix(p);
|
||||
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
Ok(Rs256 {
|
||||
kid,
|
||||
signing_key,
|
||||
verifying_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn discovery_key(&self) -> Result<serde_json::Value> {
|
||||
let public_key: &RsaPublicKey = self.verifying_key.as_ref();
|
||||
|
||||
let n_bytes = public_key.n().to_bytes_be();
|
||||
|
||||
let n = Base64Url::encode_string(&n_bytes);
|
||||
|
||||
Ok(json!({
|
||||
"alg": "RS256",
|
||||
"kty": "RSA",
|
||||
"kid": self.kid,
|
||||
"e": "AQAB",
|
||||
"n": n,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rsa::{PublicKeyParts, RsaPublicKey};
|
||||
|
||||
use crate::auth::oidc::rs256::*;
|
||||
|
||||
// This can fail in two situations.
|
||||
// 1. BigUint no longer uses the minimum bits required.
|
||||
// 2. The `rsa` crate has changed its default exponent.
|
||||
#[test]
|
||||
fn rsa_default_exp_is_serialized_as_aqab() {
|
||||
let private_key = RsaPrivateKey::new(&mut OsRng, RSA_BITS).unwrap();
|
||||
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
|
||||
let e_bytes = public_key.e().to_bytes_be();
|
||||
|
||||
let e_str = Base64Url::encode_string(&e_bytes);
|
||||
|
||||
assert_eq!(e_str, "AQAB");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
server: ServerConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
host: String,
|
||||
port: u32,
|
||||
|
||||
#[serde(rename = "base-url")]
|
||||
base_url: String,
|
||||
|
||||
tls: TlsConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TlsConfig {
|
||||
offload: bool,
|
||||
|
||||
certificate: PathBuf,
|
||||
|
||||
#[serde(rename = "private-key")]
|
||||
private_key: PathBuf,
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
use std::convert::AsRef;
|
||||
use std::ops::{Deref, Drop};
|
||||
use std::option::Option;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::vec::Vec;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use rusqlite::Connection;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
static SCHEMA: &str = include_str!("../schema.sql");
|
||||
|
||||
pub struct PooledConnection {
|
||||
conn: Option<Connection>,
|
||||
pool: Arc<Mutex<Vec<Connection>>>,
|
||||
}
|
||||
|
||||
/// Naive databse pool.
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pool: Arc<Mutex<Vec<Connection>>>,
|
||||
}
|
||||
|
||||
/// Requires WAL and sets up complementary settings.
|
||||
pub fn setup_sqlite_conn(path: &dyn AsRef<Path>) -> Result<Connection> {
|
||||
let conn = Connection::open(path)?;
|
||||
|
||||
conn.pragma_update(None, "journal_mode", "wal")?;
|
||||
conn.pragma_update(None, "synchronous", "normal")?;
|
||||
conn.pragma_update(None, "busy_timeout", 5000)?;
|
||||
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
impl Deref for PooledConnection {
|
||||
type Target = Connection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
fn drop(&mut self) {
|
||||
let mut p = self.pool.blocking_lock();
|
||||
|
||||
p.push(self.conn.take().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(path: &dyn AsRef<Path>, capacity: usize) -> Result<Database> {
|
||||
let mut v = Vec::new();
|
||||
|
||||
v.try_reserve_exact(capacity)?;
|
||||
|
||||
for _ in 0..capacity {
|
||||
let c = setup_sqlite_conn(path)?;
|
||||
|
||||
v.push(c);
|
||||
}
|
||||
|
||||
let pool = Arc::new(Mutex::new(v));
|
||||
|
||||
Ok(Database { pool })
|
||||
}
|
||||
|
||||
pub async fn get(&mut self) -> Result<PooledConnection> {
|
||||
let mut pool = self.pool.lock().await;
|
||||
|
||||
let conn = pool.pop();
|
||||
|
||||
Ok(PooledConnection {
|
||||
conn,
|
||||
pool: Arc::clone(&self.pool),
|
||||
})
|
||||
}
|
||||
}
|
120
src/main.rs
120
src/main.rs
|
@ -1,3 +1,119 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod auth;
|
||||
mod config;
|
||||
mod database;
|
||||
mod model;
|
||||
mod state;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use tracing::{error, Level};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Run the server
|
||||
#[command(arg_required_else_help = true)]
|
||||
Run {
|
||||
/// Path to the configuration file
|
||||
#[arg(short, long)]
|
||||
config: PathBuf,
|
||||
|
||||
/// Emit logs as ND-JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
|
||||
/// Repeat to increase verbosity up to 2 times
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
},
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Keygen {
|
||||
/// Path to the configuration file
|
||||
#[arg(short, long)]
|
||||
config: PathBuf,
|
||||
|
||||
#[arg(long)]
|
||||
api: bool,
|
||||
|
||||
#[arg(long)]
|
||||
encrypt: bool,
|
||||
|
||||
#[arg(long)]
|
||||
oidc_rs256: bool,
|
||||
},
|
||||
|
||||
#[command(arg_required_else_help = true)]
|
||||
Gc {
|
||||
/// Path to the configuration file
|
||||
#[arg(short, long)]
|
||||
config: PathBuf,
|
||||
},
|
||||
|
||||
Revoke {
|
||||
/// Path to the configuration file
|
||||
#[arg(short, long)]
|
||||
config: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
fn setup_logging(use_json: bool, verbosity: u8) -> Result<()> {
|
||||
let max_level = match verbosity {
|
||||
0 => Level::INFO,
|
||||
1 => Level::DEBUG,
|
||||
2.. => Level::TRACE,
|
||||
};
|
||||
|
||||
let fmt = tracing_subscriber::fmt().with_max_level(max_level);
|
||||
|
||||
if use_json {
|
||||
let collector = fmt.json().finish();
|
||||
tracing::subscriber::set_global_default(collector)?;
|
||||
} else {
|
||||
let collector = fmt.pretty().finish();
|
||||
tracing::subscriber::set_global_default(collector)?;
|
||||
}
|
||||
|
||||
std::panic::set_hook(Box::new(|panic| {
|
||||
if let Some(location) = panic.location() {
|
||||
error!(
|
||||
message = %panic,
|
||||
panic.file = location.file(),
|
||||
panic.line = location.line(),
|
||||
panic.column = location.column(),
|
||||
);
|
||||
} else {
|
||||
error!(message = %panic);
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let args = Cli::parse();
|
||||
|
||||
match args.command {
|
||||
Commands::Run {
|
||||
config,
|
||||
json,
|
||||
verbose,
|
||||
} => {
|
||||
setup_logging(json, verbose);
|
||||
}
|
||||
Commands::Keygen {
|
||||
config,
|
||||
api,
|
||||
encrypt,
|
||||
oidc_rs256,
|
||||
} => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod user;
|
|
@ -0,0 +1,64 @@
|
|||
use anyhow::Result;
|
||||
use argon2::password_hash::PasswordHash;
|
||||
use argon2::{Algorithm, Argon2, PasswordHasher, PasswordVerifier, Version};
|
||||
use password_hash::SaltString;
|
||||
use rand::rngs::OsRng;
|
||||
use secrecy::SecretString;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Parameters are from https://twitter.com/Sc00bzT/status/1557495201064558592
|
||||
static ARGON2_M: u32 = 256;
|
||||
static ARGON2_T: u32 = 8;
|
||||
static ARGON2_P: u32 = 1;
|
||||
static ARGON2_OUTPUT_LEN: Option<usize> = Some(32);
|
||||
|
||||
pub struct User {
|
||||
uuid: Uuid,
|
||||
username: String,
|
||||
secrethash: String,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_passphrase_kdf<'a>() -> Result<Argon2<'a>> {
|
||||
let params = argon2::Params::new(256, 8, 1, Some(32))?;
|
||||
|
||||
let kdf = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
|
||||
Ok(kdf)
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: String, secret: &[u8]) -> Result<User> {
|
||||
let params = argon2::Params::new(ARGON2_M, ARGON2_T, ARGON2_P, ARGON2_OUTPUT_LEN)?;
|
||||
let kdf = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let secrethash = kdf.hash_password(secret, &salt)?.to_string();
|
||||
|
||||
Ok(User {
|
||||
uuid: Uuid::new_v4(),
|
||||
username,
|
||||
secrethash,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regenerate_id(self) -> User {
|
||||
User {
|
||||
uuid: Uuid::new_v4(),
|
||||
username: self.username,
|
||||
secrethash: self.secrethash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(self, pass: &[u8]) -> Result<bool> {
|
||||
let kdf = get_passphrase_kdf()?;
|
||||
|
||||
let parsed_hash = PasswordHash::new(&self.secrethash)?;
|
||||
|
||||
match kdf.verify_password(pass, &parsed_hash) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use axum::extract::FromRef;
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
// clone if cloneable
|
||||
pub struct AppState {
|
||||
database: Database,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Database {
|
||||
fn from_ref(state: &AppState) -> Database {
|
||||
state.database.clone()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue