[Add] Ease of use functions
Some checks failed
CI / rustfmt (push) Failing after 17s
CI / wasm build (push) Failing after 1m13s
CI / clippy (push) Failing after 1m17s
CI / example (push) Failing after 1m30s
CI / test (push) Successful in 1m50s
CI / duplicate code (push) Failing after 31s
CI / web client (push) Failing after 31s
CI / cargo-machete (push) Successful in 1m15s
CI / cargo-deny (push) Failing after 2m26s
Some checks failed
CI / rustfmt (push) Failing after 17s
CI / wasm build (push) Failing after 1m13s
CI / clippy (push) Failing after 1m17s
CI / example (push) Failing after 1m30s
CI / test (push) Successful in 1m50s
CI / duplicate code (push) Failing after 31s
CI / web client (push) Failing after 31s
CI / cargo-machete (push) Successful in 1m15s
CI / cargo-deny (push) Failing after 2m26s
This commit is contained in:
parent
5caa1c9d5f
commit
6ef1293603
15 changed files with 1203 additions and 124 deletions
|
|
@ -9,6 +9,7 @@ mtp-codec = { path = "../codec" }
|
|||
mtp-transport = { path = "../transport" }
|
||||
mtp-crypto = { path = "../crypto", optional = true }
|
||||
rand = "0.8"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
|
||||
[features]
|
||||
crypto = ["dep:mtp-crypto", "mtp-codec/crypto"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use mtp_codec::{CommunicationValue, DataType, DataValue, PROTOCOL_VERSION, Version};
|
||||
use mtp_common::CommunicationError;
|
||||
use mtp_transport::{Policy, Receiver, Sender};
|
||||
use tokio::time::Duration;
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
fn unexpected_response_type_error(
|
||||
|
|
@ -20,6 +21,8 @@ pub struct ClientConfig {
|
|||
pub url: String,
|
||||
pub tls: ClientTlsConfig,
|
||||
pub client_id: u64,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -34,6 +37,8 @@ impl ClientConfig {
|
|||
url: url.into(),
|
||||
tls: ClientTlsConfig::SystemRoots,
|
||||
client_id: 0,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +56,12 @@ impl ClientConfig {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.auth_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
fn server_cert(&self) -> Option<Vec<u8>> {
|
||||
match &self.tls {
|
||||
ClientTlsConfig::SystemRoots => None,
|
||||
|
|
@ -73,6 +84,7 @@ pub struct MTPConnection {
|
|||
#[cfg(feature = "crypto")]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuthState {
|
||||
Unauthenticated,
|
||||
Pending,
|
||||
Authenticated,
|
||||
Failed,
|
||||
|
|
@ -91,7 +103,6 @@ impl MTPClient {
|
|||
let (sender, receiver) =
|
||||
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||
|
||||
// Build the initial identification message with the protocol version.
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
|
|
@ -107,7 +118,7 @@ impl MTPClient {
|
|||
sender,
|
||||
receiver,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_state: AuthState::Authenticated,
|
||||
auth_state: AuthState::Unauthenticated,
|
||||
#[cfg(feature = "crypto")]
|
||||
client_id: config.client_id,
|
||||
})
|
||||
|
|
@ -126,14 +137,13 @@ impl MTPClient {
|
|||
#[cfg(feature = "crypto")]
|
||||
fn verify_host_challenge(
|
||||
challenge: &CommunicationValue,
|
||||
tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
server_challenge: u128,
|
||||
) -> Result<(), CommunicationError> {
|
||||
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
|
||||
|
||||
let sig = match challenge.get_data(DataType::Signature.to_id(tm)) {
|
||||
let sig = match challenge.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
|
|
@ -141,7 +151,7 @@ fn verify_host_challenge(
|
|||
));
|
||||
}
|
||||
};
|
||||
let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) {
|
||||
let pq_sig = match challenge.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
|
@ -165,7 +175,6 @@ fn verify_host_challenge(
|
|||
#[cfg(feature = "crypto")]
|
||||
fn verify_host_final(
|
||||
response: &CommunicationValue,
|
||||
tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
client_nonce: u128,
|
||||
|
|
@ -173,7 +182,7 @@ fn verify_host_final(
|
|||
) -> Result<(), CommunicationError> {
|
||||
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
|
||||
|
||||
match response.get_data(DataType::ClientNonce.to_id(tm)) {
|
||||
match response.get_data(DataType::ClientNonce) {
|
||||
DataValue::UnsignedNumber(n) if *n == client_nonce => {}
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
|
|
@ -182,7 +191,7 @@ fn verify_host_final(
|
|||
}
|
||||
}
|
||||
|
||||
let sig = match response.get_data(DataType::Signature.to_id(tm)) {
|
||||
let sig = match response.get_data(DataType::Signature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
|
|
@ -190,7 +199,7 @@ fn verify_host_final(
|
|||
));
|
||||
}
|
||||
};
|
||||
let pq_sig = match response.get_data(DataType::PqSignature.to_id(tm)) {
|
||||
let pq_sig = match response.get_data(DataType::PqSignature) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
|
@ -210,10 +219,9 @@ fn verify_host_final(
|
|||
#[cfg(feature = "crypto")]
|
||||
fn check_connected(
|
||||
response: &CommunicationValue,
|
||||
tm: &mtp_codec::TypeMap,
|
||||
reject_msg: &str,
|
||||
) -> Result<(), CommunicationError> {
|
||||
match response.get_data(DataType::Connected.to_id(tm)) {
|
||||
match response.get_data(DataType::Connected) {
|
||||
DataValue::BoolTrue => Ok(()),
|
||||
DataValue::BoolFalse => Err(CommunicationError::AuthenticationFailed(reject_msg.into())),
|
||||
_ => Err(CommunicationError::AuthenticationFailed(
|
||||
|
|
@ -271,7 +279,7 @@ async fn receive_verified_challenge(
|
|||
));
|
||||
}
|
||||
|
||||
let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(tm)) {
|
||||
let server_challenge = match challenge.get_data(DataType::ServerNonce) {
|
||||
DataValue::UnsignedNumber(n) => *n,
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
|
|
@ -280,13 +288,7 @@ async fn receive_verified_challenge(
|
|||
}
|
||||
};
|
||||
|
||||
verify_host_challenge(
|
||||
&challenge,
|
||||
tm,
|
||||
host_public_key_bundle,
|
||||
bound_id,
|
||||
server_challenge,
|
||||
)?;
|
||||
verify_host_challenge(&challenge, host_public_key_bundle, bound_id, server_challenge)?;
|
||||
|
||||
Ok(server_challenge)
|
||||
}
|
||||
|
|
@ -297,6 +299,25 @@ impl MTPClient {
|
|||
config: ClientConfig,
|
||||
keys: &mtp_crypto::Keyring,
|
||||
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
|
||||
) -> Result<MTPConnection, CommunicationError> {
|
||||
let timeout = config.auth_timeout;
|
||||
match tokio::time::timeout(
|
||||
timeout,
|
||||
Self::auth_connect_inner(config, keys, host_public_key_bundle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(CommunicationError::AuthenticationFailed(
|
||||
"authentication timed out".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_connect_inner(
|
||||
config: ClientConfig,
|
||||
keys: &mtp_crypto::Keyring,
|
||||
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
|
||||
) -> Result<MTPConnection, CommunicationError> {
|
||||
use mtp_crypto::auth;
|
||||
|
||||
|
|
@ -313,17 +334,27 @@ impl MTPClient {
|
|||
DataType::Id,
|
||||
DataValue::UnsignedNumber(config.client_id as u128),
|
||||
);
|
||||
sender.send(&ident).await?;
|
||||
if let Err(e) = sender.send(&ident).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 2. Receive and verify the host's challenge.
|
||||
let server_challenge = receive_verified_challenge(
|
||||
let server_challenge = match receive_verified_challenge(
|
||||
&receiver,
|
||||
&tm,
|
||||
host_public_key_bundle,
|
||||
config.client_id,
|
||||
"auth_connect challenge",
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Sign the host's challenge and send the proof.
|
||||
let client_nonce: u128 = rand::random();
|
||||
|
|
@ -334,28 +365,49 @@ impl MTPClient {
|
|||
client_nonce,
|
||||
);
|
||||
|
||||
let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?;
|
||||
sender.send(&proof).await?;
|
||||
let proof = match signed_challenge_response(keys, &proof_payload, client_nonce) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if let Err(e) = sender.send(&proof).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 4. Receive and verify the host's final confirmation.
|
||||
let response = receiver.receive().await?;
|
||||
let response = match receiver.receive().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm);
|
||||
if response.get_type() != expected_type {
|
||||
sender.close();
|
||||
return Err(unexpected_response_type_error(
|
||||
"auth_connect",
|
||||
expected_type,
|
||||
&response,
|
||||
));
|
||||
}
|
||||
check_connected(&response, &tm, "Server rejected authentication")?;
|
||||
verify_host_final(
|
||||
if let Err(e) = check_connected(&response, "Server rejected authentication") {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
if let Err(e) = verify_host_final(
|
||||
&response,
|
||||
&tm,
|
||||
host_public_key_bundle,
|
||||
config.client_id,
|
||||
client_nonce,
|
||||
server_challenge,
|
||||
)?;
|
||||
) {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(MTPConnection {
|
||||
version: PROTOCOL_VERSION,
|
||||
|
|
@ -370,6 +422,25 @@ impl MTPClient {
|
|||
config: ClientConfig,
|
||||
keys: &mtp_crypto::Keyring,
|
||||
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
|
||||
) -> Result<MTPConnection, CommunicationError> {
|
||||
let timeout = config.auth_timeout;
|
||||
match tokio::time::timeout(
|
||||
timeout,
|
||||
Self::auth_register_inner(config, keys, host_public_key_bundle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(CommunicationError::AuthenticationFailed(
|
||||
"authentication timed out".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_register_inner(
|
||||
config: ClientConfig,
|
||||
keys: &mtp_crypto::Keyring,
|
||||
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
|
||||
) -> Result<MTPConnection, CommunicationError> {
|
||||
use mtp_crypto::auth;
|
||||
|
||||
|
|
@ -385,54 +456,86 @@ impl MTPClient {
|
|||
let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
||||
sender.send(®ister).await?;
|
||||
if let Err(e) = sender.send(®ister).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 2. Receive and verify the host's challenge (register binds id = 0).
|
||||
let server_challenge = receive_verified_challenge(
|
||||
let server_challenge = match receive_verified_challenge(
|
||||
&receiver,
|
||||
&tm,
|
||||
host_public_key_bundle,
|
||||
0,
|
||||
"auth_register challenge",
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Sign the host's challenge over the bundle and send the proof.
|
||||
let client_nonce: u128 = rand::random();
|
||||
let proof_payload =
|
||||
auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce);
|
||||
|
||||
let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?;
|
||||
sender.send(&proof).await?;
|
||||
let proof = match signed_challenge_response(keys, &proof_payload, client_nonce) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if let Err(e) = sender.send(&proof).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 4. Receive the host's final confirmation; extract the assigned id and
|
||||
// verify the host signature binds to it.
|
||||
let response = receiver.receive().await?;
|
||||
let response = match receiver.receive().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm);
|
||||
if response.get_type() != expected_type {
|
||||
sender.close();
|
||||
return Err(unexpected_response_type_error(
|
||||
"auth_register",
|
||||
expected_type,
|
||||
&response,
|
||||
));
|
||||
}
|
||||
check_connected(&response, &tm, "Server rejected registration")?;
|
||||
let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) {
|
||||
if let Err(e) = check_connected(&response, "Server rejected registration") {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
let assigned_id = match response.get_data(DataType::Id) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => {
|
||||
sender.close();
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Missing assigned ID".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
verify_host_final(
|
||||
if let Err(e) = verify_host_final(
|
||||
&response,
|
||||
&tm,
|
||||
host_public_key_bundle,
|
||||
assigned_id,
|
||||
client_nonce,
|
||||
server_challenge,
|
||||
)?;
|
||||
) {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(MTPConnection {
|
||||
version: PROTOCOL_VERSION,
|
||||
|
|
@ -470,8 +573,23 @@ mod tests {
|
|||
|
||||
#[cfg(feature = "crypto")]
|
||||
#[test]
|
||||
fn test_auth_state_derive() {
|
||||
assert_eq!(AuthState::Pending, AuthState::Pending);
|
||||
assert_ne!(AuthState::Authenticated, AuthState::Failed);
|
||||
fn test_auth_state_unauthenticated_is_not_authenticated() {
|
||||
assert_ne!(AuthState::Unauthenticated, AuthState::Authenticated);
|
||||
assert_ne!(AuthState::Pending, AuthState::Authenticated);
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
#[test]
|
||||
fn test_auth_timeout_default() {
|
||||
let config = ClientConfig::new("https://localhost:4433");
|
||||
assert_eq!(config.auth_timeout, Duration::from_secs(30));
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
#[test]
|
||||
fn test_auth_timeout_custom() {
|
||||
let config = ClientConfig::new("https://localhost:4433")
|
||||
.with_auth_timeout(Duration::from_secs(10));
|
||||
assert_eq!(config.auth_timeout, Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue