Take the link from wg-app-link instead of keeping a second copy
The five modules underneath this backend that were never about AI sessions -- the pinned CA and leaf, QR enrollment and the bearer token, wg0 binding and the certificate's SANs, owner-only files, and the RON house rules -- were written twice, once here and once in dev-updater, and had drifted. They now come from the submodule, as a path dependency so both projects stay locked to one commit. What stayed is what makes this project itself: the routes, the drivers, the config schema, and the auth middleware, which is generic over this server's state. Sharing a transport is worth doing; sharing an API would mean inventing a vocabulary neither project wants. Four dependencies go with the code -- rcgen, qrcode, subtle and if-addrs are no longer named here at all -- and the three that remain are now described by what still uses them rather than by what used to. Verified by running it, not only by building: a fresh server generates its CA, prints an `aiapp://enroll` QR with the scheme now passed as a parameter, covers 127.0.0.1, 10.0.2.2 and wg0's 10.66.0.1 in the leaf, answers an enrolled token and returns 401 without one, and writes config.ron in the house rules with every file owner-only. 36 tests pass, clippy is silent, rustfmt is clean.
This commit is contained in:
1 parent
a83dbcff6a
commit
aa05ff9336
13 files changed
+76
-526
No files matched your search
+3
-45
@@ -21,9 +21,7 @@ use axum::extract::{ConnectInfo, Request, State};
|
||||
use axum::http::{StatusCode, header};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use base64::Engine;
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
use wg_app_link::enroll::token_matches;
|
||||
|
||||
use crate::session::SessionManager;
|
||||
|
||||
@@ -32,35 +30,6 @@ use crate::session::SessionManager;
|
||||
/// drip rather than a fast one.
|
||||
const REJECT_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
/// 256 bits from the OS CSPRNG, base64url. A machine credential carried by
|
||||
/// a QR code, never typed, so unguessable costs nothing.
|
||||
pub fn generate_token() -> String {
|
||||
use rand::Rng;
|
||||
let mut bytes = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
||||
/// What `config.ron` stores instead of the token: hex SHA-256. A plain
|
||||
/// hash is enough for high-entropy random input, and buys that a leaked
|
||||
/// config doesn't leak the credential.
|
||||
pub fn token_hash_hex(token: &str) -> String {
|
||||
Sha256::digest(token.as_bytes())
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Hash-then-constant-time-compare against every enrolled hash. The fold
|
||||
/// visits every entry regardless of match so the timing doesn't say which
|
||||
/// entry (if any) matched.
|
||||
fn token_matches(presented: &str, stored_hashes: &[String]) -> bool {
|
||||
let presented = token_hash_hex(presented);
|
||||
stored_hashes.iter().fold(false, |matched, stored| {
|
||||
matched | bool::from(presented.as_bytes().ct_eq(stored.as_bytes()))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn require_token(
|
||||
State(manager): State<Arc<SessionManager>>,
|
||||
request: Request,
|
||||
@@ -104,6 +73,8 @@ mod tests {
|
||||
use axum::routing::get;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use wg_app_link::enroll::{generate_token, token_hash_hex};
|
||||
|
||||
use crate::config::TokenEntry;
|
||||
|
||||
fn manager_with_token(dir: &std::path::Path, token: &str) -> Arc<SessionManager> {
|
||||
@@ -139,19 +110,6 @@ mod tests {
|
||||
builder.body(Body::empty()).expect("request")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashing_is_stable_and_tokens_verify() {
|
||||
let token = generate_token();
|
||||
assert_eq!(token_hash_hex(&token), token_hash_hex(&token));
|
||||
assert_ne!(token, generate_token(), "tokens must not repeat");
|
||||
|
||||
let hashes = vec![token_hash_hex(&token), token_hash_hex("other")];
|
||||
assert!(token_matches(&token, &hashes));
|
||||
assert!(token_matches("other", &hashes));
|
||||
assert!(!token_matches("wrong", &hashes));
|
||||
assert!(!token_matches(&token, &[]));
|
||||
}
|
||||
|
||||
/// One test rather than separate gating and logging tests,
|
||||
/// deliberately: tracing caches callsite interest process-wide, so a
|
||||
/// test that hits the rejection path with no subscriber installed can
|
||||
|
||||
Reference in new issue
Block a user