Use shared OPAQUE password authentication
This commit is contained in:
parent
8ad56b938d
commit
e3a16be2f9
10 changed files with 115 additions and 657 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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::<password::TensaminOpaque>::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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<password::TensaminOpaque>::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()))
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
pub(super) state: password::SerializedLoginState,
|
||||
pub(super) state: Option<ServerLoginState>,
|
||||
pub(super) real_record: bool,
|
||||
pub(super) record_hash: Option<Vec<u8>>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -75,5 +75,10 @@ pub(super) fn public_key(content: &DataValue) -> Result<PublicKeyBundle, String>
|
|||
|
||||
pub(super) fn key_fingerprint(encoded: &str) -> Result<Vec<u8>, 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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue