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.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -83,6 +83,24 @@ dependencies = [
"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]]
name = "autocfg"
version = "1.4.0"
@ -847,24 +865,6 @@ dependencies = [
"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]]
name = "smallvec"
version = "1.13.2"

View file

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

View file

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

View file

@ -27,8 +27,15 @@ struct GenerationRequest {
password: String,
}
#[derive(Serialize)]
struct GenerationResponse {
jwt: String,
server_fingerprint: String,
}
#[derive(Clone)]
struct GenerationState {
server_fingerprint: Arc<String>,
encoding_key: Arc<EncodingKey>,
audiences: Arc<HashSet<String>>,
}
@ -49,16 +56,24 @@ struct ValidationState {
#[derive(Parser)]
#[command(version, about, long_about = None)]
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")]
private_key: PathBuf,
/// Public key for validating JWTs.
#[arg(short, long, value_name = "FILE")]
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>,
#[arg(short, long)]
/// Secret token that's part of the URL for authentication.
#[arg(short, long, value_name = "TOKEN")]
validation_token: String,
}
@ -82,6 +97,7 @@ async fn main() -> Result<()> {
let router = Router::new()
.route("/", post(generation_handler))
.with_state(GenerationState {
server_fingerprint: Arc::new(args.server_fingerprint),
encoding_key,
audiences: Arc::new(audiences.iter().cloned().collect()),
})
@ -103,16 +119,16 @@ async fn generation_handler(
Json(payload): Json<GenerationRequest>,
) -> impl IntoResponse {
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) {
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();
hmac.update(payload.user.as_bytes());
if hmac.verify_slice(&password).is_err() {
return StatusCode::FORBIDDEN.into_response();
return (StatusCode::FORBIDDEN, "invalid payload").into_response();
}
let claims = AuthenticationClaims {
aud: payload.audience,
@ -125,7 +141,11 @@ async fn generation_handler(
&state.encoding_key,
)
.unwrap();
jwt.into_response()
Json(GenerationResponse {
jwt,
server_fingerprint: state.server_fingerprint.as_ref().clone(),
})
.into_response()
}
async fn validation_handler(
@ -138,11 +158,11 @@ async fn validation_handler(
&state.validation,
) {
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() {
StatusCode::OK
StatusCode::OK.into_response()
} else {
StatusCode::FORBIDDEN
(StatusCode::FORBIDDEN, "invalid auth").into_response()
}
}