random progress

main
Aydin Mercan 2022-12-03 16:26:51 +03:00
parent eac80b68a2
commit cac70ae717
Signed by: jaiden
SSH Key Fingerprint: SHA256:vy6hjzotbn/MWZAbjzURNk3NL62EPkjoHsJ5xr/s7nk
17 changed files with 2337 additions and 4 deletions

1742
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,49 @@ license = "EUPL-1.2"
readme = "README.md" readme = "README.md"
keywords = ["sso", "oidc", "oauth", "oauth2"] keywords = ["sso", "oidc", "oauth", "oauth2"]
edition = "2021" edition = "2021"
rust-version = "1.64" rust-version = "1.65"
exclude = ["doc/", "example/"] exclude = ["doc/", "example/"]
[badges]
maintenance = { status = "actively-developed" }
[dependencies] [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"]

View File

@ -1,6 +1,6 @@
# ChibiAuth # ChibiAuth
Naive OpenID Provider. Naive IAM server.
## Development ## Development

13
example/config.toml Normal file
View File

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

59
schema.sql Normal file
View File

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

2
src/auth.rs Normal file
View File

@ -0,0 +1,2 @@
mod oidc;
mod api;

25
src/auth/api.rs Normal file
View File

@ -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"");
}
}

43
src/auth/oidc.rs Normal file
View File

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

24
src/auth/oidc/hs256.rs Normal file
View File

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

78
src/auth/oidc/rs256.rs Normal file
View File

@ -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");
}
}

29
src/config.rs Normal file
View File

@ -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
src/controller.rs Normal file
View File

81
src/database.rs Normal file
View File

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

View File

@ -1,3 +1,119 @@
fn main() { mod auth;
println!("Hello, world!"); 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,
} => {}
_ => {}
}
} }

1
src/model.rs Normal file
View File

@ -0,0 +1 @@
pub mod user;

64
src/model/user.rs Normal file
View File

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

14
src/state.rs Normal file
View File

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