[WIP] 0.3.0 mtp update
This commit is contained in:
parent
7dc98ef29b
commit
e1dd86ec02
42 changed files with 2422 additions and 1429 deletions
|
|
@ -14,6 +14,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
|||
"client",
|
||||
"crypto",
|
||||
"files",
|
||||
"raw",
|
||||
] }
|
||||
|
||||
dashmap = "6.2.1"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@ use iota_storage::users::user_profile::UserProfile;
|
|||
use iota_storage::util::config_util::CONFIG;
|
||||
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
|
||||
use iota_util::file_util::write_user_credential;
|
||||
use iota_util::mtp_compat::OptionalDataValueExt;
|
||||
use iota_util::tu::TuCredential;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use mtp::crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme};
|
||||
|
|
@ -33,7 +34,9 @@ pub enum LifecycleUserError {
|
|||
}
|
||||
|
||||
impl From<crate::OmikronError> for LifecycleUserError {
|
||||
fn from(value: crate::OmikronError) -> Self { Self::Transport(value) }
|
||||
fn from(value: crate::OmikronError) -> Self {
|
||||
Self::Transport(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec<u8> {
|
||||
|
|
@ -46,21 +49,31 @@ fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> V
|
|||
}
|
||||
|
||||
fn configured_iota_id() -> Result<i64, LifecycleUserError> {
|
||||
CONFIG.load().iota_id
|
||||
CONFIG
|
||||
.load()
|
||||
.iota_id
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
.filter(|id| *id > 0)
|
||||
.ok_or_else(|| LifecycleUserError::InvalidCredential("Iota identity is not registered".into()))
|
||||
.ok_or_else(|| {
|
||||
LifecycleUserError::InvalidCredential("Iota identity is not registered".into())
|
||||
})
|
||||
}
|
||||
|
||||
fn sign_lifecycle_payload(credential: &TuCredential, payload: &[u8]) -> Result<(Vec<u8>, Vec<u8>), LifecycleUserError> {
|
||||
fn sign_lifecycle_payload(
|
||||
credential: &TuCredential,
|
||||
payload: &[u8],
|
||||
) -> Result<(Vec<u8>, Vec<u8>), LifecycleUserError> {
|
||||
let classical = Ed25519Signer::new(&credential.keyring.sig_cl_secret_key)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?
|
||||
.sign(payload)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
let pq = MlDsaSigner::new(&credential.keyring.sig_pq_secret_key, &credential.keyring.sig_pq_public_key)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?
|
||||
.sign(payload)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
let pq = MlDsaSigner::new(
|
||||
&credential.keyring.sig_pq_secret_key,
|
||||
&credential.keyring.sig_pq_public_key,
|
||||
)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?
|
||||
.sign(payload)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
Ok((classical, pq))
|
||||
}
|
||||
|
||||
|
|
@ -71,15 +84,25 @@ async fn inspect_credential_account(
|
|||
if credential.omega_host != omega_discovery::omega_host() {
|
||||
return Err(LifecycleUserError::OmegaHostMismatch);
|
||||
}
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData)
|
||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into()));
|
||||
let response = connection.await_response(&request, Duration::from_secs(20)).await?;
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::SignedNumber(credential.user_id.into()),
|
||||
);
|
||||
let response = connection
|
||||
.await_response(&request, Duration::from_secs(20))
|
||||
.await?;
|
||||
if !response.is_type(CommunicationType::GetUserData) {
|
||||
return Err(LifecycleUserError::RemoteRejected);
|
||||
}
|
||||
let username = response.get_data(DataType::Username).as_str().map(str::to_owned)
|
||||
let username = response
|
||||
.get_data(DataType::Username)
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.ok_or(LifecycleUserError::RemoteRejected)?;
|
||||
let public_key = response.get_data(DataType::PublicKey).as_str().map(str::to_owned)
|
||||
let public_key = response
|
||||
.get_data(DataType::PublicKey)
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.ok_or(LifecycleUserError::RemoteRejected)?;
|
||||
if public_key != public_key_bundle_to_base64(&credential.public_key_bundle()) {
|
||||
return Err(LifecycleUserError::RemoteRejected);
|
||||
|
|
@ -96,54 +119,121 @@ async fn credential_proof(
|
|||
domain: &[u8],
|
||||
) -> Result<(), LifecycleUserError> {
|
||||
let iota_id = configured_iota_id()?;
|
||||
let begin_request = CommunicationValue::new(begin)
|
||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into()));
|
||||
let challenge_response = connection.await_response(&begin_request, Duration::from_secs(20)).await?;
|
||||
let begin_request = CommunicationValue::new(begin).add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::SignedNumber(credential.user_id.into()),
|
||||
);
|
||||
let challenge_response = connection
|
||||
.await_response(&begin_request, Duration::from_secs(20))
|
||||
.await?;
|
||||
if !challenge_response.is_type(challenge) {
|
||||
return Err(LifecycleUserError::RemoteRejected);
|
||||
}
|
||||
let nonce = challenge_response.get_data(DataType::ServerNonce).as_signed_number()
|
||||
let nonce = challenge_response
|
||||
.get_data(DataType::ServerNonce)
|
||||
.as_signed_number()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.ok_or(LifecycleUserError::RemoteRejected)?;
|
||||
let (signature, pq_signature) = sign_lifecycle_payload(credential, &lifecycle_payload(domain, credential.user_id, iota_id, nonce))?;
|
||||
let (signature, pq_signature) = sign_lifecycle_payload(
|
||||
credential,
|
||||
&lifecycle_payload(domain, credential.user_id, iota_id, nonce),
|
||||
)?;
|
||||
let complete_request = CommunicationValue::new(complete)
|
||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(credential.user_id.into()))
|
||||
.add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::SignedNumber(credential.user_id.into()),
|
||||
)
|
||||
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||
.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
|
||||
let response = connection.await_response(&complete_request, Duration::from_secs(20)).await?;
|
||||
if response.is_type(CommunicationType::Success) { Ok(()) } else { Err(LifecycleUserError::RemoteRejected) }
|
||||
let response = connection
|
||||
.await_response(&complete_request, Duration::from_secs(20))
|
||||
.await?;
|
||||
if response.is_type(CommunicationType::Success) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LifecycleUserError::RemoteRejected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach or migrate an existing account. Local state is written only after
|
||||
/// Omega has accepted the credential proof and changed its assignment.
|
||||
pub async fn attach_user_from_tu(connection: &dyn OmikronClient, contents: &str) -> Result<UserProfile, LifecycleUserError> {
|
||||
let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
pub async fn attach_user_from_tu(
|
||||
connection: &dyn OmikronClient,
|
||||
contents: &str,
|
||||
) -> Result<UserProfile, LifecycleUserError> {
|
||||
let credential = TuCredential::parse(contents)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
let (username, public_key) = inspect_credential_account(connection, &credential).await?;
|
||||
credential_proof(connection, &credential, CommunicationType::AttachUserBegin, CommunicationType::AttachUserChallenge, CommunicationType::AttachUserComplete, b"tensamin:user-attach:v1\0").await?;
|
||||
let profile = UserProfile::new(credential.user_id, username, None, public_key, hex_hash(contents), String::new());
|
||||
credential_proof(
|
||||
connection,
|
||||
&credential,
|
||||
CommunicationType::AttachUserBegin,
|
||||
CommunicationType::AttachUserChallenge,
|
||||
CommunicationType::AttachUserComplete,
|
||||
b"tensamin:user-attach:v1\0",
|
||||
)
|
||||
.await?;
|
||||
let profile = UserProfile::new(
|
||||
credential.user_id,
|
||||
username,
|
||||
None,
|
||||
public_key,
|
||||
hex_hash(contents),
|
||||
String::new(),
|
||||
);
|
||||
write_user_credential(profile.user_id, &credential.to_canonical_string())
|
||||
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
|
||||
try_add_user(profile.clone()).map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
|
||||
try_add_user(profile.clone())
|
||||
.map_err(|error| LifecycleUserError::LocalPersistence(error.to_string()))?;
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn complete_delete_user_with_tu(connection: &dyn OmikronClient, contents: &str, expected_user_id: i64) -> Result<(), LifecycleUserError> {
|
||||
let credential = TuCredential::parse(contents).map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
if credential.user_id != expected_user_id { return Err(LifecycleUserError::InvalidCredential("credential user ID does not match deletion target".into())); }
|
||||
pub async fn complete_delete_user_with_tu(
|
||||
connection: &dyn OmikronClient,
|
||||
contents: &str,
|
||||
expected_user_id: i64,
|
||||
) -> Result<(), LifecycleUserError> {
|
||||
let credential = TuCredential::parse(contents)
|
||||
.map_err(|error| LifecycleUserError::InvalidCredential(error.to_string()))?;
|
||||
if credential.user_id != expected_user_id {
|
||||
return Err(LifecycleUserError::InvalidCredential(
|
||||
"credential user ID does not match deletion target".into(),
|
||||
));
|
||||
}
|
||||
inspect_credential_account(connection, &credential).await?;
|
||||
credential_proof(connection, &credential, CommunicationType::DeleteUserCredentialBegin, CommunicationType::DeleteUserCredentialChallenge, CommunicationType::DeleteUserCredentialComplete, b"tensamin:user-delete:v1\0").await
|
||||
credential_proof(
|
||||
connection,
|
||||
&credential,
|
||||
CommunicationType::DeleteUserCredentialBegin,
|
||||
CommunicationType::DeleteUserCredentialChallenge,
|
||||
CommunicationType::DeleteUserCredentialComplete,
|
||||
b"tensamin:user-delete:v1\0",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Repair local management state after a release or migration committed in
|
||||
/// Omega but local cleanup was interrupted. Hosted data is retained.
|
||||
pub async fn reconcile_managed_users(connection: &dyn OmikronClient) {
|
||||
let Ok(local_iota_id) = configured_iota_id() else { return; };
|
||||
let Ok(local_iota_id) = configured_iota_id() else {
|
||||
return;
|
||||
};
|
||||
for user in iota_storage::users::user_manager::get_users() {
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData)
|
||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user.user_id.into()));
|
||||
let Ok(response) = connection.await_response(&request, Duration::from_secs(10)).await else { continue; };
|
||||
let remote_iota_id = response.get_data(DataType::IotaId).as_signed_number().and_then(|value| i64::try_from(value).ok());
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::SignedNumber(user.user_id.into()),
|
||||
);
|
||||
let Ok(response) = connection
|
||||
.await_response(&request, Duration::from_secs(10))
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let remote_iota_id = response
|
||||
.get_data(DataType::IotaId)
|
||||
.as_signed_number()
|
||||
.and_then(|value| i64::try_from(value).ok());
|
||||
if remote_iota_id != Some(local_iota_id) {
|
||||
let _ = iota_storage::users::user_manager::release_user(user.user_id);
|
||||
}
|
||||
|
|
@ -268,7 +358,12 @@ pub async fn create_user(
|
|||
log!("Created User");
|
||||
write_user_credential(
|
||||
user_id,
|
||||
&format!("{}@{}::{}", user_id, omega_discovery::omega_host(), keyring_b64),
|
||||
&format!(
|
||||
"{}@{}::{}",
|
||||
user_id,
|
||||
omega_discovery::omega_host(),
|
||||
keyring_b64
|
||||
),
|
||||
)
|
||||
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
|
||||
|
||||
|
|
@ -282,6 +377,7 @@ mod tests {
|
|||
use super::{CreateUserError, request_user_id, valid_username};
|
||||
use crate::{OmikronClient, OmikronError};
|
||||
use async_trait::async_trait;
|
||||
use iota_util::mtp_compat::CommunicationValueCompat;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue