fix(opaque): use safe url base64
This commit is contained in:
@@ -45,7 +45,21 @@
|
|||||||
//! because subsequent hurl files don't assume `hasOpaque=false`.
|
//! because subsequent hurl files don't assume `hasOpaque=false`.
|
||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
use base64::engine::general_purpose::{
|
||||||
|
STANDARD as B64, URL_SAFE_NO_PAD as B64_URL_NO_PAD,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Decode base64 emitted by the server. The server emits URL-safe-no-pad
|
||||||
|
/// (matching what the SPA's WASM client expects); this helper accepts
|
||||||
|
/// both flavours so a future format change on either side doesn't
|
||||||
|
/// silently break the round-trip. Mirrors `decode_opaque_b64` in the
|
||||||
|
/// server-side handler.
|
||||||
|
fn decode_opaque_b64(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
B64_URL_NO_PAD
|
||||||
|
.decode(trimmed)
|
||||||
|
.or_else(|_| B64.decode(trimmed))
|
||||||
|
}
|
||||||
use opaque_ke::{
|
use opaque_ke::{
|
||||||
ClientLogin, ClientLoginFinishParameters, ClientRegistration,
|
ClientLogin, ClientLoginFinishParameters, ClientRegistration,
|
||||||
ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse,
|
ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse,
|
||||||
@@ -213,7 +227,7 @@ async fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
Err(e) => return fail(format!("register/start network: {e}")),
|
Err(e) => return fail(format!("register/start network: {e}")),
|
||||||
};
|
};
|
||||||
let reg_response_bytes = match B64.decode(reg_start.registration_response.trim()) {
|
let reg_response_bytes = match decode_opaque_b64(®_start.registration_response) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => return fail(format!("decode registration_response: {e}")),
|
Err(e) => return fail(format!("decode registration_response: {e}")),
|
||||||
};
|
};
|
||||||
@@ -280,7 +294,7 @@ async fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
Err(e) => return fail(format!("login/ke1 network: {e}")),
|
Err(e) => return fail(format!("login/ke1 network: {e}")),
|
||||||
};
|
};
|
||||||
let cred_bytes = match B64.decode(ke1.login_response.trim()) {
|
let cred_bytes = match decode_opaque_b64(&ke1.login_response) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => return fail(format!("decode loginResponse: {e}")),
|
Err(e) => return fail(format!("decode loginResponse: {e}")),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,11 +30,15 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Payload encoding
|
//! ## Payload encoding
|
||||||
//!
|
//!
|
||||||
//! All OPAQUE messages are opaque byte blobs. We serialise them as
|
//! All OPAQUE messages are opaque byte blobs, round-tripped through
|
||||||
//! **standard base64** (not URL-safe, no padding-strip) because the
|
//! `serde_json` as a `String` field. The server emits **URL-safe-no-pad**
|
||||||
//! WASM client (`@serenity-kit/opaque`) emits the same shape and both
|
//! base64 (`-`/`_`, no `=`) via `B64.encode(...)` — the WASM client
|
||||||
//! ends need to agree on one flavour. Round-tripped through
|
//! (`@serenity-kit/opaque`) rejects standard base64 with an
|
||||||
//! `serde_json` as a `String` field.
|
//! `Invalid symbol` error on the first `+`/`/`. On decode the server
|
||||||
|
//! accepts BOTH flavours via `decode_opaque_b64` so the Rust
|
||||||
|
//! `opaque-hurl-helper` test binary (which emits standard) still
|
||||||
|
//! round-trips. Asymmetry is intentional: URL-safe is the compatible
|
||||||
|
//! superset for the client mix we support.
|
||||||
//!
|
//!
|
||||||
//! ## Ciphersuite version handshake
|
//! ## Ciphersuite version handshake
|
||||||
//!
|
//!
|
||||||
@@ -64,7 +68,32 @@ use axum::http::StatusCode;
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
use base64::engine::general_purpose::{
|
||||||
|
STANDARD as B64_STANDARD, URL_SAFE_NO_PAD as B64_URL_NO_PAD,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Decode base64 payloads received from OPAQUE clients, accepting BOTH
|
||||||
|
/// standard (`+`/`/`, padded) and URL-safe-no-pad (`-`/`_`, no padding)
|
||||||
|
/// alphabets. `@serenity-kit/opaque` (the WASM client the SPA uses)
|
||||||
|
/// emits URL-safe-no-pad; the `opaque-hurl-helper` binary and the
|
||||||
|
/// original spec docs use standard. Accepting both means neither
|
||||||
|
/// side has to renormalize.
|
||||||
|
fn decode_opaque_b64(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
B64_STANDARD
|
||||||
|
.decode(trimmed)
|
||||||
|
.or_else(|_| B64_URL_NO_PAD.decode(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode OPAQUE payloads emitted BY the server. Emits **URL-safe-no-pad**
|
||||||
|
/// (`-`/`_`, no `=`) because the WASM client (`@serenity-kit/opaque` —
|
||||||
|
/// the SPA's OPAQUE library) decodes strictly as URL-safe-no-pad and
|
||||||
|
/// rejects standard base64 with an `Invalid symbol` error on the first
|
||||||
|
/// `+`/`/` in the payload. Rust `opaque-hurl-helper` and any other
|
||||||
|
/// client parses through `decode_opaque_b64` above which accepts BOTH
|
||||||
|
/// flavours, so this direction is asymmetric on purpose — URL-safe is
|
||||||
|
/// the compatible superset for the client mix we support.
|
||||||
|
const B64: base64::engine::general_purpose::GeneralPurpose = B64_URL_NO_PAD;
|
||||||
use opaque_ke::{
|
use opaque_ke::{
|
||||||
CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload,
|
CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload,
|
||||||
ServerLoginStartParameters, ServerRegistration,
|
ServerLoginStartParameters, ServerRegistration,
|
||||||
@@ -186,11 +215,28 @@ pub async fn register_start(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let svc = require_opaque_service(&state)?;
|
let svc = require_opaque_service(&state)?;
|
||||||
|
|
||||||
let req_bytes = B64
|
let req_bytes = decode_opaque_b64(&dto.registration_request).map_err(|e| {
|
||||||
.decode(dto.registration_request.trim())
|
tracing::info!(
|
||||||
.map_err(|_| malformed("registrationRequest is not valid base64"))?;
|
target: "audit",
|
||||||
let req = RegistrationRequest::<OxiCloudSuite>::deserialize(&req_bytes)
|
event = "opaque.register_start_rejected",
|
||||||
.map_err(|_| malformed("registrationRequest failed to deserialize"))?;
|
reason = "malformed_base64",
|
||||||
|
user_id = %user_id,
|
||||||
|
error = %e,
|
||||||
|
"👮🏻♂️ OPAQUE register/start rejected: registrationRequest is not valid base64"
|
||||||
|
);
|
||||||
|
malformed("registrationRequest is not valid base64")
|
||||||
|
})?;
|
||||||
|
let req = RegistrationRequest::<OxiCloudSuite>::deserialize(&req_bytes).map_err(|e| {
|
||||||
|
tracing::info!(
|
||||||
|
target: "audit",
|
||||||
|
event = "opaque.register_start_rejected",
|
||||||
|
reason = "malformed_registration_request",
|
||||||
|
user_id = %user_id,
|
||||||
|
error = %e,
|
||||||
|
"👮🏻♂️ OPAQUE register/start rejected: RegistrationRequest deserialize failed"
|
||||||
|
);
|
||||||
|
malformed("registrationRequest failed to deserialize")
|
||||||
|
})?;
|
||||||
|
|
||||||
// `user_id` (a UUID) is the OPAQUE server-side user identifier.
|
// `user_id` (a UUID) is the OPAQUE server-side user identifier.
|
||||||
// Encoded as the UUID's raw bytes so the same identifier bytes
|
// Encoded as the UUID's raw bytes so the same identifier bytes
|
||||||
@@ -270,8 +316,7 @@ pub async fn register_finish(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let record_bytes = B64
|
let record_bytes = decode_opaque_b64(&dto.registration_record)
|
||||||
.decode(dto.registration_record.trim())
|
|
||||||
.map_err(|_| malformed("registrationRecord is not valid base64"))?;
|
.map_err(|_| malformed("registrationRecord is not valid base64"))?;
|
||||||
let record = RegistrationUpload::<OxiCloudSuite>::deserialize(&record_bytes)
|
let record = RegistrationUpload::<OxiCloudSuite>::deserialize(&record_bytes)
|
||||||
.map_err(|_| malformed("registrationRecord failed to deserialize"))?;
|
.map_err(|_| malformed("registrationRecord failed to deserialize"))?;
|
||||||
@@ -464,8 +509,7 @@ pub async fn login_ke1(
|
|||||||
let repo = require_opaque_repo(&state)?;
|
let repo = require_opaque_repo(&state)?;
|
||||||
let exchange = require_opaque_exchange(&state)?;
|
let exchange = require_opaque_exchange(&state)?;
|
||||||
|
|
||||||
let cred_bytes = B64
|
let cred_bytes = decode_opaque_b64(&dto.start_login_request)
|
||||||
.decode(dto.start_login_request.trim())
|
|
||||||
.map_err(|_| malformed("startLoginRequest is not valid base64"))?;
|
.map_err(|_| malformed("startLoginRequest is not valid base64"))?;
|
||||||
let cred_request = CredentialRequest::<OxiCloudSuite>::deserialize(&cred_bytes)
|
let cred_request = CredentialRequest::<OxiCloudSuite>::deserialize(&cred_bytes)
|
||||||
.map_err(|_| malformed("startLoginRequest failed to deserialize"))?;
|
.map_err(|_| malformed("startLoginRequest failed to deserialize"))?;
|
||||||
@@ -596,8 +640,7 @@ pub async fn login_ke3(
|
|||||||
invalid_credentials()
|
invalid_credentials()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let cred_bytes = B64
|
let cred_bytes = decode_opaque_b64(&dto.finish_login_request)
|
||||||
.decode(dto.finish_login_request.trim())
|
|
||||||
.map_err(|_| malformed("finishLoginRequest is not valid base64"))?;
|
.map_err(|_| malformed("finishLoginRequest is not valid base64"))?;
|
||||||
let cred_final = CredentialFinalization::<OxiCloudSuite>::deserialize(&cred_bytes)
|
let cred_final = CredentialFinalization::<OxiCloudSuite>::deserialize(&cred_bytes)
|
||||||
.map_err(|_| malformed("finishLoginRequest failed to deserialize"))?;
|
.map_err(|_| malformed("finishLoginRequest failed to deserialize"))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user