diff --git a/crates/op-collab-relay-control-plane/src/pairing_wire_tests.rs b/crates/op-collab-relay-control-plane/src/pairing_wire_tests.rs new file mode 100644 index 000000000..b40b34686 --- /dev/null +++ b/crates/op-collab-relay-control-plane/src/pairing_wire_tests.rs @@ -0,0 +1,176 @@ +//! Pairing publish/claim wire-format tests, split from `tests.rs` at the +//! 800-line cap. + +use crate::{ + PairingClaimRequest, PairingPublishRequest, RelayLocatorIssueError, MAX_PAIRING_CODE_TTL_SECS, + MAX_PAIRING_PUBLISH_REQUEST_BYTES, PAIRING_CLAIM_REQUEST_BYTES, +}; + +#[test] +fn pairing_publish_round_trips_and_bounds_every_field() { + let request = + PairingPublishRequest::new([7; 32], [9; 16], 600, vec![1, 2, 3, 4]).expect("valid"); + let raw = request.encode_binary(); + assert!(raw.len() <= MAX_PAIRING_PUBLISH_REQUEST_BYTES); + let decoded = PairingPublishRequest::decode_binary(&raw).expect("round trip"); + assert_eq!(decoded, request); + + // Truncation and trailing bytes both fail the exact length check. + for length in 0..raw.len() { + assert!(PairingPublishRequest::decode_binary(&raw[..length]).is_err()); + } + let mut trailing = raw.clone(); + trailing.push(0); + assert!(PairingPublishRequest::decode_binary(&trailing).is_err()); + + let mut wrong_version = raw; + wrong_version[0] ^= 0xFF; + assert!(matches!( + PairingPublishRequest::decode_binary(&wrong_version), + Err(RelayLocatorIssueError::UnsupportedRequestVersion { .. }) + )); + + // TTL and sealed-blob bounds are enforced at construction. + assert!(PairingPublishRequest::new([7; 32], [9; 16], 0, vec![1]).is_err()); + assert!( + PairingPublishRequest::new([7; 32], [9; 16], MAX_PAIRING_CODE_TTL_SECS + 1, vec![1]) + .is_err() + ); + assert!(PairingPublishRequest::new([7; 32], [9; 16], 600, Vec::new()).is_err()); +} + +#[test] +fn pairing_claim_round_trips_and_is_exact() { + let request = PairingClaimRequest::new([3; 32], [5; 16]); + let raw = request.encode_binary(); + assert_eq!(raw.len(), PAIRING_CLAIM_REQUEST_BYTES); + let decoded = PairingClaimRequest::decode_binary(&raw).expect("round trip"); + assert_eq!(decoded, request); + for length in 0..raw.len() { + assert!(PairingClaimRequest::decode_binary(&raw[..length]).is_err()); + } + let mut trailing = raw.to_vec(); + trailing.push(0); + assert!(PairingClaimRequest::decode_binary(&trailing).is_err()); + assert_eq!(format!("{request:?}"), "PairingClaimRequest([REDACTED])"); +} + +mod pairing_service_tests { + use std::sync::{Arc, Mutex}; + use std::time::Instant; + + use op_auth_bridge::{ + CollabJwksCacheLimits, CollabTicketVerifier, OpaqueCollabTicket, StaticTestJwksFetcher, + TestCollabIssuer, TestCollabTicketSpec, + }; + + use crate::{ + PairingClaimRequest, PairingCodeStore, PairingPublishRequest, PairingPutOutcome, + PairingStoreRejection, RelayPairingService, RelayPairingServiceError, + MAX_PAIRING_CODE_TTL_SECS, + }; + + type PutLog = Arc>>; + + struct RecordingStore(PutLog); + + impl PairingCodeStore for RecordingStore { + fn put( + &self, + owner: [u8; 32], + code_id: [u8; 16], + _sealed: Vec, + now_unix: u64, + expires_at_unix: u64, + ) -> Result { + self.0 + .lock() + .expect("puts lock") + .push(((owner, code_id), now_unix, expires_at_unix)); + Ok(PairingPutOutcome::Stored) + } + + fn claim(&self, _code_id: &[u8; 16], _now_unix: u64) -> Option> { + Some(vec![0xAB]) + } + } + + fn service_fixture( + device: [u8; 32], + ) -> ( + RelayPairingService, + OpaqueCollabTicket, + u64, + PutLog, + ) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_secs(); + let issuer = TestCollabIssuer::initial(); + let ticket = issuer + .issue(&TestCollabTicketSpec::valid_at(now, device)) + .expect("ticket"); + let verifier = CollabTicketVerifier::new( + TestCollabIssuer::verifier_config().expect("verifier config"), + StaticTestJwksFetcher::new(issuer.jwks_json().expect("JWKS"), 300), + CollabJwksCacheLimits::default(), + ) + .expect("ticket verifier"); + let log: PutLog = Arc::default(); + ( + RelayPairingService::new(verifier, RecordingStore(Arc::clone(&log))), + ticket, + now, + log, + ) + } + + #[test] + fn pairing_endpoints_reject_a_ticket_bound_to_a_different_device() { + let (service, ticket, now, log) = service_fixture([0x11; 32]); + // Body claims device 0x22 while the ticket is bound to 0x11. + let request = + PairingPublishRequest::new([0x22; 32], [9; 16], 600, vec![1, 2, 3]).expect("request"); + assert_eq!( + service.publish_at( + &request.encode_binary(), + ticket.expose(), + now, + Instant::now() + ), + Err(RelayPairingServiceError::AuthenticationFailed) + ); + assert!(log.lock().expect("puts lock").is_empty()); + + let claim = PairingClaimRequest::new([0x22; 32], [9; 16]); + assert_eq!( + service.claim_at(&claim.encode_binary(), ticket.expose(), now, Instant::now()), + Err(RelayPairingServiceError::AuthenticationFailed) + ); + } + + #[test] + fn publish_clamps_ttl_and_records_the_verified_device() { + let device = [0x11; 32]; + let (service, ticket, now, log) = service_fixture(device); + let request = + PairingPublishRequest::new(device, [7; 16], MAX_PAIRING_CODE_TTL_SECS, vec![1, 2, 3]) + .expect("request"); + service + .publish_at( + &request.encode_binary(), + ticket.expose(), + now, + Instant::now(), + ) + .expect("publish"); + let puts = log.lock().expect("puts lock").clone(); + assert_eq!(puts.len(), 1); + let ((owner, code_id), seen_now, expires) = puts[0]; + assert_eq!(owner, device); + assert_eq!(code_id, [7; 16]); + assert_eq!(seen_now, now); + assert_eq!(expires, now + u64::from(MAX_PAIRING_CODE_TTL_SECS)); + } +} diff --git a/crates/op-collab-relay-control-plane/src/tests.rs b/crates/op-collab-relay-control-plane/src/tests.rs index 9f7587a6f..4857f471f 100644 --- a/crates/op-collab-relay-control-plane/src/tests.rs +++ b/crates/op-collab-relay-control-plane/src/tests.rs @@ -671,181 +671,5 @@ fn signed_locator_at( SignedLocatorResponse::decode(&claims.attach_signature(signature).encode()).expect("response") } -mod pairing_wire_tests { - use crate::{ - PairingClaimRequest, PairingPublishRequest, RelayLocatorIssueError, - MAX_PAIRING_CODE_TTL_SECS, MAX_PAIRING_PUBLISH_REQUEST_BYTES, PAIRING_CLAIM_REQUEST_BYTES, - }; - - #[test] - fn pairing_publish_round_trips_and_bounds_every_field() { - let request = - PairingPublishRequest::new([7; 32], [9; 16], 600, vec![1, 2, 3, 4]).expect("valid"); - let raw = request.encode_binary(); - assert!(raw.len() <= MAX_PAIRING_PUBLISH_REQUEST_BYTES); - let decoded = PairingPublishRequest::decode_binary(&raw).expect("round trip"); - assert_eq!(decoded, request); - - // Truncation and trailing bytes both fail the exact length check. - for length in 0..raw.len() { - assert!(PairingPublishRequest::decode_binary(&raw[..length]).is_err()); - } - let mut trailing = raw.clone(); - trailing.push(0); - assert!(PairingPublishRequest::decode_binary(&trailing).is_err()); - - let mut wrong_version = raw; - wrong_version[0] ^= 0xFF; - assert!(matches!( - PairingPublishRequest::decode_binary(&wrong_version), - Err(RelayLocatorIssueError::UnsupportedRequestVersion { .. }) - )); - - // TTL and sealed-blob bounds are enforced at construction. - assert!(PairingPublishRequest::new([7; 32], [9; 16], 0, vec![1]).is_err()); - assert!(PairingPublishRequest::new( - [7; 32], - [9; 16], - MAX_PAIRING_CODE_TTL_SECS + 1, - vec![1] - ) - .is_err()); - assert!(PairingPublishRequest::new([7; 32], [9; 16], 600, Vec::new()).is_err()); - } - - #[test] - fn pairing_claim_round_trips_and_is_exact() { - let request = PairingClaimRequest::new([3; 32], [5; 16]); - let raw = request.encode_binary(); - assert_eq!(raw.len(), PAIRING_CLAIM_REQUEST_BYTES); - let decoded = PairingClaimRequest::decode_binary(&raw).expect("round trip"); - assert_eq!(decoded, request); - for length in 0..raw.len() { - assert!(PairingClaimRequest::decode_binary(&raw[..length]).is_err()); - } - let mut trailing = raw.to_vec(); - trailing.push(0); - assert!(PairingClaimRequest::decode_binary(&trailing).is_err()); - assert_eq!(format!("{request:?}"), "PairingClaimRequest([REDACTED])"); - } -} - -mod pairing_service_tests { - use std::sync::{Arc, Mutex}; - use std::time::Instant; - - use op_auth_bridge::{ - CollabJwksCacheLimits, CollabTicketVerifier, OpaqueCollabTicket, StaticTestJwksFetcher, - TestCollabIssuer, TestCollabTicketSpec, - }; - - use crate::{ - PairingClaimRequest, PairingCodeStore, PairingPublishRequest, PairingPutOutcome, - PairingStoreRejection, RelayPairingService, RelayPairingServiceError, - MAX_PAIRING_CODE_TTL_SECS, - }; - - type PutLog = Arc>>; - - struct RecordingStore(PutLog); - - impl PairingCodeStore for RecordingStore { - fn put( - &self, - owner: [u8; 32], - code_id: [u8; 16], - _sealed: Vec, - now_unix: u64, - expires_at_unix: u64, - ) -> Result { - self.0 - .lock() - .expect("puts lock") - .push(((owner, code_id), now_unix, expires_at_unix)); - Ok(PairingPutOutcome::Stored) - } - - fn claim(&self, _code_id: &[u8; 16], _now_unix: u64) -> Option> { - Some(vec![0xAB]) - } - } - - fn service_fixture( - device: [u8; 32], - ) -> ( - RelayPairingService, - OpaqueCollabTicket, - u64, - PutLog, - ) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("clock") - .as_secs(); - let issuer = TestCollabIssuer::initial(); - let ticket = issuer - .issue(&TestCollabTicketSpec::valid_at(now, device)) - .expect("ticket"); - let verifier = CollabTicketVerifier::new( - TestCollabIssuer::verifier_config().expect("verifier config"), - StaticTestJwksFetcher::new(issuer.jwks_json().expect("JWKS"), 300), - CollabJwksCacheLimits::default(), - ) - .expect("ticket verifier"); - let log: PutLog = Arc::default(); - ( - RelayPairingService::new(verifier, RecordingStore(Arc::clone(&log))), - ticket, - now, - log, - ) - } - - #[test] - fn pairing_endpoints_reject_a_ticket_bound_to_a_different_device() { - let (service, ticket, now, log) = service_fixture([0x11; 32]); - // Body claims device 0x22 while the ticket is bound to 0x11. - let request = - PairingPublishRequest::new([0x22; 32], [9; 16], 600, vec![1, 2, 3]).expect("request"); - assert_eq!( - service.publish_at( - &request.encode_binary(), - ticket.expose(), - now, - Instant::now() - ), - Err(RelayPairingServiceError::AuthenticationFailed) - ); - assert!(log.lock().expect("puts lock").is_empty()); - - let claim = PairingClaimRequest::new([0x22; 32], [9; 16]); - assert_eq!( - service.claim_at(&claim.encode_binary(), ticket.expose(), now, Instant::now()), - Err(RelayPairingServiceError::AuthenticationFailed) - ); - } - - #[test] - fn publish_clamps_ttl_and_records_the_verified_device() { - let device = [0x11; 32]; - let (service, ticket, now, log) = service_fixture(device); - let request = - PairingPublishRequest::new(device, [7; 16], MAX_PAIRING_CODE_TTL_SECS, vec![1, 2, 3]) - .expect("request"); - service - .publish_at( - &request.encode_binary(), - ticket.expose(), - now, - Instant::now(), - ) - .expect("publish"); - let puts = log.lock().expect("puts lock").clone(); - assert_eq!(puts.len(), 1); - let ((owner, code_id), seen_now, expires) = puts[0]; - assert_eq!(owner, device); - assert_eq!(code_id, [7; 16]); - assert_eq!(seen_now, now); - assert_eq!(expires, now + u64::from(MAX_PAIRING_CODE_TTL_SECS)); - } -} +#[path = "pairing_wire_tests.rs"] +mod pairing_wire_tests;