Use shared OPAQUE password authentication
This commit is contained in:
parent
8ad56b938d
commit
e3a16be2f9
10 changed files with 115 additions and 657 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -2229,19 +2229,13 @@ dependencies = [
|
||||||
name = "iota-auth"
|
name = "iota-auth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"generic-array",
|
|
||||||
"iota-identity",
|
"iota-identity",
|
||||||
"mtp",
|
"mtp",
|
||||||
"opaque-ke",
|
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"sha2 0.10.9",
|
|
||||||
"thiserror 2.0.20",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3299,7 +3293,7 @@ dependencies = [
|
||||||
"iota-util",
|
"iota-util",
|
||||||
"json",
|
"json",
|
||||||
"mtp",
|
"mtp",
|
||||||
"opaque-ke",
|
"opague-integration",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -3311,7 +3305,6 @@ dependencies = [
|
||||||
"trust-dns-resolver",
|
"trust-dns-resolver",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3326,6 +3319,32 @@ version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
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]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,6 @@ dashmap = "6.2.1"
|
||||||
iota-identity = { path = "../iota-identity" }
|
iota-identity = { path = "../iota-identity" }
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "bb0f682b735de5ebb36bb41dc699260578341828", features = ["crypto"] }
|
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "bb0f682b735de5ebb36bb41dc699260578341828", features = ["crypto"] }
|
||||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
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"] }
|
uuid = { version = "*", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod password;
|
|
||||||
mod principal_auth;
|
mod principal_auth;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<u8> {
|
|
||||||
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<Vec<u8>, 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<L: ArrayLength<u8>>(
|
|
||||||
&self,
|
|
||||||
input: GenericArray<u8, L>,
|
|
||||||
) -> Result<GenericArray<u8, L>, opaque_ke::errors::InternalError> {
|
|
||||||
let mut output = GenericArray::<u8, L>::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<Ristretto255, sha2::Sha512>;
|
|
||||||
type Ksf = PasswordKsfV1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type PasswordServerSetup = ServerSetup<TensaminOpaque>;
|
|
||||||
|
|
||||||
#[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<u8> {
|
|
||||||
setup.serialize().to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize_server_setup(bytes: &[u8]) -> Result<PasswordServerSetup, PasswordAuthError> {
|
|
||||||
ServerSetup::deserialize(bytes).map_err(|error| PasswordAuthError::Opaque(error.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize_server_setup_owned(
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
) -> Result<PasswordServerSetup, PasswordAuthError> {
|
|
||||||
let bytes = Zeroizing::new(bytes);
|
|
||||||
deserialize_server_setup(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn registration_start(
|
|
||||||
setup: &PasswordServerSetup,
|
|
||||||
request: &[u8],
|
|
||||||
identifier: &[u8],
|
|
||||||
) -> Result<Vec<u8>, PasswordAuthError> {
|
|
||||||
let message = RegistrationRequest::<TensaminOpaque>::deserialize(request)
|
|
||||||
.map_err(|error| PasswordAuthError::Opaque(error.to_string()))?;
|
|
||||||
let result = ServerRegistration::<TensaminOpaque>::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<Vec<u8>, PasswordAuthError> {
|
|
||||||
let message = RegistrationUpload::<TensaminOpaque>::deserialize(upload)
|
|
||||||
.map_err(|error| PasswordAuthError::Opaque(error.to_string()))?;
|
|
||||||
Ok(ServerRegistration::<TensaminOpaque>::finish(message)
|
|
||||||
.serialize()
|
|
||||||
.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_login_request(request: &[u8]) -> Result<(), PasswordAuthError> {
|
|
||||||
CredentialRequest::<TensaminOpaque>::deserialize(request)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|error| PasswordAuthError::Opaque(error.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn server_identifier(iota_id: i64) -> Vec<u8> {
|
|
||||||
OpaqueProfileV1::server_identifier(iota_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login_context(
|
|
||||||
principal: &str,
|
|
||||||
iota_id: i64,
|
|
||||||
session_id: uuid::Uuid,
|
|
||||||
contact_key: &PublicKeyBundle,
|
|
||||||
) -> Result<Vec<u8>, PasswordAuthError> {
|
|
||||||
OpaqueProfileV1::login_context(principal, iota_id, session_id, contact_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LoginStartResult {
|
|
||||||
pub response: Vec<u8>,
|
|
||||||
pub state: SerializedLoginState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SerializedLoginState = Zeroizing<Vec<u8>>;
|
|
||||||
|
|
||||||
pub fn login_start(
|
|
||||||
setup: &PasswordServerSetup,
|
|
||||||
record: Option<&[u8]>,
|
|
||||||
request: &[u8],
|
|
||||||
principal: &[u8],
|
|
||||||
server_id: &[u8],
|
|
||||||
context: &[u8],
|
|
||||||
) -> Result<LoginStartResult, PasswordAuthError> {
|
|
||||||
let message = CredentialRequest::<TensaminOpaque>::deserialize(request)
|
|
||||||
.map_err(|error| PasswordAuthError::Opaque(error.to_string()))?;
|
|
||||||
let registration = record
|
|
||||||
.map(ServerRegistration::<TensaminOpaque>::deserialize)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|error| PasswordAuthError::Opaque(error.to_string()))?;
|
|
||||||
let result = ServerLogin::<TensaminOpaque>::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::<TensaminOpaque>::deserialize(serialized_state)
|
|
||||||
.map_err(|_| LoginFinishError::InvalidMessage)?;
|
|
||||||
let message = CredentialFinalization::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::start(&mut OsRng, b"correct horse").unwrap();
|
|
||||||
let result = login_start(
|
|
||||||
&restored,
|
|
||||||
Some(&record),
|
|
||||||
&client.message.serialize(),
|
|
||||||
principal,
|
|
||||||
&server,
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let ke2 = CredentialResponse::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::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::<TensaminOpaque>::deserialize(&result.response)
|
|
||||||
.unwrap(),
|
|
||||||
ClientLoginFinishParameters::new(Some(&context), identifiers, None)
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = ClientLogin::<TensaminOpaque>::start(&mut OsRng, b"correct horse").unwrap();
|
|
||||||
let dummy = login_start(
|
|
||||||
&restored,
|
|
||||||
None,
|
|
||||||
&client.message.serialize(),
|
|
||||||
principal,
|
|
||||||
&server,
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(CredentialResponse::<TensaminOpaque>::deserialize(&dummy.response).is_ok());
|
|
||||||
let unrelated = generate_server_setup();
|
|
||||||
let client = ClientLogin::<TensaminOpaque>::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::<TensaminOpaque>::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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
opague-integration = { git = "https://git.methanium.net/tensamin/opaque-integration.git", rev = "d996c29366429160103e2a3f99c3e435522456f0" }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
iota-connection = { path = "../iota-connection" }
|
iota-connection = { path = "../iota-connection" }
|
||||||
iota-auth = { path = "../iota-auth" }
|
iota-auth = { path = "../iota-auth" }
|
||||||
|
|
@ -34,7 +35,3 @@ base64 = "0.22.1"
|
||||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
||||||
trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] }
|
trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
zeroize = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
opaque-ke = { version = "4.0.1", features = ["argon2"] }
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use iota_auth::password;
|
|
||||||
use iota_storage::users::{
|
use iota_storage::users::{
|
||||||
password_credentials::{self, PasswordCredential},
|
password_credentials::{self, PasswordCredential},
|
||||||
user_manager,
|
user_manager,
|
||||||
};
|
};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
|
use opague_integration::opaque;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
@ -48,7 +48,7 @@ impl OmikronConnection {
|
||||||
.password_auth
|
.password_auth
|
||||||
.setup()
|
.setup()
|
||||||
.map_err(|error| error.to_string())?;
|
.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())?;
|
.map_err(|error| error.to_string())?;
|
||||||
let enrollment_id = Uuid::new_v4();
|
let enrollment_id = Uuid::new_v4();
|
||||||
self.password_auth
|
self.password_auth
|
||||||
|
|
@ -108,7 +108,7 @@ impl OmikronConnection {
|
||||||
MAX_CREDENTIAL_BYTES,
|
MAX_CREDENTIAL_BYTES,
|
||||||
)?;
|
)?;
|
||||||
let opaque_record =
|
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)
|
let profile = user_manager::get_user(user_id)
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.ok_or("account not hosted")?;
|
.ok_or("account not hosted")?;
|
||||||
|
|
@ -116,7 +116,7 @@ impl OmikronConnection {
|
||||||
let now = now_millis() as i64;
|
let now = now_millis() as i64;
|
||||||
password_credentials::upsert(&PasswordCredential {
|
password_credentials::upsert(&PasswordCredential {
|
||||||
user_id,
|
user_id,
|
||||||
opaque_profile: password::CURRENT_OPAQUE_PROFILE,
|
opaque_profile: opaque::CURRENT_OPAQUE_PROFILE,
|
||||||
opaque_record,
|
opaque_record,
|
||||||
encrypted_tu_credential: encrypted,
|
encrypted_tu_credential: encrypted,
|
||||||
account_public_key_sha256: fingerprint,
|
account_public_key_sha256: fingerprint,
|
||||||
|
|
@ -148,7 +148,7 @@ impl OmikronConnection {
|
||||||
let enabled = password_credentials::get(user_id)
|
let enabled = password_credentials::get(user_id)
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.is_some_and(|credential| {
|
.is_some_and(|credential| {
|
||||||
credential.opaque_profile == password::CURRENT_OPAQUE_PROFILE
|
credential.opaque_profile == opaque::CURRENT_OPAQUE_PROFILE
|
||||||
});
|
});
|
||||||
self.protected_response(
|
self.protected_response(
|
||||||
frame,
|
frame,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ use std::time::Duration;
|
||||||
use iota_storage::users::user_manager;
|
use iota_storage::users::user_manager;
|
||||||
use mtp::crypto::PublicKeyBundle;
|
use mtp::crypto::PublicKeyBundle;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use iota_auth::password;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mtp::codec::{
|
use mtp::codec::{
|
||||||
CommunicationType, CommunicationValue, DataType, DataValue, ProtectedMessageBuilder,
|
CommunicationType, CommunicationValue, DataType, DataValue, ProtectedMessageBuilder,
|
||||||
|
|
@ -13,15 +11,13 @@ use mtp::codec::{
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mtp::crypto::DualSigner;
|
use mtp::crypto::DualSigner;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use rand_core::OsRng;
|
use opague_integration::opaque::{self, PasswordLoginBindingV1};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
#[cfg(test)]
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
use crate::omikron_connection::OmikronConnection;
|
use crate::omikron_connection::OmikronConnection;
|
||||||
|
|
||||||
|
|
@ -107,10 +103,6 @@ impl OmikronConnection {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use mtp::{codec::InMemoryReplayGuard, crypto::Keyring};
|
use mtp::{codec::InMemoryReplayGuard, crypto::Keyring};
|
||||||
use opaque_ke::{
|
|
||||||
ClientLogin, ClientLoginFinishParameters, ClientRegistration,
|
|
||||||
ClientRegistrationFinishParameters, CredentialResponse, Identifiers, RegistrationResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn damaged_setup_stays_unavailable() {
|
fn damaged_setup_stays_unavailable() {
|
||||||
|
|
@ -144,8 +136,7 @@ mod tests {
|
||||||
user_id,
|
user_id,
|
||||||
participant_id: 1,
|
participant_id: 1,
|
||||||
contact_key: Keyring::generate().public_key_bundle(),
|
contact_key: Keyring::generate().public_key_bundle(),
|
||||||
context: vec![],
|
state: None,
|
||||||
state: Zeroizing::new(vec![]),
|
|
||||||
real_record: false,
|
real_record: false,
|
||||||
record_hash: None,
|
record_hash: None,
|
||||||
created,
|
created,
|
||||||
|
|
@ -275,66 +266,37 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrong_password_client_stops_after_ke2_and_expiry_consumes_budget() {
|
fn wrong_password_client_stops_after_ke2_and_expiry_consumes_budget() {
|
||||||
let setup = password::generate_server_setup();
|
let setup = opaque::generate_server_setup();
|
||||||
let principal = b"omega-key:example#7";
|
let principal = "omega-key:example#7";
|
||||||
let server = password::server_identifier(11);
|
let registration = opaque::client_registration_start(b"correct password").unwrap();
|
||||||
let identifiers = Identifiers {
|
|
||||||
client: Some(principal),
|
|
||||||
server: Some(&server),
|
|
||||||
};
|
|
||||||
let registration =
|
|
||||||
ClientRegistration::<password::TensaminOpaque>::start(&mut OsRng, b"correct password")
|
|
||||||
.unwrap();
|
|
||||||
let response =
|
let response =
|
||||||
password::registration_start(&setup, ®istration.message.serialize(), principal)
|
opaque::server_registration_start(&setup, ®istration.request, principal.as_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let upload = registration
|
let upload = registration.state.finish(&response, principal, 11).unwrap();
|
||||||
.state
|
let record = opaque::server_registration_finish(&upload.upload).unwrap();
|
||||||
.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 runtime = limited_runtime();
|
let runtime = limited_runtime();
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let contact_key = Keyring::generate().public_key_bundle();
|
let contact_key = Keyring::generate().public_key_bundle();
|
||||||
let context = password::login_context("omega-key:example#7", 11, id, &contact_key).unwrap();
|
let fingerprint = wire::public_key_fingerprint(&contact_key).unwrap();
|
||||||
let client =
|
let binding = PasswordLoginBindingV1 {
|
||||||
ClientLogin::<password::TensaminOpaque>::start(&mut OsRng, b"wrong password").unwrap();
|
|
||||||
let ke2 = password::login_start(
|
|
||||||
&setup,
|
|
||||||
Some(&record),
|
|
||||||
&client.message.serialize(),
|
|
||||||
principal,
|
principal,
|
||||||
&server,
|
iota_id: 11,
|
||||||
&context,
|
session_id: id,
|
||||||
)
|
contact_key_sha256: &fingerprint,
|
||||||
.unwrap();
|
};
|
||||||
|
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();
|
let (attempt, _) = runtime.begin_attempt(7).unwrap();
|
||||||
assert!(
|
assert!(client.state.finish(&ke2.response, &binding).is_err());
|
||||||
client
|
|
||||||
.state
|
|
||||||
.finish(
|
|
||||||
&mut OsRng,
|
|
||||||
b"wrong password",
|
|
||||||
CredentialResponse::<password::TensaminOpaque>::deserialize(&ke2.response)
|
|
||||||
.unwrap(),
|
|
||||||
ClientLoginFinishParameters::new(Some(&context), identifiers, None),
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
runtime.logins.insert(
|
runtime.logins.insert(
|
||||||
id,
|
id,
|
||||||
PendingLogin {
|
PendingLogin {
|
||||||
user_id: 7,
|
user_id: 7,
|
||||||
participant_id: 1,
|
participant_id: 1,
|
||||||
contact_key,
|
contact_key,
|
||||||
context,
|
state: Some(ke2.state),
|
||||||
state: ke2.state,
|
|
||||||
real_record: true,
|
real_record: true,
|
||||||
record_hash: Some(Sha256::digest(&record).to_vec()),
|
record_hash: Some(Sha256::digest(&record).to_vec()),
|
||||||
created: Instant::now() - runtime.pending_ttl - Duration::from_secs(1),
|
created: Instant::now() - runtime.pending_ttl - Duration::from_secs(1),
|
||||||
|
|
@ -349,68 +311,30 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn correct_ke3_releases_the_reserved_account_slot() {
|
fn correct_ke3_releases_the_reserved_account_slot() {
|
||||||
let setup = password::generate_server_setup();
|
let setup = opaque::generate_server_setup();
|
||||||
let principal = b"omega-key:example#7";
|
let principal = "omega-key:example#7";
|
||||||
let server = password::server_identifier(11);
|
let registration = opaque::client_registration_start(b"correct password").unwrap();
|
||||||
let identifiers = Identifiers {
|
|
||||||
client: Some(principal),
|
|
||||||
server: Some(&server),
|
|
||||||
};
|
|
||||||
let registration =
|
|
||||||
ClientRegistration::<password::TensaminOpaque>::start(&mut OsRng, b"correct password")
|
|
||||||
.unwrap();
|
|
||||||
let response =
|
let response =
|
||||||
password::registration_start(&setup, ®istration.message.serialize(), principal)
|
opaque::server_registration_start(&setup, ®istration.request, principal.as_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let upload = registration
|
let upload = registration.state.finish(&response, principal, 11).unwrap();
|
||||||
.state
|
let record = opaque::server_registration_finish(&upload.upload).unwrap();
|
||||||
.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 id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let context = password::login_context(
|
let fingerprint =
|
||||||
"omega-key:example#7",
|
wire::public_key_fingerprint(&Keyring::generate().public_key_bundle()).unwrap();
|
||||||
11,
|
let binding = PasswordLoginBindingV1 {
|
||||||
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(),
|
|
||||||
principal,
|
principal,
|
||||||
&server,
|
iota_id: 11,
|
||||||
&context,
|
session_id: id,
|
||||||
)
|
contact_key_sha256: &fingerprint,
|
||||||
.unwrap();
|
};
|
||||||
|
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 runtime = limited_runtime();
|
||||||
let (attempt, _) = runtime.begin_attempt(7).unwrap();
|
let (attempt, _) = runtime.begin_attempt(7).unwrap();
|
||||||
let ke3 = client
|
let ke3 = client.state.finish(&ke2.response, &binding).unwrap();
|
||||||
.state
|
opaque::server_login_finish(ke2.state, &ke3.finalization, &binding).unwrap();
|
||||||
.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();
|
|
||||||
attempt.success();
|
attempt.success();
|
||||||
assert_eq!(runtime.begin_attempt(7).unwrap().1, Duration::ZERO);
|
assert_eq!(runtime.begin_attempt(7).unwrap().1, Duration::ZERO);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::mapref::entry::Entry;
|
use dashmap::mapref::entry::Entry;
|
||||||
use iota_auth::password::{self, PasswordServerSetup};
|
|
||||||
use iota_storage::users::{password_credentials, user_manager};
|
use iota_storage::users::{password_credentials, user_manager};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use mtp::crypto::PublicKeyBundle;
|
use mtp::crypto::PublicKeyBundle;
|
||||||
|
use opague_integration::opaque::{self, PasswordLoginBindingV1, PasswordServerSetup};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ use super::{
|
||||||
MAX_CREDENTIAL_BYTES, MAX_OPAQUE_BYTES, OmikronConnection, PasswordFinishError, PendingLogin,
|
MAX_CREDENTIAL_BYTES, MAX_OPAQUE_BYTES, OmikronConnection, PasswordFinishError, PendingLogin,
|
||||||
app_protection::{PROVISIONING_CREDENTIAL_ENCRYPTION, PROVISIONING_CREDENTIAL_SIGNATURE},
|
app_protection::{PROVISIONING_CREDENTIAL_ENCRYPTION, PROVISIONING_CREDENTIAL_SIGNATURE},
|
||||||
runtime::AttemptLease,
|
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 {
|
struct PreparedPasswordStart {
|
||||||
|
|
@ -84,7 +84,7 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
let contact_key = public_key(content)?;
|
let contact_key = public_key(content)?;
|
||||||
let ke1 = bytes(content, DataType::OpaqueMessage, MAX_OPAQUE_BYTES)?;
|
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
|
let setup = self
|
||||||
.password_auth
|
.password_auth
|
||||||
.setup()
|
.setup()
|
||||||
|
|
@ -127,27 +127,29 @@ impl OmikronConnection {
|
||||||
tokio::time::sleep(delay).await;
|
tokio::time::sleep(delay).await;
|
||||||
}
|
}
|
||||||
let principal = self.password_principal(user_id).await?;
|
let principal = self.password_principal(user_id).await?;
|
||||||
let context = password::login_context(&principal, iota_id, session_id, &contact_key)
|
let contact_key_sha256 = public_key_fingerprint(&contact_key)?;
|
||||||
.map_err(|error| error.to_string())?;
|
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 credential = password_credentials::get(user_id).map_err(|error| error.to_string())?;
|
||||||
let compatible_credential = credential
|
let compatible_credential = credential
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|credential| credential.opaque_profile == password::CURRENT_OPAQUE_PROFILE);
|
.filter(|credential| credential.opaque_profile == opaque::CURRENT_OPAQUE_PROFILE);
|
||||||
let response = password::login_start(
|
let response = opaque::server_login_start(
|
||||||
&setup,
|
&setup,
|
||||||
compatible_credential.map(|value| value.opaque_record.as_slice()),
|
compatible_credential.map(|value| value.opaque_record.as_slice()),
|
||||||
&ke1,
|
&ke1,
|
||||||
principal.as_bytes(),
|
&binding,
|
||||||
&password::server_identifier(iota_id),
|
|
||||||
&context,
|
|
||||||
)
|
)
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
let pending = PendingLogin {
|
let pending = PendingLogin {
|
||||||
user_id,
|
user_id,
|
||||||
participant_id,
|
participant_id,
|
||||||
contact_key,
|
contact_key,
|
||||||
context,
|
state: Some(response.state),
|
||||||
state: response.state,
|
|
||||||
real_record: compatible_credential.is_some(),
|
real_record: compatible_credential.is_some(),
|
||||||
created: Instant::now(),
|
created: Instant::now(),
|
||||||
attempt: Some(attempt),
|
attempt: Some(attempt),
|
||||||
|
|
@ -250,18 +252,24 @@ impl OmikronConnection {
|
||||||
.password_principal(pending.user_id)
|
.password_principal(pending.user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| PasswordFinishError::CredentialUnavailable)?;
|
.map_err(|_| PasswordFinishError::CredentialUnavailable)?;
|
||||||
match password::login_finish(
|
let contact_key_sha256 = public_key_fingerprint(&pending.contact_key)
|
||||||
&pending.state,
|
.map_err(|_| PasswordFinishError::InvalidClientMessage)?;
|
||||||
&ke3,
|
let binding = PasswordLoginBindingV1 {
|
||||||
principal.as_bytes(),
|
principal: &principal,
|
||||||
&password::server_identifier(iota_id),
|
iota_id,
|
||||||
&pending.context,
|
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(()) => {}
|
Ok(()) => {}
|
||||||
Err(password::LoginFinishError::AuthenticationFailed) => {
|
Err(opaque::LoginFinishError::AuthenticationFailed) => {
|
||||||
return Err(PasswordFinishError::Authentication);
|
return Err(PasswordFinishError::Authentication);
|
||||||
}
|
}
|
||||||
Err(password::LoginFinishError::InvalidMessage) => {
|
Err(opaque::LoginFinishError::InvalidMessage) => {
|
||||||
return Err(PasswordFinishError::InvalidClientMessage);
|
return Err(PasswordFinishError::InvalidClientMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +279,7 @@ impl OmikronConnection {
|
||||||
let credential = password_credentials::get(pending.user_id)
|
let credential = password_credentials::get(pending.user_id)
|
||||||
.map_err(|_| PasswordFinishError::Storage)?
|
.map_err(|_| PasswordFinishError::Storage)?
|
||||||
.ok_or(PasswordFinishError::CredentialUnavailable)?;
|
.ok_or(PasswordFinishError::CredentialUnavailable)?;
|
||||||
if credential.opaque_profile != password::CURRENT_OPAQUE_PROFILE {
|
if credential.opaque_profile != opaque::CURRENT_OPAQUE_PROFILE {
|
||||||
return Err(PasswordFinishError::CredentialUnavailable);
|
return Err(PasswordFinishError::CredentialUnavailable);
|
||||||
}
|
}
|
||||||
if pending.record_hash.as_deref()
|
if pending.record_hash.as_deref()
|
||||||
|
|
@ -343,8 +351,6 @@ mod admission_tests {
|
||||||
use dashmap::DashSet;
|
use dashmap::DashSet;
|
||||||
use iota_storage::util::config_util::CONFIG;
|
use iota_storage::util::config_util::CONFIG;
|
||||||
use mtp::crypto::Keyring;
|
use mtp::crypto::Keyring;
|
||||||
use opaque_ke::ClientLogin;
|
|
||||||
use rand_core::OsRng;
|
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -366,7 +372,7 @@ mod admission_tests {
|
||||||
connection
|
connection
|
||||||
.password_auth
|
.password_auth
|
||||||
.setup
|
.setup
|
||||||
.set(Arc::new(password::generate_server_setup()))
|
.set(Arc::new(opaque::generate_server_setup()))
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let user_id = 777;
|
let user_id = 777;
|
||||||
|
|
@ -376,11 +382,7 @@ mod admission_tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0
|
.0
|
||||||
.failure();
|
.failure();
|
||||||
let ke1 = ClientLogin::<password::TensaminOpaque>::start(&mut OsRng, b"password")
|
let ke1 = opaque::client_login_start(b"password").unwrap().request;
|
||||||
.unwrap()
|
|
||||||
.message
|
|
||||||
.serialize()
|
|
||||||
.to_vec();
|
|
||||||
let frame = CommunicationValue::new(CommunicationType::PasswordProvisioningStart)
|
let frame = CommunicationValue::new(CommunicationType::PasswordProvisioningStart)
|
||||||
.with_id(42)
|
.with_id(42)
|
||||||
.add_typed_default(DataType::Uuid, DataValue::Str(Uuid::new_v4().to_string()))
|
.add_typed_default(DataType::Uuid, DataValue::Str(Uuid::new_v4().to_string()))
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,17 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use iota_auth::password::{self, PasswordServerSetup};
|
|
||||||
use iota_storage::users::password_credentials;
|
use iota_storage::users::password_credentials;
|
||||||
use mtp::crypto::PublicKeyBundle;
|
use mtp::crypto::PublicKeyBundle;
|
||||||
|
use opague_integration::opaque::{self, PasswordServerSetup, ServerLoginState};
|
||||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
pub(super) struct PendingLogin {
|
pub(super) struct PendingLogin {
|
||||||
pub(super) user_id: i64,
|
pub(super) user_id: i64,
|
||||||
pub(super) participant_id: u64,
|
pub(super) participant_id: u64,
|
||||||
pub(super) contact_key: PublicKeyBundle,
|
pub(super) contact_key: PublicKeyBundle,
|
||||||
pub(super) context: Vec<u8>,
|
pub(super) state: Option<ServerLoginState>,
|
||||||
pub(super) state: password::SerializedLoginState,
|
|
||||||
pub(super) real_record: bool,
|
pub(super) real_record: bool,
|
||||||
pub(super) record_hash: Option<Vec<u8>>,
|
pub(super) record_hash: Option<Vec<u8>>,
|
||||||
pub(super) created: Instant,
|
pub(super) created: Instant,
|
||||||
|
|
@ -150,14 +148,15 @@ impl PasswordAuthRuntime {
|
||||||
}
|
}
|
||||||
let path = identity.with_file_name("password-auth.setup");
|
let path = identity.with_file_name("password-auth.setup");
|
||||||
let setup = match std::fs::read(&path) {
|
let setup = match std::fs::read(&path) {
|
||||||
Ok(bytes) => password::deserialize_server_setup_owned(bytes)
|
Ok(bytes) => {
|
||||||
.map_err(|error| error.to_string())?,
|
opaque::deserialize_server_setup_owned(bytes).map_err(|error| error.to_string())?
|
||||||
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||||
if password_credentials::any().map_err(|error| error.to_string())? {
|
if password_credentials::any().map_err(|error| error.to_string())? {
|
||||||
return Err("OPAQUE setup is missing while password credentials exist".into());
|
return Err("OPAQUE setup is missing while password credentials exist".into());
|
||||||
}
|
}
|
||||||
let setup = password::generate_server_setup();
|
let setup = opaque::generate_server_setup();
|
||||||
let serialized = Zeroizing::new(password::serialize_server_setup(&setup));
|
let serialized = opaque::serialize_server_setup(&setup);
|
||||||
iota_util::atomic_file::replace_private(&path, &serialized, 0)
|
iota_util::atomic_file::replace_private(&path, &serialized, 0)
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
setup
|
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> {
|
pub(super) fn key_fingerprint(encoded: &str) -> Result<Vec<u8>, String> {
|
||||||
let bundle = PublicKeyBundle::from_base64(encoded).map_err(|error| error.to_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