Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **P2P**: Identify protocol no longer rejects peers that advertise unknown
protocol strings, improving forward compatibility with newer nodes
([#2200](https://github.com/o1-labs/mina-rust/pull/2200))
- **P2P**: Improved error types for WebRTC connection signaling and fixed
broken doc tests in the webrtc module
([#2202](https://github.com/o1-labs/mina-rust/pull/2202))

### Removed

- **P2P**: Deprecated legacy WebRTC address format (`/{peer_id}/{signaling}`);
Expand Down
15 changes: 12 additions & 3 deletions crates/p2p/src/channels/p2p_channels_effectful_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use crate::webrtc::{Offer, P2pConnectionResponse};
use super::{
signaling::{
discovery::{P2pChannelsSignalingDiscoveryAction, SignalingDiscoveryChannelMsg},
exchange::{P2pChannelsSignalingExchangeAction, SignalingExchangeChannelMsg},
exchange::{
OfferDecryptErrorKind, P2pChannelsSignalingExchangeAction, SignalingExchangeChannelMsg,
},
},
ChannelMsg, MsgId, P2pChannelsEffectfulAction, P2pChannelsService,
};
Expand Down Expand Up @@ -85,13 +87,20 @@ impl P2pChannelsEffectfulAction {
Err(_) => {
store.dispatch(P2pChannelsSignalingExchangeAction::OfferDecryptError {
peer_id,
error: OfferDecryptErrorKind::DecryptionFailed,
});
}
Ok(offer) if offer.identity_pub_key != pub_key => {
// TODO(binier): propagate specific error.
// This is invalid behavior either from relayer or offerer.
// The offer's embedded identity key doesn't match the
// expected public key. This indicates a compromised relay,
// offer tampering, or wrong key pair usage.
//
// We deliberately respond with SignalDecryptionFailed
// rather than a specific mismatch error to avoid leaking
// information to a potential attacker.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably be some kind of log here, right?

store.dispatch(P2pChannelsSignalingExchangeAction::OfferDecryptError {
peer_id,
error: OfferDecryptErrorKind::IdentityKeyMismatch,
});
}
Ok(offer) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,9 @@ impl P2pChannelsSignalingDiscoveryState {

let dispatcher = state_context.into_dispatcher();
match answer {
// TODO(binier): custom error
None => dispatcher.push(P2pConnectionOutgoingAction::AnswerRecvError {
peer_id: target_public_key.peer_id(),
error: P2pConnectionErrorResponse::InternalError,
error: P2pConnectionErrorResponse::AnswerNotProvided,
}),
Some(answer) => dispatcher.push(
P2pChannelsEffectfulAction::SignalingDiscoveryAnswerDecrypt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ use crate::{

use super::{P2pChannelsSignalingExchangeState, SignalingExchangeState};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OfferDecryptErrorKind {
DecryptionFailed,
IdentityKeyMismatch,
}

#[derive(Debug, Clone, Serialize, Deserialize, ActionEvent)]
#[action_event(fields(display(peer_id)))]
pub enum P2pChannelsSignalingExchangeAction {
Expand All @@ -36,6 +42,7 @@ pub enum P2pChannelsSignalingExchangeAction {
},
OfferDecryptError {
peer_id: PeerId,
error: OfferDecryptErrorKind,
},
OfferDecryptSuccess {
peer_id: PeerId,
Expand Down Expand Up @@ -126,7 +133,7 @@ impl redux::EnablingCondition<P2pState> for P2pChannelsSignalingExchangeAction {
}
_ => false,
}),
P2pChannelsSignalingExchangeAction::OfferDecryptError { peer_id } => state
P2pChannelsSignalingExchangeAction::OfferDecryptError { peer_id, .. } => state
.get_ready_peer(peer_id)
.is_some_and(|p| match &p.channels.signaling.exchange {
P2pChannelsSignalingExchangeState::Ready { local, .. } => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,14 @@ impl P2pChannelsSignalingExchangeState {
});
Ok(())
}
P2pChannelsSignalingExchangeAction::OfferDecryptError { .. } => {
P2pChannelsSignalingExchangeAction::OfferDecryptError { error, .. } => {
if matches!(error, super::OfferDecryptErrorKind::IdentityKeyMismatch) {
tracing::warn!(
%peer_id,
"offer identity key mismatch: possible relay \
tampering or spoofed offer"
);
}
let dispatcher = state_context.into_dispatcher();
let answer = P2pConnectionResponse::SignalDecryptionFailed;
dispatcher.push(P2pChannelsSignalingExchangeAction::AnswerSend { peer_id, answer });
Expand Down
2 changes: 2 additions & 0 deletions crates/p2p/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ pub enum P2pConnectionErrorResponse {
SignalDecryptionFailed,
#[error("internal error")]
InternalError,
#[error("answer not provided")]
AnswerNotProvided,
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ impl redux::EnablingCondition<P2pState> for P2pConnectionOutgoingAction {
}
P2pConnectionOutgoingError::Rejected(_)
| P2pConnectionOutgoingError::RemoteSignalDecryptionFailed
| P2pConnectionOutgoingError::RemoteInternalError => {
| P2pConnectionOutgoingError::RemoteInternalError
| P2pConnectionOutgoingError::RemoteAnswerNotProvided => {
matches!(s, P2pConnectionOutgoingState::AnswerRecvPending { .. })
}
P2pConnectionOutgoingError::FinalizeError(_) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ impl P2pConnectionOutgoingState {
P2pConnectionErrorResponse::InternalError => {
P2pConnectionOutgoingError::RemoteInternalError
}
P2pConnectionErrorResponse::AnswerNotProvided => {
P2pConnectionOutgoingError::RemoteAnswerNotProvided
}
},
});
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ pub enum P2pConnectionOutgoingError {
RemoteSignalDecryptionFailed,
#[error("remote internal error")]
RemoteInternalError,
#[error("remote answer not provided")]
RemoteAnswerNotProvided,
#[error("finalization error: {0}")]
FinalizeError(String),
#[error("connection authorization error")]
Expand Down
116 changes: 108 additions & 8 deletions crates/p2p/src/webrtc/connection_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,29 @@ use super::{Answer, Offer};
/// ## Usage
///
/// ```rust
/// use mina_p2p::webrtc::{ConnectionAuth, Offer, Answer};
/// use mina_p2p::webrtc::{ConnectionAuth, Offer, Answer, Host};
/// use mina_p2p::identity::SecretKey;
/// use rand::thread_rng;
///
/// let sk_a = SecretKey::rand();
/// let sk_b = SecretKey::rand();
/// let offer = Offer {
/// sdp: "v=0\r\no=- 0 0 IN IP4 127.0.0.1".into(),
/// chain_id: mina_core::DEVNET_CHAIN_ID,
/// identity_pub_key: sk_a.public_key(),
/// target_peer_id: sk_b.public_key().peer_id(),
/// host: Host::Domain("localhost".into()),
/// listen_port: Some(8080),
/// };
/// let answer = Answer {
/// sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1".into(),
/// identity_pub_key: sk_b.public_key(),
/// target_peer_id: sk_a.public_key().peer_id(),
/// };
///
/// let connection_auth = ConnectionAuth::new(&offer, &answer);
/// let encrypted_auth = connection_auth.encrypt(&my_secret_key, &peer_public_key, rng)?;
/// let encrypted_auth = connection_auth.encrypt(&sk_a, &sk_b.public_key(), thread_rng());
/// assert!(encrypted_auth.is_some());
/// ```
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct ConnectionAuth(Vec<u8>);
Expand Down Expand Up @@ -124,9 +143,32 @@ pub struct ConnectionAuth(Vec<u8>);
/// ## Example
///
/// ```rust
/// use mina_p2p::webrtc::{ConnectionAuth, ConnectionAuthEncrypted, Offer, Answer, Host};
/// use mina_p2p::identity::SecretKey;
/// use rand::thread_rng;
///
/// let sk_a = SecretKey::rand();
/// let sk_b = SecretKey::rand();
/// let offer = Offer {
/// sdp: "v=0\r\no=- 0 0 IN IP4 127.0.0.1".into(),
/// chain_id: mina_core::DEVNET_CHAIN_ID,
/// identity_pub_key: sk_a.public_key(),
/// target_peer_id: sk_b.public_key().peer_id(),
/// host: Host::Domain("localhost".into()),
/// listen_port: Some(8080),
/// };
/// let answer = Answer {
/// sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1".into(),
/// identity_pub_key: sk_b.public_key(),
/// target_peer_id: sk_a.public_key().peer_id(),
/// };
///
/// let auth = ConnectionAuth::new(&offer, &answer);
/// let encrypted_auth = auth.encrypt(&sk_a, &sk_b.public_key(), thread_rng()).unwrap();
///
/// // After receiving encrypted authentication data
/// let decrypted_auth = encrypted_auth.decrypt(&my_secret_key, &peer_public_key)?;
/// // Verify that the decrypted data matches expected values
/// let decrypted_auth = encrypted_auth.decrypt(&sk_b, &sk_a.public_key());
/// assert!(decrypted_auth.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct ConnectionAuthEncrypted(Box<[u8; 92]>);
Expand Down Expand Up @@ -158,7 +200,24 @@ impl ConnectionAuth {
/// # Example
///
/// ```rust
/// use mina_p2p::webrtc::ConnectionAuth;
/// use mina_p2p::webrtc::{ConnectionAuth, Offer, Answer, Host};
/// use mina_p2p::identity::SecretKey;
///
/// let sk_a = SecretKey::rand();
/// let sk_b = SecretKey::rand();
/// let offer = Offer {
/// sdp: "v=0\r\no=- 0 0 IN IP4 127.0.0.1".into(),
/// chain_id: mina_core::DEVNET_CHAIN_ID,
/// identity_pub_key: sk_a.public_key(),
/// target_peer_id: sk_b.public_key().peer_id(),
/// host: Host::Domain("localhost".into()),
/// listen_port: Some(8080),
/// };
/// let answer = Answer {
/// sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1".into(),
/// identity_pub_key: sk_b.public_key(),
/// target_peer_id: sk_a.public_key().peer_id(),
/// };
///
/// let auth = ConnectionAuth::new(&offer, &answer);
/// // Use auth for connection verification
Expand Down Expand Up @@ -196,10 +255,28 @@ impl ConnectionAuth {
/// # Example
///
/// ```rust
/// use mina_p2p::webrtc::{ConnectionAuth, Offer, Answer, Host};
/// use mina_p2p::identity::SecretKey;
/// use rand::thread_rng;
///
/// let mut rng = thread_rng();
/// let encrypted_auth = connection_auth.encrypt(&my_secret_key, &peer_public_key, &mut rng);
/// let sk_a = SecretKey::rand();
/// let sk_b = SecretKey::rand();
/// let offer = Offer {
/// sdp: "v=0\r\no=- 0 0 IN IP4 127.0.0.1".into(),
/// chain_id: mina_core::DEVNET_CHAIN_ID,
/// identity_pub_key: sk_a.public_key(),
/// target_peer_id: sk_b.public_key().peer_id(),
/// host: Host::Domain("localhost".into()),
/// listen_port: Some(8080),
/// };
/// let answer = Answer {
/// sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1".into(),
/// identity_pub_key: sk_b.public_key(),
/// target_peer_id: sk_a.public_key().peer_id(),
/// };
///
/// let connection_auth = ConnectionAuth::new(&offer, &answer);
/// let encrypted_auth = connection_auth.encrypt(&sk_a, &sk_b.public_key(), thread_rng());
///
/// if let Some(encrypted) = encrypted_auth {
/// // Send encrypted authentication data to peer
Expand Down Expand Up @@ -254,8 +331,31 @@ impl ConnectionAuthEncrypted {
/// # Example
///
/// ```rust
/// use mina_p2p::webrtc::{ConnectionAuth, Offer, Answer, Host};
/// use mina_p2p::identity::SecretKey;
/// use rand::thread_rng;
///
/// let sk_a = SecretKey::rand();
/// let sk_b = SecretKey::rand();
/// let offer = Offer {
/// sdp: "v=0\r\no=- 0 0 IN IP4 127.0.0.1".into(),
/// chain_id: mina_core::DEVNET_CHAIN_ID,
/// identity_pub_key: sk_a.public_key(),
/// target_peer_id: sk_b.public_key().peer_id(),
/// host: Host::Domain("localhost".into()),
/// listen_port: Some(8080),
/// };
/// let answer = Answer {
/// sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1".into(),
/// identity_pub_key: sk_b.public_key(),
/// target_peer_id: sk_a.public_key().peer_id(),
/// };
///
/// let auth = ConnectionAuth::new(&offer, &answer);
/// let encrypted_auth = auth.encrypt(&sk_a, &sk_b.public_key(), thread_rng()).unwrap();
///
/// // After receiving encrypted authentication data from peer
/// if let Some(decrypted_auth) = encrypted_auth.decrypt(&my_secret_key, &peer_public_key) {
/// if let Some(decrypted_auth) = encrypted_auth.decrypt(&sk_b, &sk_a.public_key()) {
/// // Authentication successful, proceed with connection
/// println!("Peer authentication verified");
/// } else {
Expand Down
Loading