use crate::iroh_carrier_bootstrap::CarrierBootstrapKind;
pub const CARRIER_CANDIDATE_PROOF_TYPE: &str = "#openrtc-iroh-carrier-proof";
pub const CARRIER_CANDIDATE_PROOF_VERSION: u8 = 1;
pub(crate) const CARRIER_CANDIDATE_PROOF_DELIVERY_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(20);
pub(crate) async fn read_candidate_proof_stream(
recv: &mut crate::application_crypto_streams::PeerRecvStream,
) -> anyhow::Result<Vec<u8>> {
const MAX_PROOF_BYTES: usize = 2_048;
let mut payload = Vec::new();
loop {
let mut chunk = [0_u8; 512];
let read = recv.read(&mut chunk).await?;
if read == 0 {
break;
}
anyhow::ensure!(
payload.len().saturating_add(read) <= MAX_PROOF_BYTES,
"carrier candidate proof exceeds {MAX_PROOF_BYTES} bytes"
);
payload.extend_from_slice(&chunk[..read]);
}
anyhow::ensure!(!payload.is_empty(), "carrier candidate proof is empty");
Ok(payload)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CarrierCandidateProofRole {
Probe,
Ack,
Commit,
Committed,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CarrierCandidateProofFrame {
#[serde(rename = "type")]
pub frame_type: String,
pub version: u8,
pub role: CarrierCandidateProofRole,
pub carrier: CarrierBootstrapKind,
pub transport_id: u64,
pub upgrade_id: String,
pub challenge: String,
pub base_transport_generation: u64,
pub base_route_generation: u64,
}
impl CarrierCandidateProofFrame {
pub fn probe(
carrier: CarrierBootstrapKind,
transport_id: u64,
upgrade_id: impl Into<String>,
base_transport_generation: u64,
base_route_generation: u64,
) -> Result<Self, CarrierCandidateProofError> {
let mut challenge = [0u8; 16];
getrandom::getrandom(&mut challenge)
.map_err(|_| CarrierCandidateProofError::EntropyUnavailable)?;
let frame = Self {
frame_type: CARRIER_CANDIDATE_PROOF_TYPE.to_string(),
version: CARRIER_CANDIDATE_PROOF_VERSION,
role: CarrierCandidateProofRole::Probe,
carrier,
transport_id,
upgrade_id: upgrade_id.into(),
challenge: hex::encode(challenge),
base_transport_generation,
base_route_generation,
};
frame.validate()?;
Ok(frame)
}
pub fn ack_from(probe: &Self) -> Result<Self, CarrierCandidateProofError> {
probe.validate()?;
if probe.role != CarrierCandidateProofRole::Probe {
return Err(CarrierCandidateProofError::InvalidTransition);
}
let mut ack = probe.clone();
ack.role = CarrierCandidateProofRole::Ack;
Ok(ack)
}
pub fn commit_from(probe: &Self) -> Result<Self, CarrierCandidateProofError> {
probe.validate()?;
if probe.role != CarrierCandidateProofRole::Probe {
return Err(CarrierCandidateProofError::InvalidTransition);
}
let mut commit = probe.clone();
commit.role = CarrierCandidateProofRole::Commit;
Ok(commit)
}
pub fn committed_from(commit: &Self) -> Result<Self, CarrierCandidateProofError> {
commit.validate()?;
if commit.role != CarrierCandidateProofRole::Commit {
return Err(CarrierCandidateProofError::InvalidTransition);
}
let mut committed = commit.clone();
committed.role = CarrierCandidateProofRole::Committed;
Ok(committed)
}
pub fn validate(&self) -> Result<(), CarrierCandidateProofError> {
if self.frame_type != CARRIER_CANDIDATE_PROOF_TYPE {
return Err(CarrierCandidateProofError::WrongFrameType);
}
if self.version != CARRIER_CANDIDATE_PROOF_VERSION {
return Err(CarrierCandidateProofError::UnsupportedVersion);
}
if self.transport_id == 0 {
return Err(CarrierCandidateProofError::InvalidTransportId);
}
if !(8..=96).contains(&self.upgrade_id.len())
|| !self
.upgrade_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
return Err(CarrierCandidateProofError::InvalidUpgradeId);
}
if self.challenge.len() != 32
|| !self
.challenge
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(CarrierCandidateProofError::InvalidChallenge);
}
Ok(())
}
pub fn matches_probe(&self, probe: &Self) -> bool {
self.validate().is_ok()
&& probe.validate().is_ok()
&& self.role == CarrierCandidateProofRole::Ack
&& probe.role == CarrierCandidateProofRole::Probe
&& self.carrier == probe.carrier
&& self.transport_id == probe.transport_id
&& self.upgrade_id == probe.upgrade_id
&& self.challenge == probe.challenge
&& self.base_transport_generation == probe.base_transport_generation
&& self.base_route_generation == probe.base_route_generation
}
pub fn matches_commit_for_probe(&self, probe: &Self) -> bool {
self.validate().is_ok()
&& probe.validate().is_ok()
&& self.role == CarrierCandidateProofRole::Commit
&& probe.role == CarrierCandidateProofRole::Probe
&& self.carrier == probe.carrier
&& self.transport_id == probe.transport_id
&& self.upgrade_id == probe.upgrade_id
&& self.challenge == probe.challenge
&& self.base_transport_generation == probe.base_transport_generation
&& self.base_route_generation == probe.base_route_generation
}
pub fn matches_committed_for_commit(&self, commit: &Self) -> bool {
self.validate().is_ok()
&& commit.validate().is_ok()
&& self.role == CarrierCandidateProofRole::Committed
&& commit.role == CarrierCandidateProofRole::Commit
&& self.carrier == commit.carrier
&& self.transport_id == commit.transport_id
&& self.upgrade_id == commit.upgrade_id
&& self.challenge == commit.challenge
&& self.base_transport_generation == commit.base_transport_generation
&& self.base_route_generation == commit.base_route_generation
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierCandidateProofError {
EntropyUnavailable,
WrongFrameType,
UnsupportedVersion,
InvalidTransportId,
InvalidUpgradeId,
InvalidChallenge,
InvalidTransition,
}
impl std::fmt::Display for CarrierCandidateProofError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{self:?}")
}
}
impl std::error::Error for CarrierCandidateProofError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ack_echoes_complete_probe_fence() {
let probe = CarrierCandidateProofFrame::probe(
CarrierBootstrapKind::Ble,
0x424c45,
"upgrade-1234",
7,
2,
)
.unwrap();
let ack = CarrierCandidateProofFrame::ack_from(&probe).unwrap();
assert!(ack.matches_probe(&probe));
}
#[test]
fn stale_or_cross_transport_ack_is_rejected() {
let probe = CarrierCandidateProofFrame::probe(
CarrierBootstrapKind::WebRtc,
41,
"upgrade-1234",
7,
2,
)
.unwrap();
let mut ack = CarrierCandidateProofFrame::ack_from(&probe).unwrap();
ack.transport_id = 42;
assert!(!ack.matches_probe(&probe));
}
#[test]
fn commit_confirmation_echoes_the_proven_candidate() {
let probe = CarrierCandidateProofFrame::probe(
CarrierBootstrapKind::WebRtc,
41,
"upgrade-1234",
7,
2,
)
.unwrap();
let commit = CarrierCandidateProofFrame::commit_from(&probe).unwrap();
assert!(commit.matches_commit_for_probe(&probe));
let committed = CarrierCandidateProofFrame::committed_from(&commit).unwrap();
assert!(committed.matches_committed_for_commit(&commit));
}
}