From e3a16be2f93552dd12b6a1517f3d0e6904608d62 Mon Sep 17 00:00:00 2001 From: Alex-Emmet Date: Wed, 30 Sep 2026 20:45:19 +0200 Subject: [PATCH] Use shared OPAQUE password authentication --- Cargo.lock | 35 +- iota-auth/Cargo.toml | 6 - iota-auth/src/lib.rs | 1 - iota-auth/src/password.rs | 481 ------------------ omikron-connector/Cargo.toml | 5 +- omikron-connector/src/password/management.rs | 10 +- omikron-connector/src/password/mod.rs | 150 ++---- .../src/password/provisioning.rs | 62 +-- omikron-connector/src/password/runtime.rs | 15 +- omikron-connector/src/password/wire.rs | 7 +- 10 files changed, 115 insertions(+), 657 deletions(-) delete mode 100644 iota-auth/src/password.rs diff --git a/Cargo.lock b/Cargo.lock index 2dc0b0b..7d32cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2229,19 +2229,13 @@ dependencies = [ name = "iota-auth" version = "0.1.0" dependencies = [ - "argon2", "async-trait", "dashmap", - "generic-array", "iota-identity", "mtp", - "opaque-ke", "rand_core 0.6.4", - "sha2 0.10.9", - "thiserror 2.0.20", "tokio", "uuid", - "zeroize", ] [[package]] @@ -3299,7 +3293,7 @@ dependencies = [ "iota-util", "json", "mtp", - "opaque-ke", + "opague-integration", "rand_core 0.6.4", "reqwest", "serde", @@ -3311,7 +3305,6 @@ dependencies = [ "trust-dns-resolver", "url", "uuid", - "zeroize", ] [[package]] @@ -3326,6 +3319,32 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opague-integration" +version = "0.1.0" +source = "git+https://git.methanium.net/tensamin/opaque-integration.git?rev=d996c29366429160103e2a3f99c3e435522456f0#d996c29366429160103e2a3f99c3e435522456f0" +dependencies = [ + "opague-integration-core", +] + +[[package]] +name = "opague-integration-core" +version = "0.1.0" +source = "git+https://git.methanium.net/tensamin/opaque-integration.git?rev=d996c29366429160103e2a3f99c3e435522456f0#d996c29366429160103e2a3f99c3e435522456f0" +dependencies = [ + "argon2", + "chacha20poly1305", + "generic-array", + "getrandom 0.2.17", + "hkdf 0.12.4", + "opaque-ke", + "rand_core 0.6.4", + "sha2 0.10.9", + "thiserror 2.0.20", + "uuid", + "zeroize", +] + [[package]] name = "opaque-debug" version = "0.3.1" diff --git a/iota-auth/Cargo.toml b/iota-auth/Cargo.toml index a99188e..9a135da 100644 --- a/iota-auth/Cargo.toml +++ b/iota-auth/Cargo.toml @@ -9,12 +9,6 @@ dashmap = "6.2.1" iota-identity = { path = "../iota-identity" } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "bb0f682b735de5ebb36bb41dc699260578341828", features = ["crypto"] } rand_core = { version = "0.6", features = ["getrandom", "std"] } -opaque-ke = { version = "4.0.1", features = ["argon2"] } -argon2 = "0.5" -generic-array = "0.14" -zeroize = "1" -sha2 = "0.10" -thiserror = "2" uuid = { version = "*", features = ["v4"] } [dev-dependencies] diff --git a/iota-auth/src/lib.rs b/iota-auth/src/lib.rs index 2416be9..bf6e334 100644 --- a/iota-auth/src/lib.rs +++ b/iota-auth/src/lib.rs @@ -1,4 +1,3 @@ -pub mod password; mod principal_auth; mod session; diff --git a/iota-auth/src/password.rs b/iota-auth/src/password.rs deleted file mode 100644 index 856b517..0000000 --- a/iota-auth/src/password.rs +++ /dev/null @@ -1,481 +0,0 @@ -use generic_array::{ArrayLength, GenericArray}; -use mtp::crypto::PublicKeyBundle; -use opaque_ke::ksf::Ksf; -use opaque_ke::{ - CipherSuite, CredentialFinalization, CredentialRequest, Identifiers, RegistrationRequest, - RegistrationUpload, Ristretto255, ServerLogin, ServerLoginParameters, ServerRegistration, - ServerSetup, TripleDh, -}; -use rand_core::OsRng; -use sha2::{Digest, Sha256}; -use zeroize::Zeroizing; - -pub struct OpaqueProfileV1; - -impl OpaqueProfileV1 { - pub const ID: i64 = 1; - pub const SERVER_ID_DOMAIN: &'static [u8] = b"tensamin:iota-password\0"; - pub const LOGIN_CONTEXT_DOMAIN: &'static [u8] = b"tensamin:password-provisioning\0"; - - pub fn server_identifier(iota_id: i64) -> Vec { - let mut result = Self::SERVER_ID_DOMAIN.to_vec(); - result.extend_from_slice(&iota_id.to_be_bytes()); - result - } - - pub fn login_context( - principal: &str, - iota_id: i64, - session_id: uuid::Uuid, - contact_key: &PublicKeyBundle, - ) -> Result, PasswordAuthError> { - let mut result = Self::LOGIN_CONTEXT_DOMAIN.to_vec(); - let principal_len = - u32::try_from(principal.len()).map_err(|_| PasswordAuthError::FieldTooLarge)?; - result.extend_from_slice(&principal_len.to_be_bytes()); - result.extend_from_slice(principal.as_bytes()); - result.extend_from_slice(&iota_id.to_be_bytes()); - result.extend_from_slice(session_id.as_bytes()); - let key = contact_key - .try_as_bytes() - .map_err(|error| PasswordAuthError::ContactKey(error.to_string()))?; - result.extend_from_slice(&Sha256::digest(key)); - Ok(result) - } -} - -pub const CURRENT_OPAQUE_PROFILE: i64 = OpaqueProfileV1::ID; - -/// KSF parameters are persistent semantics of OpaqueProfileV1. Do not change deployed profile-1 credentials. -pub struct PasswordKsfV1(argon2::Argon2<'static>); - -impl Default for PasswordKsfV1 { - fn default() -> Self { - let params = argon2::Params::new(19 * 1024, 2, 1, None) - .expect("PasswordKsfV1 parameters must be valid"); - Self(argon2::Argon2::new( - argon2::Algorithm::Argon2id, - argon2::Version::V0x13, - params, - )) - } -} - -impl Ksf for PasswordKsfV1 { - fn hash>( - &self, - input: GenericArray, - ) -> Result, opaque_ke::errors::InternalError> { - let mut output = GenericArray::::default(); - self.0 - .hash_password_into(&input, &[0; argon2::RECOMMENDED_SALT_LEN], &mut output) - .map_err(|_| opaque_ke::errors::InternalError::KsfError)?; - Ok(output) - } -} - -pub type TensaminOpaque = OpaqueProfileV1; - -impl CipherSuite for OpaqueProfileV1 { - type OprfCs = Ristretto255; - type KeyExchange = TripleDh; - type Ksf = PasswordKsfV1; -} - -pub type PasswordServerSetup = ServerSetup; - -#[derive(Debug, thiserror::Error)] -pub enum PasswordAuthError { - #[error("invalid OPAQUE exchange: {0}")] - Opaque(String), - #[error("invalid Contact public key: {0}")] - ContactKey(String), - #[error("OPAQUE context field is too large")] - FieldTooLarge, -} - -pub fn generate_server_setup() -> PasswordServerSetup { - ServerSetup::new(&mut OsRng) -} - -pub fn serialize_server_setup(setup: &PasswordServerSetup) -> Vec { - setup.serialize().to_vec() -} - -pub fn deserialize_server_setup(bytes: &[u8]) -> Result { - ServerSetup::deserialize(bytes).map_err(|error| PasswordAuthError::Opaque(error.to_string())) -} - -pub fn deserialize_server_setup_owned( - bytes: Vec, -) -> Result { - let bytes = Zeroizing::new(bytes); - deserialize_server_setup(&bytes) -} - -pub fn registration_start( - setup: &PasswordServerSetup, - request: &[u8], - identifier: &[u8], -) -> Result, PasswordAuthError> { - let message = RegistrationRequest::::deserialize(request) - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - let result = ServerRegistration::::start(setup, message, identifier) - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - Ok(result.message.serialize().to_vec()) -} - -pub fn registration_finish(upload: &[u8]) -> Result, PasswordAuthError> { - let message = RegistrationUpload::::deserialize(upload) - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - Ok(ServerRegistration::::finish(message) - .serialize() - .to_vec()) -} - -pub fn validate_login_request(request: &[u8]) -> Result<(), PasswordAuthError> { - CredentialRequest::::deserialize(request) - .map(|_| ()) - .map_err(|error| PasswordAuthError::Opaque(error.to_string())) -} - -pub fn server_identifier(iota_id: i64) -> Vec { - OpaqueProfileV1::server_identifier(iota_id) -} - -pub fn login_context( - principal: &str, - iota_id: i64, - session_id: uuid::Uuid, - contact_key: &PublicKeyBundle, -) -> Result, PasswordAuthError> { - OpaqueProfileV1::login_context(principal, iota_id, session_id, contact_key) -} - -pub struct LoginStartResult { - pub response: Vec, - pub state: SerializedLoginState, -} - -pub type SerializedLoginState = Zeroizing>; - -pub fn login_start( - setup: &PasswordServerSetup, - record: Option<&[u8]>, - request: &[u8], - principal: &[u8], - server_id: &[u8], - context: &[u8], -) -> Result { - let message = CredentialRequest::::deserialize(request) - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - let registration = record - .map(ServerRegistration::::deserialize) - .transpose() - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - let result = ServerLogin::::start( - &mut OsRng, - setup, - registration, - message, - principal, - ServerLoginParameters { - context: Some(context), - identifiers: Identifiers { - client: Some(principal), - server: Some(server_id), - }, - }, - ) - .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; - Ok(LoginStartResult { - response: result.message.serialize().to_vec(), - state: Zeroizing::new(result.state.serialize().to_vec()), - }) -} - -#[derive(Debug, thiserror::Error)] -pub enum LoginFinishError { - #[error("invalid OPAQUE finalization message")] - InvalidMessage, - #[error("OPAQUE authentication failed")] - AuthenticationFailed, -} - -pub fn login_finish( - serialized_state: &[u8], - finalization: &[u8], - principal: &[u8], - server_id: &[u8], - context: &[u8], -) -> Result<(), LoginFinishError> { - let state = ServerLogin::::deserialize(serialized_state) - .map_err(|_| LoginFinishError::InvalidMessage)?; - let message = CredentialFinalization::::deserialize(finalization) - .map_err(|_| LoginFinishError::InvalidMessage)?; - state - .finish( - message, - ServerLoginParameters { - context: Some(context), - identifiers: Identifiers { - client: Some(principal), - server: Some(server_id), - }, - }, - ) - .map_err(|_| LoginFinishError::AuthenticationFailed)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use mtp::crypto::Keyring; - use opaque_ke::{ - ClientLogin, ClientLoginFinishParameters, ClientRegistration, - ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse, - }; - use uuid::Uuid; - - #[test] - fn profile_one_persistent_semantics_are_stable() { - assert_eq!(OpaqueProfileV1::ID, 1); - assert_eq!( - OpaqueProfileV1::SERVER_ID_DOMAIN, - b"tensamin:iota-password\0" - ); - assert_eq!( - OpaqueProfileV1::LOGIN_CONTEXT_DOMAIN, - b"tensamin:password-provisioning\0" - ); - assert_eq!( - OpaqueProfileV1::server_identifier(11), - [ - b"tensamin:iota-password\0".as_slice(), - &11_i64.to_be_bytes() - ] - .concat() - ); - let input = GenericArray::from([7_u8; 64]); - let pinned = PasswordKsfV1::default().hash(input.clone()).unwrap(); - let previous = argon2::Argon2::default().hash(input).unwrap(); - assert_eq!(pinned, previous); - } - - #[test] - fn malformed_finalization_is_not_an_authentication_failure() { - assert!(matches!( - login_finish(b"invalid", b"invalid", b"account", b"server", b"context"), - Err(LoginFinishError::InvalidMessage) - )); - } - - #[test] - fn login_request_validation_rejects_arbitrary_bytes() { - assert!(validate_login_request(b"not a KE1").is_err()); - let request = ClientLogin::::start(&mut OsRng, b"password").unwrap(); - assert!(validate_login_request(&request.message.serialize()).is_ok()); - } - - #[test] - fn registration_and_login_bind_context_identifiers_and_persisted_setup() { - let setup = generate_server_setup(); - let restored = deserialize_server_setup(&serialize_server_setup(&setup)).unwrap(); - let principal = b"omega-key:example#7"; - let server = server_identifier(11); - let identifiers = Identifiers { - client: Some(principal), - server: Some(&server), - }; - let registration = - ClientRegistration::::start(&mut OsRng, b"correct horse").unwrap(); - let response = - registration_start(&restored, ®istration.message.serialize(), principal).unwrap(); - let upload = registration - .state - .finish( - &mut OsRng, - b"correct horse", - RegistrationResponse::::deserialize(&response).unwrap(), - ClientRegistrationFinishParameters::new(identifiers, None), - ) - .unwrap(); - let record = registration_finish(&upload.message.serialize()).unwrap(); - let key = Keyring::generate().public_key_bundle(); - let session = Uuid::new_v4(); - let context = login_context("omega-key:example#7", 11, session, &key).unwrap(); - - let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); - let result = login_start( - &restored, - Some(&record), - &client.message.serialize(), - principal, - &server, - &context, - ) - .unwrap(); - let ke2 = CredentialResponse::::deserialize(&result.response).unwrap(); - let finish = client - .state - .finish( - &mut OsRng, - b"correct horse", - ke2, - ClientLoginFinishParameters::new(Some(&context), identifiers, None), - ) - .unwrap(); - assert!( - login_finish( - &result.state, - &finish.message.serialize(), - principal, - &server, - &context - ) - .is_ok() - ); - for (binding, changed_context) in [ - ( - "session", - login_context("omega-key:example#7", 11, Uuid::new_v4(), &key).unwrap(), - ), - ( - "contact key", - login_context( - "omega-key:example#7", - 11, - session, - &Keyring::generate().public_key_bundle(), - ) - .unwrap(), - ), - ( - "account", - login_context("omega-key:example#8", 11, session, &key).unwrap(), - ), - ( - "Iota", - login_context("omega-key:example#7", 12, session, &key).unwrap(), - ), - ] { - let client = - ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); - let mismatched = login_start( - &restored, - Some(&record), - &client.message.serialize(), - principal, - &server, - &changed_context, - ) - .unwrap(); - assert!( - client - .state - .finish( - &mut OsRng, - b"correct horse", - CredentialResponse::::deserialize(&mismatched.response) - .unwrap(), - ClientLoginFinishParameters::new(Some(&context), identifiers, None), - ) - .is_err(), - "login accepted a different {binding}" - ); - } - - let wrong_server = server_identifier(12); - for (login_principal, login_server, password) in [ - ( - b"omega-key:example#8".as_slice(), - server.as_slice(), - b"correct horse".as_slice(), - ), - ( - principal.as_slice(), - wrong_server.as_slice(), - b"correct horse".as_slice(), - ), - ( - principal.as_slice(), - server.as_slice(), - b"wrong horse".as_slice(), - ), - ] { - let client = ClientLogin::::start(&mut OsRng, password).unwrap(); - let result = login_start( - &restored, - Some(&record), - &client.message.serialize(), - login_principal, - login_server, - &context, - ) - .unwrap(); - assert!( - client - .state - .finish( - &mut OsRng, - password, - CredentialResponse::::deserialize(&result.response) - .unwrap(), - ClientLoginFinishParameters::new(Some(&context), identifiers, None) - ) - .is_err() - ); - } - - let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); - let dummy = login_start( - &restored, - None, - &client.message.serialize(), - principal, - &server, - &context, - ) - .unwrap(); - assert!(CredentialResponse::::deserialize(&dummy.response).is_ok()); - let unrelated = generate_server_setup(); - let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); - let response = login_start( - &unrelated, - Some(&record), - &client.message.serialize(), - principal, - &server, - &context, - ) - .unwrap(); - assert!( - client - .state - .finish( - &mut OsRng, - b"correct horse", - CredentialResponse::::deserialize(&response.response).unwrap(), - ClientLoginFinishParameters::new(Some(&context), identifiers, None) - ) - .is_err() - ); - } - - #[test] - fn login_context_binds_account_iota_session_and_contact_key() { - let first = Keyring::generate().public_key_bundle(); - let second = Keyring::generate().public_key_bundle(); - let session = Uuid::new_v4(); - let original = login_context("omega-key:example#7", 11, session, &first).unwrap(); - for changed in [ - login_context("omega-key:example#8", 11, session, &first).unwrap(), - login_context("omega-key:example#7", 12, session, &first).unwrap(), - login_context("omega-key:example#7", 11, Uuid::new_v4(), &first).unwrap(), - login_context("omega-key:example#7", 11, session, &second).unwrap(), - ] { - assert_ne!(original, changed); - } - } -} diff --git a/omikron-connector/Cargo.toml b/omikron-connector/Cargo.toml index a8936d2..de29224 100644 --- a/omikron-connector/Cargo.toml +++ b/omikron-connector/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +opague-integration = { git = "https://git.methanium.net/tensamin/opaque-integration.git", rev = "d996c29366429160103e2a3f99c3e435522456f0" } async-trait = "0.1.89" iota-connection = { path = "../iota-connection" } iota-auth = { path = "../iota-auth" } @@ -34,7 +35,3 @@ base64 = "0.22.1" rand_core = { version = "0.6", features = ["getrandom", "std"] } trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] } url = "2" -zeroize = "1" - -[dev-dependencies] -opaque-ke = { version = "4.0.1", features = ["argon2"] } diff --git a/omikron-connector/src/password/management.rs b/omikron-connector/src/password/management.rs index 84cc120..3b592e6 100644 --- a/omikron-connector/src/password/management.rs +++ b/omikron-connector/src/password/management.rs @@ -1,11 +1,11 @@ use std::{sync::Arc, time::Instant}; -use iota_auth::password; use iota_storage::users::{ password_credentials::{self, PasswordCredential}, user_manager, }; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use opague_integration::opaque; use uuid::Uuid; use super::{ @@ -48,7 +48,7 @@ impl OmikronConnection { .password_auth .setup() .map_err(|error| error.to_string())?; - let response = password::registration_start(&setup, &request, principal.as_bytes()) + let response = opaque::server_registration_start(&setup, &request, principal.as_bytes()) .map_err(|error| error.to_string())?; let enrollment_id = Uuid::new_v4(); self.password_auth @@ -108,7 +108,7 @@ impl OmikronConnection { MAX_CREDENTIAL_BYTES, )?; let opaque_record = - password::registration_finish(&upload).map_err(|error| error.to_string())?; + opaque::server_registration_finish(&upload).map_err(|error| error.to_string())?; let profile = user_manager::get_user(user_id) .map_err(|error| error.to_string())? .ok_or("account not hosted")?; @@ -116,7 +116,7 @@ impl OmikronConnection { let now = now_millis() as i64; password_credentials::upsert(&PasswordCredential { user_id, - opaque_profile: password::CURRENT_OPAQUE_PROFILE, + opaque_profile: opaque::CURRENT_OPAQUE_PROFILE, opaque_record, encrypted_tu_credential: encrypted, account_public_key_sha256: fingerprint, @@ -148,7 +148,7 @@ impl OmikronConnection { let enabled = password_credentials::get(user_id) .map_err(|error| error.to_string())? .is_some_and(|credential| { - credential.opaque_profile == password::CURRENT_OPAQUE_PROFILE + credential.opaque_profile == opaque::CURRENT_OPAQUE_PROFILE }); self.protected_response( frame, diff --git a/omikron-connector/src/password/mod.rs b/omikron-connector/src/password/mod.rs index 20016b0..c471560 100644 --- a/omikron-connector/src/password/mod.rs +++ b/omikron-connector/src/password/mod.rs @@ -3,8 +3,6 @@ use std::time::Duration; use iota_storage::users::user_manager; use mtp::crypto::PublicKeyBundle; -#[cfg(test)] -use iota_auth::password; #[cfg(test)] use mtp::codec::{ CommunicationType, CommunicationValue, DataType, DataValue, ProtectedMessageBuilder, @@ -13,15 +11,13 @@ use mtp::codec::{ #[cfg(test)] use mtp::crypto::DualSigner; #[cfg(test)] -use rand_core::OsRng; +use opague_integration::opaque::{self, PasswordLoginBindingV1}; #[cfg(test)] use sha2::{Digest, Sha256}; #[cfg(test)] use std::time::Instant; #[cfg(test)] use uuid::Uuid; -#[cfg(test)] -use zeroize::Zeroizing; use crate::omikron_connection::OmikronConnection; @@ -107,10 +103,6 @@ impl OmikronConnection { mod tests { use super::*; use mtp::{codec::InMemoryReplayGuard, crypto::Keyring}; - use opaque_ke::{ - ClientLogin, ClientLoginFinishParameters, ClientRegistration, - ClientRegistrationFinishParameters, CredentialResponse, Identifiers, RegistrationResponse, - }; #[test] fn damaged_setup_stays_unavailable() { @@ -144,8 +136,7 @@ mod tests { user_id, participant_id: 1, contact_key: Keyring::generate().public_key_bundle(), - context: vec![], - state: Zeroizing::new(vec![]), + state: None, real_record: false, record_hash: None, created, @@ -275,66 +266,37 @@ mod tests { #[test] fn wrong_password_client_stops_after_ke2_and_expiry_consumes_budget() { - let setup = password::generate_server_setup(); - let principal = b"omega-key:example#7"; - let server = password::server_identifier(11); - let identifiers = Identifiers { - client: Some(principal), - server: Some(&server), - }; - let registration = - ClientRegistration::::start(&mut OsRng, b"correct password") - .unwrap(); + let setup = opaque::generate_server_setup(); + let principal = "omega-key:example#7"; + let registration = opaque::client_registration_start(b"correct password").unwrap(); let response = - password::registration_start(&setup, ®istration.message.serialize(), principal) + opaque::server_registration_start(&setup, ®istration.request, principal.as_bytes()) .unwrap(); - let upload = registration - .state - .finish( - &mut OsRng, - b"correct password", - RegistrationResponse::::deserialize(&response).unwrap(), - ClientRegistrationFinishParameters::new(identifiers, None), - ) - .unwrap(); - let record = password::registration_finish(&upload.message.serialize()).unwrap(); + let upload = registration.state.finish(&response, principal, 11).unwrap(); + let record = opaque::server_registration_finish(&upload.upload).unwrap(); let runtime = limited_runtime(); let id = Uuid::new_v4(); let contact_key = Keyring::generate().public_key_bundle(); - let context = password::login_context("omega-key:example#7", 11, id, &contact_key).unwrap(); - let client = - ClientLogin::::start(&mut OsRng, b"wrong password").unwrap(); - let ke2 = password::login_start( - &setup, - Some(&record), - &client.message.serialize(), + let fingerprint = wire::public_key_fingerprint(&contact_key).unwrap(); + let binding = PasswordLoginBindingV1 { principal, - &server, - &context, - ) - .unwrap(); + iota_id: 11, + session_id: id, + contact_key_sha256: &fingerprint, + }; + let client = opaque::client_login_start(b"wrong password").unwrap(); + let ke2 = + opaque::server_login_start(&setup, Some(&record), &client.request, &binding).unwrap(); let (attempt, _) = runtime.begin_attempt(7).unwrap(); - assert!( - client - .state - .finish( - &mut OsRng, - b"wrong password", - CredentialResponse::::deserialize(&ke2.response) - .unwrap(), - ClientLoginFinishParameters::new(Some(&context), identifiers, None), - ) - .is_err() - ); + assert!(client.state.finish(&ke2.response, &binding).is_err()); runtime.logins.insert( id, PendingLogin { user_id: 7, participant_id: 1, contact_key, - context, - state: ke2.state, + state: Some(ke2.state), real_record: true, record_hash: Some(Sha256::digest(&record).to_vec()), created: Instant::now() - runtime.pending_ttl - Duration::from_secs(1), @@ -349,68 +311,30 @@ mod tests { #[test] fn correct_ke3_releases_the_reserved_account_slot() { - let setup = password::generate_server_setup(); - let principal = b"omega-key:example#7"; - let server = password::server_identifier(11); - let identifiers = Identifiers { - client: Some(principal), - server: Some(&server), - }; - let registration = - ClientRegistration::::start(&mut OsRng, b"correct password") - .unwrap(); + let setup = opaque::generate_server_setup(); + let principal = "omega-key:example#7"; + let registration = opaque::client_registration_start(b"correct password").unwrap(); let response = - password::registration_start(&setup, ®istration.message.serialize(), principal) + opaque::server_registration_start(&setup, ®istration.request, principal.as_bytes()) .unwrap(); - let upload = registration - .state - .finish( - &mut OsRng, - b"correct password", - RegistrationResponse::::deserialize(&response).unwrap(), - ClientRegistrationFinishParameters::new(identifiers, None), - ) - .unwrap(); - let record = password::registration_finish(&upload.message.serialize()).unwrap(); + let upload = registration.state.finish(&response, principal, 11).unwrap(); + let record = opaque::server_registration_finish(&upload.upload).unwrap(); let id = Uuid::new_v4(); - let context = password::login_context( - "omega-key:example#7", - 11, - id, - &Keyring::generate().public_key_bundle(), - ) - .unwrap(); - let client = - ClientLogin::::start(&mut OsRng, b"correct password") - .unwrap(); - let ke2 = password::login_start( - &setup, - Some(&record), - &client.message.serialize(), + let fingerprint = + wire::public_key_fingerprint(&Keyring::generate().public_key_bundle()).unwrap(); + let binding = PasswordLoginBindingV1 { principal, - &server, - &context, - ) - .unwrap(); + iota_id: 11, + session_id: id, + contact_key_sha256: &fingerprint, + }; + let client = opaque::client_login_start(b"correct password").unwrap(); + let ke2 = + opaque::server_login_start(&setup, Some(&record), &client.request, &binding).unwrap(); let runtime = limited_runtime(); let (attempt, _) = runtime.begin_attempt(7).unwrap(); - let ke3 = client - .state - .finish( - &mut OsRng, - b"correct password", - CredentialResponse::::deserialize(&ke2.response).unwrap(), - ClientLoginFinishParameters::new(Some(&context), identifiers, None), - ) - .unwrap(); - password::login_finish( - &ke2.state, - &ke3.message.serialize(), - principal, - &server, - &context, - ) - .unwrap(); + let ke3 = client.state.finish(&ke2.response, &binding).unwrap(); + opaque::server_login_finish(ke2.state, &ke3.finalization, &binding).unwrap(); attempt.success(); assert_eq!(runtime.begin_attempt(7).unwrap().1, Duration::ZERO); } diff --git a/omikron-connector/src/password/provisioning.rs b/omikron-connector/src/password/provisioning.rs index c5259f5..63f6033 100644 --- a/omikron-connector/src/password/provisioning.rs +++ b/omikron-connector/src/password/provisioning.rs @@ -4,10 +4,10 @@ use std::{ }; use dashmap::mapref::entry::Entry; -use iota_auth::password::{self, PasswordServerSetup}; use iota_storage::users::{password_credentials, user_manager}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::PublicKeyBundle; +use opague_integration::opaque::{self, PasswordLoginBindingV1, PasswordServerSetup}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -15,7 +15,7 @@ use super::{ MAX_CREDENTIAL_BYTES, MAX_OPAQUE_BYTES, OmikronConnection, PasswordFinishError, PendingLogin, app_protection::{PROVISIONING_CREDENTIAL_ENCRYPTION, PROVISIONING_CREDENTIAL_SIGNATURE}, runtime::AttemptLease, - wire::{bytes, key_fingerprint, positive, public_key, session, typed}, + wire::{bytes, key_fingerprint, positive, public_key, public_key_fingerprint, session, typed}, }; struct PreparedPasswordStart { @@ -84,7 +84,7 @@ impl OmikronConnection { } let contact_key = public_key(content)?; let ke1 = bytes(content, DataType::OpaqueMessage, MAX_OPAQUE_BYTES)?; - password::validate_login_request(&ke1).map_err(|error| error.to_string())?; + opaque::validate_login_request(&ke1).map_err(|error| error.to_string())?; let setup = self .password_auth .setup() @@ -127,27 +127,29 @@ impl OmikronConnection { tokio::time::sleep(delay).await; } let principal = self.password_principal(user_id).await?; - let context = password::login_context(&principal, iota_id, session_id, &contact_key) - .map_err(|error| error.to_string())?; + let contact_key_sha256 = public_key_fingerprint(&contact_key)?; + let binding = PasswordLoginBindingV1 { + principal: &principal, + iota_id, + session_id, + contact_key_sha256: &contact_key_sha256, + }; let credential = password_credentials::get(user_id).map_err(|error| error.to_string())?; let compatible_credential = credential .as_ref() - .filter(|credential| credential.opaque_profile == password::CURRENT_OPAQUE_PROFILE); - let response = password::login_start( + .filter(|credential| credential.opaque_profile == opaque::CURRENT_OPAQUE_PROFILE); + let response = opaque::server_login_start( &setup, compatible_credential.map(|value| value.opaque_record.as_slice()), &ke1, - principal.as_bytes(), - &password::server_identifier(iota_id), - &context, + &binding, ) .map_err(|error| error.to_string())?; let pending = PendingLogin { user_id, participant_id, contact_key, - context, - state: response.state, + state: Some(response.state), real_record: compatible_credential.is_some(), created: Instant::now(), attempt: Some(attempt), @@ -250,18 +252,24 @@ impl OmikronConnection { .password_principal(pending.user_id) .await .map_err(|_| PasswordFinishError::CredentialUnavailable)?; - match password::login_finish( - &pending.state, - &ke3, - principal.as_bytes(), - &password::server_identifier(iota_id), - &pending.context, - ) { + let contact_key_sha256 = public_key_fingerprint(&pending.contact_key) + .map_err(|_| PasswordFinishError::InvalidClientMessage)?; + let binding = PasswordLoginBindingV1 { + principal: &principal, + iota_id, + session_id: id, + contact_key_sha256: &contact_key_sha256, + }; + let state = pending + .state + .take() + .ok_or(PasswordFinishError::InvalidClientMessage)?; + match opaque::server_login_finish(state, &ke3, &binding) { Ok(()) => {} - Err(password::LoginFinishError::AuthenticationFailed) => { + Err(opaque::LoginFinishError::AuthenticationFailed) => { return Err(PasswordFinishError::Authentication); } - Err(password::LoginFinishError::InvalidMessage) => { + Err(opaque::LoginFinishError::InvalidMessage) => { return Err(PasswordFinishError::InvalidClientMessage); } } @@ -271,7 +279,7 @@ impl OmikronConnection { let credential = password_credentials::get(pending.user_id) .map_err(|_| PasswordFinishError::Storage)? .ok_or(PasswordFinishError::CredentialUnavailable)?; - if credential.opaque_profile != password::CURRENT_OPAQUE_PROFILE { + if credential.opaque_profile != opaque::CURRENT_OPAQUE_PROFILE { return Err(PasswordFinishError::CredentialUnavailable); } if pending.record_hash.as_deref() @@ -343,8 +351,6 @@ mod admission_tests { use dashmap::DashSet; use iota_storage::util::config_util::CONFIG; use mtp::crypto::Keyring; - use opaque_ke::ClientLogin; - use rand_core::OsRng; use tokio::sync::Semaphore; #[tokio::test] @@ -366,7 +372,7 @@ mod admission_tests { connection .password_auth .setup - .set(Arc::new(password::generate_server_setup())) + .set(Arc::new(opaque::generate_server_setup())) .ok() .unwrap(); let user_id = 777; @@ -376,11 +382,7 @@ mod admission_tests { .unwrap() .0 .failure(); - let ke1 = ClientLogin::::start(&mut OsRng, b"password") - .unwrap() - .message - .serialize() - .to_vec(); + let ke1 = opaque::client_login_start(b"password").unwrap().request; let frame = CommunicationValue::new(CommunicationType::PasswordProvisioningStart) .with_id(42) .add_typed_default(DataType::Uuid, DataValue::Str(Uuid::new_v4().to_string())) diff --git a/omikron-connector/src/password/runtime.rs b/omikron-connector/src/password/runtime.rs index ae99eaa..7c17534 100644 --- a/omikron-connector/src/password/runtime.rs +++ b/omikron-connector/src/password/runtime.rs @@ -5,19 +5,17 @@ use std::{ }; use dashmap::DashMap; -use iota_auth::password::{self, PasswordServerSetup}; use iota_storage::users::password_credentials; use mtp::crypto::PublicKeyBundle; +use opague_integration::opaque::{self, PasswordServerSetup, ServerLoginState}; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use uuid::Uuid; -use zeroize::Zeroizing; pub(super) struct PendingLogin { pub(super) user_id: i64, pub(super) participant_id: u64, pub(super) contact_key: PublicKeyBundle, - pub(super) context: Vec, - pub(super) state: password::SerializedLoginState, + pub(super) state: Option, pub(super) real_record: bool, pub(super) record_hash: Option>, pub(super) created: Instant, @@ -150,14 +148,15 @@ impl PasswordAuthRuntime { } let path = identity.with_file_name("password-auth.setup"); let setup = match std::fs::read(&path) { - Ok(bytes) => password::deserialize_server_setup_owned(bytes) - .map_err(|error| error.to_string())?, + Ok(bytes) => { + opaque::deserialize_server_setup_owned(bytes).map_err(|error| error.to_string())? + } Err(error) if error.kind() == std::io::ErrorKind::NotFound => { if password_credentials::any().map_err(|error| error.to_string())? { return Err("OPAQUE setup is missing while password credentials exist".into()); } - let setup = password::generate_server_setup(); - let serialized = Zeroizing::new(password::serialize_server_setup(&setup)); + let setup = opaque::generate_server_setup(); + let serialized = opaque::serialize_server_setup(&setup); iota_util::atomic_file::replace_private(&path, &serialized, 0) .map_err(|error| error.to_string())?; setup diff --git a/omikron-connector/src/password/wire.rs b/omikron-connector/src/password/wire.rs index db61e53..d758133 100644 --- a/omikron-connector/src/password/wire.rs +++ b/omikron-connector/src/password/wire.rs @@ -75,5 +75,10 @@ pub(super) fn public_key(content: &DataValue) -> Result pub(super) fn key_fingerprint(encoded: &str) -> Result, String> { let bundle = PublicKeyBundle::from_base64(encoded).map_err(|error| error.to_string())?; - Ok(Sha256::digest(bundle.try_as_bytes().map_err(|error| error.to_string())?).to_vec()) + Ok(public_key_fingerprint(&bundle)?.to_vec()) +} + +pub(super) fn public_key_fingerprint(bundle: &PublicKeyBundle) -> Result<[u8; 32], String> { + let bytes = bundle.try_as_bytes().map_err(|error| error.to_string())?; + Ok(Sha256::digest(bytes).into()) }