Files
kestrove-core/crates/kv-proto/src/opaque.rs
T
samiandClaude Opus 5.5 48c7e32d33 kv-proto: OPAQUE suite, RFC 9421 signing profile and M1 auth wire types
Adds the OpaqueSuite (Ristretto255 OPRF, TripleDH, SHA-512, identity KSF)
shared by client and server, the httpsig signature-base builders, and the
setup/credentials/account/device/SSE types for milestone M1.
Cargo.lock changes only for kv-proto's new dependencies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 21:22:03 +09:00

79 lines
3.6 KiB
Rust

//! The OPAQUE profile used for login, setup and password change (docs/02 "Authentication to the server").
//!
//! Client and server MUST both use [`OpaqueSuite`] with these conventions, or login fails:
//! * password input: the 32-byte `AUTH_INPUT`, which the client has already stretched with Argon2id,
//! so the suite's key-stretching function is the identity;
//! * credential identifier (server side only): the 16 raw bytes of the account UUID;
//! * default parameters everywhere (`ClientRegistrationFinishParameters::default()`,
//! `ClientLoginFinishParameters::default()`, `ServerLoginStartParameters::default()`):
//! no custom identifiers and no context string.
//!
//! Messages travel base64url-encoded (no padding) inside the JSON bodies in this crate, using
//! opaque-ke's own `serialize()` / `deserialize()`.
pub use opaque_ke;
/// Ristretto255 OPRF, Ristretto255 + TripleDH key exchange, SHA-512, identity KSF.
pub struct OpaqueSuite;
impl opaque_ke::CipherSuite for OpaqueSuite {
type OprfCs = opaque_ke::Ristretto255;
type KeGroup = opaque_ke::Ristretto255;
type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh;
type Ksf = opaque_ke::ksf::Identity;
}
#[cfg(test)]
mod tests {
use opaque_ke::{
ClientLogin, ClientLoginFinishParameters, ClientRegistration, ClientRegistrationFinishParameters,
CredentialFinalization, CredentialRequest, CredentialResponse, RegistrationRequest, RegistrationResponse,
RegistrationUpload, ServerLogin, ServerLoginStartParameters, ServerRegistration, ServerSetup,
};
use rand_core::OsRng;
use super::OpaqueSuite;
/// Registration then login across serialized messages, as the client and server exchange them.
#[test]
fn registration_and_login_roundtrip() {
let mut rng = OsRng;
let setup = ServerSetup::<OpaqueSuite>::new(&mut rng);
let cred_id = uuid::Uuid::new_v4();
let auth_input = [7u8; 32];
let c = ClientRegistration::<OpaqueSuite>::start(&mut rng, &auth_input).unwrap();
let req = RegistrationRequest::deserialize(&c.message.serialize()).unwrap();
let s = ServerRegistration::<OpaqueSuite>::start(&setup, req, cred_id.as_bytes()).unwrap();
let resp = RegistrationResponse::deserialize(&s.message.serialize()).unwrap();
let fin = c.state.finish(&mut rng, &auth_input, resp, ClientRegistrationFinishParameters::default()).unwrap();
let upload = RegistrationUpload::<OpaqueSuite>::deserialize(&fin.message.serialize()).unwrap();
let record = ServerRegistration::finish(upload).serialize();
let login = |password: &[u8]| {
let c = ClientLogin::<OpaqueSuite>::start(&mut OsRng, password).unwrap();
let ke1 = CredentialRequest::deserialize(&c.message.serialize()).unwrap();
let record = ServerRegistration::deserialize(&record).unwrap();
let s = ServerLogin::start(
&mut OsRng,
&setup,
Some(record),
ke1,
cred_id.as_bytes(),
ServerLoginStartParameters::default(),
)
.unwrap();
let ke2 = CredentialResponse::deserialize(&s.message.serialize()).unwrap();
let Ok(done) = c.state.finish(password, ke2, ClientLoginFinishParameters::default()) else {
return false;
};
let ke3 = CredentialFinalization::deserialize(&done.message.serialize()).unwrap();
let server = s.state.finish(ke3).unwrap();
server.session_key == done.session_key
};
assert!(login(&auth_input));
assert!(!login(&[8u8; 32]));
}
}