use std::sync::Arc;
use tightbeam::{
crypto::{ecies::Secp256k1EciesMessage, profiles::DefaultCryptoProvider, profiles::SecurityProfileDesc},
der::{Decode, Encode},
exactly, job, tb_assert_spec, tb_process_spec, tb_scenario,
testing::ScenarioConf,
trace::TraceCollector,
transport::handshake::{
client::EciesHandshakeClient,
negotiation::{SecurityAccept, SecurityOffer},
server::EciesHandshakeServer,
ClientHello, ServerHandshake,
},
TightBeamError,
};
use crate::common::security::{
expectation_failure, pinning_validator, strong_security_profile, weak_security_profile, ServerMaterials,
};
type EciesClient = EciesHandshakeClient<DefaultCryptoProvider, Secp256k1EciesMessage>;
type EciesServer = EciesHandshakeServer<DefaultCryptoProvider>;
tb_assert_spec! {
pub TranscriptBindingSpec,
V(1,0,0): {
mode: Accept,
gate: Accepted,
assertions: [
("tampered_accept_rejected", exactly!(1u32)),
("stripped_offer_rejected", exactly!(1u32))
]
}
}
tb_process_spec! {
pub TranscriptBindingProcess,
events {
observable { "tampered_accept_rejected", "stripped_offer_rejected" }
hidden { }
}
states {
Idle => { "tampered_accept_rejected" => AcceptBound },
AcceptBound => { "stripped_offer_rejected" => Done },
Done => { }
}
terminal { Done }
annotations { description: "Transcript binding: negotiated profile and client offer must be authenticated" }
}
tb_scenario! {
name: transcript_binding,
config: ScenarioConf::<()>::builder()
.with_spec(TranscriptBindingSpec::latest())
.with_csp(TranscriptBindingProcess)
.build(),
environment Bare {
exec: |trace| async move {
TranscriptBindingScenario::run((trace,)).await
}
}
}
fn strong_weak_pair(
materials: &ServerMaterials,
) -> (EciesClient, EciesServer, SecurityProfileDesc, SecurityProfileDesc) {
let strong = strong_security_profile();
let weak = weak_security_profile();
let validator = pinning_validator(&materials.certificate);
let client = EciesHandshakeClient::<DefaultCryptoProvider, Secp256k1EciesMessage>::new(None)
.with_security_offer(SecurityOffer::new(vec![strong, weak]))
.with_certificate_validator(validator);
let server = EciesHandshakeServer::<DefaultCryptoProvider>::new(
Arc::clone(&materials.key_provider),
Arc::clone(&materials.certificate),
None,
None,
)
.with_supported_profiles(vec![strong, weak]);
(client, server, strong, weak)
}
async fn expect_client_reject<E>(
result: Result<Vec<u8>, E>,
trace: &TraceCollector,
event: &'static str,
on_accept: &'static str,
) -> Result<(), TightBeamError> {
match result {
Err(_) => {
trace.event(event)?;
Ok(())
}
Ok(_) => Err(expectation_failure(on_accept)),
}
}
job! {
name: TranscriptBindingScenario,
async fn run((trace,): (Arc<TraceCollector>,)) -> Result<(), TightBeamError> {
let materials = ServerMaterials::generate();
let (mut client, mut server, strong, weak) = strong_weak_pair(&materials);
let client_hello = client.build_client_hello()?;
let server_handshake_der = server.process_client_hello(&client_hello).await?;
let mut server_handshake = ServerHandshake::from_der(&server_handshake_der)?;
assert_eq!(
server_handshake.security_accept.as_ref().map(|a| a.profile),
Some(strong),
"server must select the strong profile"
);
server_handshake.security_accept = Some(SecurityAccept::new(weak));
let tampered = server_handshake.to_der()?;
expect_client_reject(
client.process_server_handshake(&tampered).await,
&trace,
"tampered_accept_rejected",
"client accepted a tampered, unauthenticated security_accept",
)
.await?;
let (mut client, mut server, _strong, _weak) = strong_weak_pair(&materials);
let client_hello = client.build_client_hello()?;
let mut stripped_hello = ClientHello::from_der(&client_hello)?;
stripped_hello.security_offer = None;
let stripped_hello = stripped_hello.to_der()?;
assert_ne!(
stripped_hello, client_hello,
"offer stripping must change ClientHello bytes"
);
let server_handshake_der = server.process_client_hello(&stripped_hello).await?;
expect_client_reject(
client.process_server_handshake(&server_handshake_der).await,
&trace,
"stripped_offer_rejected",
"client accepted a signature over a rewritten ClientHello",
)
.await?;
Ok(())
}
}