Include server fingerprint in auth responses and rename project

This commit is contained in:
Bad Manners 2024-12-19 19:17:21 -03:00
parent 979cc61acd
commit 6c654072a5
4 changed files with 52 additions and 32 deletions

38
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -83,6 +83,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "auth_ssh_games"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64 0.22.1",
"clap",
"hmac",
"jsonwebtoken",
"ring",
"serde",
"serde_json",
"sha2",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -847,24 +865,6 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "sish_games"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64 0.22.1",
"clap",
"hmac",
"jsonwebtoken",
"ring",
"serde",
"serde_json",
"sha2",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "sish_games" name = "auth_ssh_games"
description = "Server for sish games" description = "Authentication server for SSH-based games"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View file

@ -5,5 +5,5 @@ COPY . .
RUN cargo build --release RUN cargo build --release
FROM alpine:3.20 FROM alpine:3.20
COPY --from=builder /usr/src/app/target/release/sish_games /usr/local/bin/sish_games COPY --from=builder /usr/src/app/target/release/auth_ssh_games /usr/local/bin/auth_ssh_games
ENTRYPOINT [ "sish_games" ] ENTRYPOINT [ "auth_ssh_games" ]

View file

@ -27,8 +27,15 @@ struct GenerationRequest {
password: String, password: String,
} }
#[derive(Serialize)]
struct GenerationResponse {
jwt: String,
server_fingerprint: String,
}
#[derive(Clone)] #[derive(Clone)]
struct GenerationState { struct GenerationState {
server_fingerprint: Arc<String>,
encoding_key: Arc<EncodingKey>, encoding_key: Arc<EncodingKey>,
audiences: Arc<HashSet<String>>, audiences: Arc<HashSet<String>>,
} }
@ -49,16 +56,24 @@ struct ValidationState {
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
/// Fingerprint of the server's SSH key, to avoid MitM attacks.
#[arg(short = 'f', long, value_name = "FINGERPRINT")]
server_fingerprint: String,
/// Private key for signing JWTs.
#[arg(short = 'i', long, value_name = "FILE")] #[arg(short = 'i', long, value_name = "FILE")]
private_key: PathBuf, private_key: PathBuf,
/// Public key for validating JWTs.
#[arg(short, long, value_name = "FILE")] #[arg(short, long, value_name = "FILE")]
public_key: PathBuf, public_key: PathBuf,
#[arg(short, long, num_args(1..))] /// Allowed audiences for JWTs.
#[arg(short, long, num_args(1..), value_name = "AUDIENCE")]
audiences: Vec<String>, audiences: Vec<String>,
#[arg(short, long)] /// Secret token that's part of the URL for authentication.
#[arg(short, long, value_name = "TOKEN")]
validation_token: String, validation_token: String,
} }
@ -82,6 +97,7 @@ async fn main() -> Result<()> {
let router = Router::new() let router = Router::new()
.route("/", post(generation_handler)) .route("/", post(generation_handler))
.with_state(GenerationState { .with_state(GenerationState {
server_fingerprint: Arc::new(args.server_fingerprint),
encoding_key, encoding_key,
audiences: Arc::new(audiences.iter().cloned().collect()), audiences: Arc::new(audiences.iter().cloned().collect()),
}) })
@ -103,16 +119,16 @@ async fn generation_handler(
Json(payload): Json<GenerationRequest>, Json(payload): Json<GenerationRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if !state.audiences.contains(&payload.audience) { if !state.audiences.contains(&payload.audience) {
return StatusCode::FORBIDDEN.into_response(); return (StatusCode::FORBIDDEN, "invalid payload").into_response();
} }
let password = match BASE64_STANDARD.decode(payload.password) { let password = match BASE64_STANDARD.decode(payload.password) {
Ok(password) => password, Ok(password) => password,
Err(_) => return StatusCode::FORBIDDEN.into_response(), Err(_) => return (StatusCode::FORBIDDEN, "invalid payload").into_response(),
}; };
let mut hmac = Hmac::<Sha256>::new_from_slice(payload.audience.as_bytes()).unwrap(); let mut hmac = Hmac::<Sha256>::new_from_slice(payload.audience.as_bytes()).unwrap();
hmac.update(payload.user.as_bytes()); hmac.update(payload.user.as_bytes());
if hmac.verify_slice(&password).is_err() { if hmac.verify_slice(&password).is_err() {
return StatusCode::FORBIDDEN.into_response(); return (StatusCode::FORBIDDEN, "invalid payload").into_response();
} }
let claims = AuthenticationClaims { let claims = AuthenticationClaims {
aud: payload.audience, aud: payload.audience,
@ -125,7 +141,11 @@ async fn generation_handler(
&state.encoding_key, &state.encoding_key,
) )
.unwrap(); .unwrap();
jwt.into_response() Json(GenerationResponse {
jwt,
server_fingerprint: state.server_fingerprint.as_ref().clone(),
})
.into_response()
} }
async fn validation_handler( async fn validation_handler(
@ -138,11 +158,11 @@ async fn validation_handler(
&state.validation, &state.validation,
) { ) {
Ok(token) => token.claims, Ok(token) => token.claims,
Err(_) => return StatusCode::FORBIDDEN, Err(_) => return (StatusCode::FORBIDDEN, "invalid auth").into_response(),
}; };
if claims.sub == payload.user && Uuid::try_parse(&payload.user).is_ok() { if claims.sub == payload.user && Uuid::try_parse(&payload.user).is_ok() {
StatusCode::OK StatusCode::OK.into_response()
} else { } else {
StatusCode::FORBIDDEN (StatusCode::FORBIDDEN, "invalid auth").into_response()
} }
} }