1use std::{
10 collections::BTreeSet,
11 future::Future,
12 io,
13 net::SocketAddr,
14 pin::Pin,
15 sync::{
16 atomic::{AtomicBool, AtomicU64, Ordering},
17 Arc,
18 },
19 task::{Context, Poll},
20 time::Duration,
21};
22
23use thiserror::Error;
24use tokio::{
25 io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf},
26 net::{lookup_host, TcpStream},
27};
28use tokio_rustls::{
29 rustls::{
30 self,
31 pki_types::{CertificateDer, ServerName},
32 ClientConfig, RootCertStore,
33 },
34 TlsConnector,
35};
36use tokio_util::sync::CancellationToken;
37use url::{Host, Url};
38
39use crate::{
40 runtime_budget::{RequestAccountingBroker, RequestAccountingLease},
41 DecisionExecutionStage, RuntimeBudgetDimension, TransportDispatchOutcome,
42 HARD_MAX_TRANSPORT_DISPATCH_RECEIPTS,
43};
44
45pub const TLS_NEGOTIATION_REVIEW_CAPABILITY_ID: &str = "web.tls-negotiation-review";
47pub const TLS_NEGOTIATION_REVIEW_AUDIT_SCHEMA: &str = "security.tls-negotiation-review-audit/v1";
49pub const TLS_NEGOTIATION_REVIEW_POLICY_ID: &str = "termivar.active-tls-negotiation-review/v1";
51pub const TLS_NEGOTIATION_REVIEW_ACTION_TLS13: &str = "web.review.tls-negotiation.tls13@1";
53pub const TLS_NEGOTIATION_REVIEW_ACTION_TLS12: &str = "web.review.tls-negotiation.tls12@1";
55
56pub(crate) const TLS_NEGOTIATION_ACTIVE_VERIFICATION_ALLOWANCE: u16 = 2;
57const MAX_CELL_INGRESS_TLS_BYTES: u64 = 128 * 1024;
58const MAX_TOTAL_INGRESS_TLS_BYTES: u64 = 256 * 1024;
59const MAX_CELL_EGRESS_TLS_BYTES: u64 = 64 * 1024;
60const MAX_TOTAL_EGRESS_TLS_BYTES: u64 = 128 * 1024;
61const MAX_RESOLVED_ADDRESSES: usize = 16;
62const CELL_TIMEOUT: Duration = Duration::from_secs(5);
63const TOTAL_TIMEOUT: Duration = Duration::from_secs(10);
64const MAX_SINGLE_RAW_READ: usize = 16 * 1024;
65
66const NETWORK_CELL_COUNT: usize = 2;
67const MATRIX_CELL_COUNT: usize = 5;
68
69pub fn tls_negotiation_review_target_is_supported(target: &Url) -> bool {
74 matches!(target.host(), Some(Host::Domain(host))
75 if !host.is_empty() && ServerName::try_from(host.to_owned()).is_ok())
76 && target.scheme() == "https"
77 && target.username().is_empty()
78 && target.password().is_none()
79 && target.port_or_known_default().is_some()
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum CellId {
84 Tls13DefaultProvider,
85 Tls12DefaultProvider,
86 Tls11LegacyProtocol,
87 Tls10LegacyProtocol,
88 LegacyCipherSuites,
89}
90
91impl CellId {
92 const fn token(self) -> &'static str {
93 match self {
94 Self::Tls13DefaultProvider => "tls13_default_provider",
95 Self::Tls12DefaultProvider => "tls12_default_provider",
96 Self::Tls11LegacyProtocol => "tls11_legacy_protocol",
97 Self::Tls10LegacyProtocol => "tls10_legacy_protocol",
98 Self::LegacyCipherSuites => "legacy_cipher_suites",
99 }
100 }
101
102 const fn offered_protocol(self) -> &'static str {
103 match self {
104 Self::Tls13DefaultProvider => "tls1.3",
105 Self::Tls12DefaultProvider => "tls1.2",
106 Self::Tls11LegacyProtocol => "tls1.1",
107 Self::Tls10LegacyProtocol => "tls1.0",
108 Self::LegacyCipherSuites => "legacy_cipher_suites",
109 }
110 }
111
112 const fn action_id(self) -> Option<&'static str> {
113 match self {
114 Self::Tls13DefaultProvider => Some(TLS_NEGOTIATION_REVIEW_ACTION_TLS13),
115 Self::Tls12DefaultProvider => Some(TLS_NEGOTIATION_REVIEW_ACTION_TLS12),
116 Self::Tls11LegacyProtocol | Self::Tls10LegacyProtocol | Self::LegacyCipherSuites => {
117 None
118 },
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124enum DispatchStatus {
125 Dispatched,
126 NotDispatched,
127}
128
129impl DispatchStatus {
130 const fn token(self) -> &'static str {
131 match self {
132 Self::Dispatched => "dispatched",
133 Self::NotDispatched => "not_dispatched",
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139enum CellOutcome {
140 Negotiated,
141 PeerAlertReceived,
142 CertificateValidationFailed,
143 HandshakeFailed,
144 TransportFailed,
145 ClientBackendUnsupported,
146 ParentAuthorityUnavailable,
147 BudgetExhausted,
148 Cancelled,
149 DeadlineReached,
150 NotReached,
151}
152
153impl CellOutcome {
154 const fn token(self) -> &'static str {
155 match self {
156 Self::Negotiated => "negotiated",
157 Self::PeerAlertReceived => "peer_alert_received",
158 Self::CertificateValidationFailed => "certificate_validation_failed",
159 Self::HandshakeFailed => "handshake_failed",
160 Self::TransportFailed => "transport_failed",
161 Self::ClientBackendUnsupported => "client_backend_unsupported",
162 Self::ParentAuthorityUnavailable => "parent_authority_unavailable",
163 Self::BudgetExhausted => "budget_exhausted",
164 Self::Cancelled => "cancelled",
165 Self::DeadlineReached => "deadline_reached",
166 Self::NotReached => "not_reached",
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum SupportState {
173 ObservedSupported,
174 NotEstablished,
175 NotTestedClientBackendUnsupported,
176}
177
178impl SupportState {
179 const fn token(self) -> &'static str {
180 match self {
181 Self::ObservedSupported => "observed_supported",
182 Self::NotEstablished => "not_established",
183 Self::NotTestedClientBackendUnsupported => "not_tested_client_backend_unsupported",
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189enum TransportValidation {
190 Succeeded,
191 NotEstablished,
192 NotApplicable,
193}
194
195impl TransportValidation {
196 const fn token(self) -> &'static str {
197 match self {
198 Self::Succeeded => "succeeded",
199 Self::NotEstablished => "not_established",
200 Self::NotApplicable => "not_applicable",
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum ReviewTerminal {
207 Completed,
208 ParentAuthorityUnavailable,
209 BudgetExhausted,
210 Cancelled,
211 DeadlineReached,
212}
213
214impl ReviewTerminal {
215 const fn token(self) -> &'static str {
216 match self {
217 Self::Completed => "completed",
218 Self::ParentAuthorityUnavailable => "parent_authority_unavailable",
219 Self::BudgetExhausted => "budget_exhausted",
220 Self::Cancelled => "cancelled",
221 Self::DeadlineReached => "deadline_reached",
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct WebAssessmentTlsNegotiationReviewCell {
229 id: CellId,
230 dispatch_status: DispatchStatus,
231 outcome: CellOutcome,
232 support: SupportState,
233 transport_validation: TransportValidation,
234 negotiated_protocol: Option<&'static str>,
235 negotiated_cipher_suite: Option<&'static str>,
236 accounted_ingress_tls_bytes: u64,
237 observed_egress_tls_bytes: u64,
238}
239
240impl WebAssessmentTlsNegotiationReviewCell {
241 pub const fn id(&self) -> &'static str {
243 self.id.token()
244 }
245
246 pub const fn offered_protocol(&self) -> &'static str {
248 self.id.offered_protocol()
249 }
250
251 pub const fn client_backend(&self) -> &'static str {
253 "rustls-ring"
254 }
255
256 pub const fn dispatch_status(&self) -> &'static str {
258 self.dispatch_status.token()
259 }
260
261 pub const fn outcome(&self) -> &'static str {
263 self.outcome.token()
264 }
265
266 pub const fn support(&self) -> &'static str {
268 self.support.token()
269 }
270
271 pub const fn transport_validation(&self) -> &'static str {
273 self.transport_validation.token()
274 }
275
276 pub const fn negotiated_protocol(&self) -> Option<&'static str> {
278 self.negotiated_protocol
279 }
280
281 pub const fn negotiated_cipher_suite(&self) -> Option<&'static str> {
283 self.negotiated_cipher_suite
284 }
285
286 pub const fn accounted_ingress_tls_bytes(&self) -> u64 {
288 self.accounted_ingress_tls_bytes
289 }
290
291 pub const fn observed_egress_tls_bytes(&self) -> u64 {
293 self.observed_egress_tls_bytes
294 }
295
296 fn pending(id: CellId) -> Self {
297 Self {
298 id,
299 dispatch_status: DispatchStatus::NotDispatched,
300 outcome: CellOutcome::NotReached,
301 support: SupportState::NotEstablished,
302 transport_validation: TransportValidation::NotEstablished,
303 negotiated_protocol: None,
304 negotiated_cipher_suite: None,
305 accounted_ingress_tls_bytes: 0,
306 observed_egress_tls_bytes: 0,
307 }
308 }
309
310 fn backend_unsupported(id: CellId) -> Self {
311 Self {
312 id,
313 dispatch_status: DispatchStatus::NotDispatched,
314 outcome: CellOutcome::ClientBackendUnsupported,
315 support: SupportState::NotTestedClientBackendUnsupported,
316 transport_validation: TransportValidation::NotApplicable,
317 negotiated_protocol: None,
318 negotiated_cipher_suite: None,
319 accounted_ingress_tls_bytes: 0,
320 observed_egress_tls_bytes: 0,
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct WebAssessmentTlsNegotiationReviewAudit {
328 terminal: ReviewTerminal,
329 attempted_connection_count: u8,
330 completed_handshake_count: u8,
331 accounted_ingress_tls_bytes: u64,
332 observed_egress_tls_bytes: u64,
333 cells: Vec<WebAssessmentTlsNegotiationReviewCell>,
334}
335
336impl WebAssessmentTlsNegotiationReviewAudit {
337 pub const fn schema(&self) -> &'static str {
339 TLS_NEGOTIATION_REVIEW_AUDIT_SCHEMA
340 }
341
342 pub const fn policy(&self) -> &'static str {
344 TLS_NEGOTIATION_REVIEW_POLICY_ID
345 }
346
347 pub const fn selected(&self) -> bool {
349 true
350 }
351
352 pub const fn terminal(&self) -> &'static str {
354 self.terminal.token()
355 }
356
357 pub const fn attempted_connection_count(&self) -> u8 {
359 self.attempted_connection_count
360 }
361
362 pub(crate) const fn network_matrix_complete(&self) -> bool {
366 matches!(self.terminal, ReviewTerminal::Completed)
367 && self.attempted_connection_count == NETWORK_CELL_COUNT as u8
368 }
369
370 pub const fn completed_handshake_count(&self) -> u8 {
372 self.completed_handshake_count
373 }
374
375 pub const fn accounted_ingress_tls_bytes(&self) -> u64 {
377 self.accounted_ingress_tls_bytes
378 }
379
380 pub const fn observed_egress_tls_bytes(&self) -> u64 {
382 self.observed_egress_tls_bytes
383 }
384
385 pub fn cells(&self) -> &[WebAssessmentTlsNegotiationReviewCell] {
387 &self.cells
388 }
389
390 pub(crate) fn parent_authority_unavailable() -> Self {
391 let mut audit = Self::empty();
392 audit.terminal = ReviewTerminal::ParentAuthorityUnavailable;
393 for cell in audit.cells.iter_mut().take(NETWORK_CELL_COUNT) {
394 cell.outcome = CellOutcome::ParentAuthorityUnavailable;
395 }
396 audit
397 }
398
399 fn empty() -> Self {
400 Self {
401 terminal: ReviewTerminal::Completed,
402 attempted_connection_count: 0,
403 completed_handshake_count: 0,
404 accounted_ingress_tls_bytes: 0,
405 observed_egress_tls_bytes: 0,
406 cells: vec![
407 WebAssessmentTlsNegotiationReviewCell::pending(CellId::Tls13DefaultProvider),
408 WebAssessmentTlsNegotiationReviewCell::pending(CellId::Tls12DefaultProvider),
409 WebAssessmentTlsNegotiationReviewCell::backend_unsupported(
410 CellId::Tls11LegacyProtocol,
411 ),
412 WebAssessmentTlsNegotiationReviewCell::backend_unsupported(
413 CellId::Tls10LegacyProtocol,
414 ),
415 WebAssessmentTlsNegotiationReviewCell::backend_unsupported(
416 CellId::LegacyCipherSuites,
417 ),
418 ],
419 }
420 }
421
422 pub(crate) fn is_valid(&self) -> bool {
423 if self.cells.len() != MATRIX_CELL_COUNT
424 || self.attempted_connection_count > NETWORK_CELL_COUNT as u8
425 || self.completed_handshake_count > self.attempted_connection_count
426 {
427 return false;
428 }
429 let expected = [
430 CellId::Tls13DefaultProvider,
431 CellId::Tls12DefaultProvider,
432 CellId::Tls11LegacyProtocol,
433 CellId::Tls10LegacyProtocol,
434 CellId::LegacyCipherSuites,
435 ];
436 if self
437 .cells
438 .iter()
439 .zip(expected)
440 .any(|(cell, id)| cell.id != id)
441 {
442 return false;
443 }
444 let attempted = self
445 .cells
446 .iter()
447 .filter(|cell| cell.dispatch_status == DispatchStatus::Dispatched)
448 .count();
449 let completed = self
450 .cells
451 .iter()
452 .filter(|cell| cell.outcome == CellOutcome::Negotiated)
453 .count();
454 let ingress = self.cells.iter().try_fold(0_u64, |total, cell| {
455 total.checked_add(cell.accounted_ingress_tls_bytes)
456 });
457 let egress = self.cells.iter().try_fold(0_u64, |total, cell| {
458 total.checked_add(cell.observed_egress_tls_bytes)
459 });
460 if attempted != usize::from(self.attempted_connection_count)
461 || completed != usize::from(self.completed_handshake_count)
462 || ingress != Some(self.accounted_ingress_tls_bytes)
463 || egress != Some(self.observed_egress_tls_bytes)
464 || self.accounted_ingress_tls_bytes > MAX_TOTAL_INGRESS_TLS_BYTES
465 || self.observed_egress_tls_bytes > MAX_TOTAL_EGRESS_TLS_BYTES
466 {
467 return false;
468 }
469 if self.terminal == ReviewTerminal::ParentAuthorityUnavailable
470 && self.cells[..NETWORK_CELL_COUNT]
471 .iter()
472 .any(|cell| cell.outcome != CellOutcome::ParentAuthorityUnavailable)
473 {
474 return false;
475 }
476 self.cells.iter().enumerate().all(|(index, cell)| {
477 let negotiated = cell.outcome == CellOutcome::Negotiated;
478 let fields_are_consistent = negotiated
479 == (cell.negotiated_protocol.is_some()
480 && cell.negotiated_cipher_suite.is_some()
481 && cell.support == SupportState::ObservedSupported
482 && cell.transport_validation == TransportValidation::Succeeded);
483 let unsupported = matches!(
484 cell.id,
485 CellId::Tls11LegacyProtocol
486 | CellId::Tls10LegacyProtocol
487 | CellId::LegacyCipherSuites
488 );
489 let network_row_is_consistent = index >= NETWORK_CELL_COUNT
490 || match cell.dispatch_status {
491 DispatchStatus::Dispatched => matches!(
492 cell.outcome,
493 CellOutcome::Negotiated
494 | CellOutcome::PeerAlertReceived
495 | CellOutcome::CertificateValidationFailed
496 | CellOutcome::HandshakeFailed
497 | CellOutcome::TransportFailed
498 | CellOutcome::BudgetExhausted
499 | CellOutcome::Cancelled
500 | CellOutcome::DeadlineReached
501 ),
502 DispatchStatus::NotDispatched => {
503 cell.accounted_ingress_tls_bytes == 0
504 && cell.observed_egress_tls_bytes == 0
505 && matches!(
506 cell.outcome,
507 CellOutcome::ParentAuthorityUnavailable
508 | CellOutcome::BudgetExhausted
509 | CellOutcome::Cancelled
510 | CellOutcome::DeadlineReached
511 | CellOutcome::NotReached
512 )
513 },
514 };
515 let negotiated_profile_is_consistent =
516 !negotiated || cell.negotiated_protocol == Some(cell.id.offered_protocol());
517 let non_negotiated_fields_are_consistent = unsupported
518 || negotiated
519 || (cell.negotiated_protocol.is_none()
520 && cell.negotiated_cipher_suite.is_none()
521 && cell.support == SupportState::NotEstablished
522 && cell.transport_validation == TransportValidation::NotEstablished);
523 fields_are_consistent
524 && network_row_is_consistent
525 && negotiated_profile_is_consistent
526 && non_negotiated_fields_are_consistent
527 && cell.accounted_ingress_tls_bytes <= MAX_CELL_INGRESS_TLS_BYTES
528 && cell.observed_egress_tls_bytes <= MAX_CELL_EGRESS_TLS_BYTES
529 && (!unsupported
530 || (cell.dispatch_status == DispatchStatus::NotDispatched
531 && cell.outcome == CellOutcome::ClientBackendUnsupported
532 && cell.support == SupportState::NotTestedClientBackendUnsupported
533 && cell.transport_validation == TransportValidation::NotApplicable
534 && cell.accounted_ingress_tls_bytes == 0
535 && cell.observed_egress_tls_bytes == 0))
536 })
537 }
538}
539
540#[derive(Clone)]
542pub(crate) struct TlsNegotiationOwnedTransportProfile {
543 resolved_address: SocketAddr,
544 root_certificate_der: Vec<u8>,
545}
546
547impl TlsNegotiationOwnedTransportProfile {
548 pub(crate) fn new(resolved_address: SocketAddr, root_certificate_der: Vec<u8>) -> Self {
549 Self {
550 resolved_address,
551 root_certificate_der,
552 }
553 }
554}
555
556#[derive(Debug, Error)]
557pub(crate) enum TlsNegotiationReviewMintError {
558 #[error("TLS negotiation endpoint is outside the selected authority")]
559 EndpointOutsideAuthority,
560 #[error("TLS negotiation requires one credential-free DNS HTTPS target")]
561 UnsupportedTarget,
562 #[error("TLS negotiation authority has already been consumed")]
563 AuthorityAlreadyMinted,
564 #[error("TLS negotiation client configuration is unavailable")]
565 ClientConfiguration,
566 #[error("TLS negotiation authority invariant failed")]
567 InternalInvariant,
568}
569
570pub(crate) struct TlsNegotiationReviewRuntime {
572 host: String,
573 port: u16,
574 fixed_address: Option<SocketAddr>,
575 tls13: Arc<ClientConfig>,
576 tls12: Arc<ClientConfig>,
577 accounting: RequestAccountingBroker,
578 cancellation: CancellationToken,
579 parent_deadline: Option<tokio::time::Instant>,
580}
581
582impl TlsNegotiationReviewRuntime {
583 pub(crate) fn new(
584 target: &Url,
585 accounting: RequestAccountingBroker,
586 cancellation: CancellationToken,
587 parent_deadline: Option<tokio::time::Instant>,
588 owned_profile: Option<TlsNegotiationOwnedTransportProfile>,
589 ) -> Result<Self, TlsNegotiationReviewMintError> {
590 let Host::Domain(host) = target
591 .host()
592 .ok_or(TlsNegotiationReviewMintError::UnsupportedTarget)?
593 else {
594 return Err(TlsNegotiationReviewMintError::UnsupportedTarget);
595 };
596 if !tls_negotiation_review_target_is_supported(target) {
597 return Err(TlsNegotiationReviewMintError::UnsupportedTarget);
598 }
599 let port = target
600 .port_or_known_default()
601 .ok_or(TlsNegotiationReviewMintError::UnsupportedTarget)?;
602 ServerName::try_from(host.to_owned())
603 .map_err(|_| TlsNegotiationReviewMintError::UnsupportedTarget)?;
604
605 let (roots, fixed_address) = match owned_profile {
606 Some(profile) => {
607 let mut roots = RootCertStore::empty();
608 roots
609 .add(CertificateDer::from(profile.root_certificate_der))
610 .map_err(|_| TlsNegotiationReviewMintError::ClientConfiguration)?;
611 (roots, Some(profile.resolved_address))
612 },
613 None => (
614 RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
615 None,
616 ),
617 };
618 let tls13 = client_config(&roots, &[&rustls::version::TLS13])?;
619 let tls12 = client_config(&roots, &[&rustls::version::TLS12])?;
620 Ok(Self {
621 host: host.to_owned(),
622 port,
623 fixed_address,
624 tls13,
625 tls12,
626 accounting,
627 cancellation,
628 parent_deadline,
629 })
630 }
631
632 pub(crate) async fn execute(mut self) -> WebAssessmentTlsNegotiationReviewAudit {
633 let mut audit = WebAssessmentTlsNegotiationReviewAudit::empty();
634 let transport = self.accounting.dispatch_audit();
639 let receipt_capacity_available = transport.omitted_receipt_count() == 0
640 && transport
641 .receipts()
642 .len()
643 .checked_add(NETWORK_CELL_COUNT)
644 .is_some_and(|required| required <= HARD_MAX_TRANSPORT_DISPATCH_RECEIPTS);
645 if !receipt_capacity_available {
646 audit.terminal = ReviewTerminal::BudgetExhausted;
647 audit.cells[0].outcome = CellOutcome::BudgetExhausted;
648 debug_assert!(audit.is_valid());
649 return audit;
650 }
651 let started = tokio::time::Instant::now();
652 let total_deadline =
653 earlier_deadline(started.checked_add(TOTAL_TIMEOUT), self.parent_deadline)
654 .unwrap_or(started);
655 let totals = Arc::new(TotalByteCounters::default());
656 let mut resolved_address = self.fixed_address;
657 let mut resolution_failed = false;
658
659 for index in 0..NETWORK_CELL_COUNT {
660 if resolution_failed {
661 break;
662 }
663 let id = if index == 0 {
664 CellId::Tls13DefaultProvider
665 } else {
666 CellId::Tls12DefaultProvider
667 };
668 let config = if index == 0 {
669 Arc::clone(&self.tls13)
670 } else {
671 Arc::clone(&self.tls12)
672 };
673 let stop = self
674 .execute_cell(
675 id,
676 config,
677 total_deadline,
678 &totals,
679 &mut resolved_address,
680 &mut resolution_failed,
681 &mut audit.cells[index],
682 )
683 .await;
684 if let Some(terminal) = stop {
685 audit.terminal = terminal;
686 break;
687 }
688 }
689
690 audit.attempted_connection_count = u8::try_from(
691 audit
692 .cells
693 .iter()
694 .filter(|cell| cell.dispatch_status == DispatchStatus::Dispatched)
695 .count(),
696 )
697 .unwrap_or(u8::MAX);
698 audit.completed_handshake_count = u8::try_from(
699 audit
700 .cells
701 .iter()
702 .filter(|cell| cell.outcome == CellOutcome::Negotiated)
703 .count(),
704 )
705 .unwrap_or(u8::MAX);
706 audit.accounted_ingress_tls_bytes = totals.ingress.load(Ordering::Relaxed);
707 audit.observed_egress_tls_bytes = totals.egress.load(Ordering::Relaxed);
708 debug_assert!(audit.is_valid());
709 audit
710 }
711
712 #[allow(clippy::too_many_arguments)]
713 async fn execute_cell(
714 &mut self,
715 id: CellId,
716 config: Arc<ClientConfig>,
717 total_deadline: tokio::time::Instant,
718 totals: &Arc<TotalByteCounters>,
719 resolved_address: &mut Option<SocketAddr>,
720 resolution_failed: &mut bool,
721 cell: &mut WebAssessmentTlsNegotiationReviewCell,
722 ) -> Option<ReviewTerminal> {
723 if self.cancellation.is_cancelled() {
724 cell.outcome = CellOutcome::Cancelled;
725 return Some(ReviewTerminal::Cancelled);
726 }
727 let now = tokio::time::Instant::now();
728 if now >= total_deadline {
729 cell.outcome = CellOutcome::DeadlineReached;
730 return Some(ReviewTerminal::DeadlineReached);
731 }
732 let action_id = id.action_id().expect("network cell has an action identity");
733 let mut lease = match self.accounting.try_begin_with_request_body_bytes(
734 action_id,
735 DecisionExecutionStage::Active,
736 None,
737 0,
738 ) {
739 Ok(lease) => lease,
740 Err(limit) => {
741 cell.outcome = CellOutcome::BudgetExhausted;
742 return Some(if limit.dimension() == RuntimeBudgetDimension::WallTime {
743 ReviewTerminal::DeadlineReached
744 } else {
745 ReviewTerminal::BudgetExhausted
746 });
747 },
748 };
749 cell.dispatch_status = DispatchStatus::Dispatched;
750 let cell_deadline = earlier_deadline(now.checked_add(CELL_TIMEOUT), Some(total_deadline))
751 .unwrap_or(total_deadline);
752 let response_gate = match bounded(
753 &self.cancellation,
754 cell_deadline,
755 lease.acquire_response_read(),
756 )
757 .await
758 {
759 Ok(guard) => guard,
760 Err(StepStop::Cancelled) => {
761 lease.finish(TransportDispatchOutcome::Cancelled);
762 cell.outcome = CellOutcome::Cancelled;
763 return Some(ReviewTerminal::Cancelled);
764 },
765 Err(StepStop::Deadline) => {
766 lease.finish(TransportDispatchOutcome::RequestTimeout);
767 cell.outcome = CellOutcome::DeadlineReached;
768 return Some(ReviewTerminal::DeadlineReached);
769 },
770 };
771
772 if resolved_address.is_none() {
773 let resolution = bounded(
774 &self.cancellation,
775 cell_deadline,
776 resolve_one_address(&self.host, self.port),
777 )
778 .await;
779 match resolution {
780 Ok(Some(address)) => *resolved_address = Some(address),
781 Ok(None) => {
782 lease.finish(TransportDispatchOutcome::TransportFailure);
783 cell.outcome = CellOutcome::TransportFailed;
784 *resolution_failed = true;
785 drop(response_gate);
786 return None;
787 },
788 Err(StepStop::Deadline) => {
789 lease.finish(TransportDispatchOutcome::RequestTimeout);
790 cell.outcome = CellOutcome::DeadlineReached;
791 *resolution_failed = true;
792 drop(response_gate);
793 return Some(ReviewTerminal::DeadlineReached);
794 },
795 Err(StepStop::Cancelled) => {
796 lease.finish(TransportDispatchOutcome::Cancelled);
797 cell.outcome = CellOutcome::Cancelled;
798 drop(response_gate);
799 return Some(ReviewTerminal::Cancelled);
800 },
801 }
802 }
803 let address = resolved_address.expect("resolution populated before connection");
804 let stream = match bounded(
805 &self.cancellation,
806 cell_deadline,
807 TcpStream::connect(address),
808 )
809 .await
810 {
811 Ok(Ok(stream)) => stream,
812 Ok(Err(_)) => {
813 lease.finish(TransportDispatchOutcome::TransportFailure);
814 cell.outcome = CellOutcome::TransportFailed;
815 drop(response_gate);
816 return None;
817 },
818 Err(StepStop::Cancelled) => {
819 lease.finish(TransportDispatchOutcome::Cancelled);
820 cell.outcome = CellOutcome::Cancelled;
821 drop(response_gate);
822 return Some(ReviewTerminal::Cancelled);
823 },
824 Err(StepStop::Deadline) => {
825 lease.finish(TransportDispatchOutcome::RequestTimeout);
826 cell.outcome = CellOutcome::DeadlineReached;
827 drop(response_gate);
828 return Some(ReviewTerminal::DeadlineReached);
829 },
830 };
831 let counters = CellByteCounters::new(Arc::clone(totals));
832 let counted = CountedTlsIo::new(stream, &mut lease, counters.clone());
833 let server_name = ServerName::try_from(self.host.clone())
834 .expect("host was validated before authority mint");
835 let handshake = bounded(
836 &self.cancellation,
837 cell_deadline,
838 TlsConnector::from(config)
839 .connect(server_name, counted)
840 .into_fallible(),
841 )
842 .await;
843
844 match handshake {
845 Ok(Ok(mut tls)) => {
846 let connection = tls.get_ref().1;
847 let protocol = connection.protocol_version().and_then(protocol_token);
848 let cipher = connection
849 .negotiated_cipher_suite()
850 .and_then(|suite| cipher_suite_token(suite.suite()));
851 if protocol.is_some() && cipher.is_some() && connection.alpn_protocol().is_none() {
852 let shutdown = bounded(
853 &self.cancellation,
854 cell_deadline,
855 AsyncWriteExt::shutdown(&mut tls),
856 )
857 .await;
858 drop(tls);
859 match shutdown {
860 Err(StepStop::Cancelled) => {
861 cell.outcome = CellOutcome::Cancelled;
862 lease.finish(TransportDispatchOutcome::Cancelled);
863 },
864 Err(StepStop::Deadline) => {
865 cell.outcome = CellOutcome::DeadlineReached;
866 lease.finish(TransportDispatchOutcome::RequestTimeout);
867 },
868 Ok(Ok(())) => {
869 if tls_byte_limit_reached(&lease, &counters, totals) {
870 cell.outcome = CellOutcome::BudgetExhausted;
871 lease.finish(TransportDispatchOutcome::ResponseBudgetReached);
872 } else {
873 cell.outcome = CellOutcome::Negotiated;
874 cell.support = SupportState::ObservedSupported;
875 cell.transport_validation = TransportValidation::Succeeded;
876 cell.negotiated_protocol = protocol;
877 cell.negotiated_cipher_suite = cipher;
878 lease.finish(TransportDispatchOutcome::Completed);
879 }
880 },
881 Ok(Err(_)) => {
882 if tls_byte_limit_reached(&lease, &counters, totals) {
883 cell.outcome = CellOutcome::BudgetExhausted;
884 lease.finish(TransportDispatchOutcome::ResponseBudgetReached);
885 } else {
886 cell.outcome = CellOutcome::TransportFailed;
887 lease.finish(TransportDispatchOutcome::TransportFailure);
888 }
889 },
890 }
891 } else {
892 drop(tls);
893 cell.outcome = CellOutcome::HandshakeFailed;
894 lease.finish(TransportDispatchOutcome::TransportFailure);
895 }
896 },
897 Ok(Err((error, counted))) => {
898 drop(counted);
899 if tls_byte_limit_reached(&lease, &counters, totals) {
900 cell.outcome = CellOutcome::BudgetExhausted;
901 lease.finish(TransportDispatchOutcome::ResponseBudgetReached);
902 } else {
903 cell.outcome = classify_handshake_error(&error);
904 lease.finish(TransportDispatchOutcome::TransportFailure);
905 }
906 },
907 Err(StepStop::Cancelled) => {
908 cell.outcome = CellOutcome::Cancelled;
909 lease.finish(TransportDispatchOutcome::Cancelled);
910 },
911 Err(StepStop::Deadline) => {
912 cell.outcome = CellOutcome::DeadlineReached;
913 lease.finish(TransportDispatchOutcome::RequestTimeout);
914 },
915 }
916 cell.accounted_ingress_tls_bytes = counters.ingress.load(Ordering::Relaxed);
917 cell.observed_egress_tls_bytes = counters.egress.load(Ordering::Relaxed);
918 drop(response_gate);
919
920 match cell.outcome {
921 CellOutcome::BudgetExhausted => Some(ReviewTerminal::BudgetExhausted),
922 CellOutcome::Cancelled => Some(ReviewTerminal::Cancelled),
923 CellOutcome::DeadlineReached => Some(ReviewTerminal::DeadlineReached),
924 _ => None,
925 }
926 }
927}
928
929fn tls_byte_limit_reached(
930 lease: &RequestAccountingLease,
931 counters: &CellByteCounters,
932 totals: &TotalByteCounters,
933) -> bool {
934 lease.response_budget_reached()
935 || counters
936 .parent_ingress_limit_reached
937 .load(Ordering::Relaxed)
938 || counters.local_ingress_limit_reached.load(Ordering::Relaxed)
939 || counters.local_egress_limit_reached.load(Ordering::Relaxed)
940 || counters.ingress.load(Ordering::Relaxed) >= MAX_CELL_INGRESS_TLS_BYTES
941 || counters.egress.load(Ordering::Relaxed) >= MAX_CELL_EGRESS_TLS_BYTES
942 || totals.ingress.load(Ordering::Relaxed) >= MAX_TOTAL_INGRESS_TLS_BYTES
943 || totals.egress.load(Ordering::Relaxed) >= MAX_TOTAL_EGRESS_TLS_BYTES
944}
945
946fn client_config(
947 roots: &RootCertStore,
948 versions: &[&'static rustls::SupportedProtocolVersion],
949) -> Result<Arc<ClientConfig>, TlsNegotiationReviewMintError> {
950 let provider = Arc::new(rustls::crypto::ring::default_provider());
951 let mut config = ClientConfig::builder_with_provider(provider)
952 .with_protocol_versions(versions)
953 .map_err(|_| TlsNegotiationReviewMintError::ClientConfiguration)?
954 .with_root_certificates(roots.clone())
955 .with_no_client_auth();
956 config.resumption = rustls::client::Resumption::disabled();
957 config.alpn_protocols.clear();
958 config.enable_early_data = false;
959 Ok(Arc::new(config))
960}
961
962async fn resolve_one_address(host: &str, port: u16) -> Option<SocketAddr> {
963 let addresses = lookup_host((host, port)).await.ok()?;
964 select_resolved_address(addresses)
965}
966
967fn select_resolved_address(addresses: impl Iterator<Item = SocketAddr>) -> Option<SocketAddr> {
968 addresses
969 .take(MAX_RESOLVED_ADDRESSES)
970 .collect::<BTreeSet<_>>()
971 .into_iter()
972 .next()
973}
974
975fn classify_handshake_error(error: &io::Error) -> CellOutcome {
976 match error
977 .get_ref()
978 .and_then(|source| source.downcast_ref::<rustls::Error>())
979 {
980 Some(rustls::Error::AlertReceived(_)) => CellOutcome::PeerAlertReceived,
981 Some(rustls::Error::InvalidCertificate(_)) => CellOutcome::CertificateValidationFailed,
982 Some(_) => CellOutcome::HandshakeFailed,
983 None => CellOutcome::TransportFailed,
984 }
985}
986
987fn protocol_token(version: rustls::ProtocolVersion) -> Option<&'static str> {
988 match version {
989 rustls::ProtocolVersion::TLSv1_3 => Some("tls1.3"),
990 rustls::ProtocolVersion::TLSv1_2 => Some("tls1.2"),
991 _ => None,
992 }
993}
994
995fn cipher_suite_token(suite: rustls::CipherSuite) -> Option<&'static str> {
996 Some(match suite {
997 rustls::CipherSuite::TLS13_AES_256_GCM_SHA384 => "tls13_aes_256_gcm_sha384",
998 rustls::CipherSuite::TLS13_AES_128_GCM_SHA256 => "tls13_aes_128_gcm_sha256",
999 rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 => "tls13_chacha20_poly1305_sha256",
1000 rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => {
1001 "tls12_ecdhe_ecdsa_aes_256_gcm_sha384"
1002 },
1003 rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => {
1004 "tls12_ecdhe_ecdsa_aes_128_gcm_sha256"
1005 },
1006 rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => {
1007 "tls12_ecdhe_ecdsa_chacha20_poly1305_sha256"
1008 },
1009 rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => {
1010 "tls12_ecdhe_rsa_aes_256_gcm_sha384"
1011 },
1012 rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => {
1013 "tls12_ecdhe_rsa_aes_128_gcm_sha256"
1014 },
1015 rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => {
1016 "tls12_ecdhe_rsa_chacha20_poly1305_sha256"
1017 },
1018 _ => return None,
1019 })
1020}
1021
1022fn earlier_deadline(
1023 first: Option<tokio::time::Instant>,
1024 second: Option<tokio::time::Instant>,
1025) -> Option<tokio::time::Instant> {
1026 match (first, second) {
1027 (Some(first), Some(second)) => Some(first.min(second)),
1028 (Some(value), None) | (None, Some(value)) => Some(value),
1029 (None, None) => None,
1030 }
1031}
1032
1033#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1034enum StepStop {
1035 Cancelled,
1036 Deadline,
1037}
1038
1039async fn bounded<T, F>(
1040 cancellation: &CancellationToken,
1041 deadline: tokio::time::Instant,
1042 future: F,
1043) -> Result<T, StepStop>
1044where
1045 F: Future<Output = T>,
1046{
1047 tokio::select! {
1048 biased;
1049 () = cancellation.cancelled() => Err(StepStop::Cancelled),
1050 result = tokio::time::timeout_at(deadline, future) => {
1051 result.map_err(|_| StepStop::Deadline)
1052 },
1053 }
1054}
1055
1056#[derive(Default)]
1057struct TotalByteCounters {
1058 ingress: AtomicU64,
1059 egress: AtomicU64,
1060}
1061
1062#[derive(Clone)]
1063struct CellByteCounters {
1064 ingress: Arc<AtomicU64>,
1065 egress: Arc<AtomicU64>,
1066 totals: Arc<TotalByteCounters>,
1067 local_ingress_limit_reached: Arc<AtomicBool>,
1068 local_egress_limit_reached: Arc<AtomicBool>,
1069 parent_ingress_limit_reached: Arc<AtomicBool>,
1070}
1071
1072impl CellByteCounters {
1073 fn new(totals: Arc<TotalByteCounters>) -> Self {
1074 Self {
1075 ingress: Arc::new(AtomicU64::new(0)),
1076 egress: Arc::new(AtomicU64::new(0)),
1077 totals,
1078 local_ingress_limit_reached: Arc::new(AtomicBool::new(false)),
1079 local_egress_limit_reached: Arc::new(AtomicBool::new(false)),
1080 parent_ingress_limit_reached: Arc::new(AtomicBool::new(false)),
1081 }
1082 }
1083}
1084
1085struct CountedTlsIo<'a> {
1086 inner: TcpStream,
1087 lease: &'a mut RequestAccountingLease,
1088 counters: CellByteCounters,
1089}
1090
1091impl<'a> CountedTlsIo<'a> {
1092 fn new(
1093 inner: TcpStream,
1094 lease: &'a mut RequestAccountingLease,
1095 counters: CellByteCounters,
1096 ) -> Self {
1097 Self {
1098 inner,
1099 lease,
1100 counters,
1101 }
1102 }
1103}
1104
1105impl AsyncRead for CountedTlsIo<'_> {
1106 fn poll_read(
1107 self: Pin<&mut Self>,
1108 context: &mut Context<'_>,
1109 buffer: &mut ReadBuf<'_>,
1110 ) -> Poll<io::Result<()>> {
1111 let this = self.get_mut();
1112 if buffer.remaining() == 0 {
1113 return Poll::Ready(Ok(()));
1114 }
1115 let cell_remaining = MAX_CELL_INGRESS_TLS_BYTES
1116 .saturating_sub(this.counters.ingress.load(Ordering::Relaxed));
1117 let total_remaining = MAX_TOTAL_INGRESS_TLS_BYTES
1118 .saturating_sub(this.counters.totals.ingress.load(Ordering::Relaxed));
1119 let parent_remaining = this.lease.remaining_response_bytes();
1120 let allowed = u64::try_from(buffer.remaining())
1121 .unwrap_or(u64::MAX)
1122 .min(cell_remaining)
1123 .min(total_remaining)
1124 .min(parent_remaining)
1125 .min(MAX_SINGLE_RAW_READ as u64);
1126 if allowed == 0 {
1127 if parent_remaining == 0 {
1128 this.counters
1129 .parent_ingress_limit_reached
1130 .store(true, Ordering::Relaxed);
1131 } else {
1132 this.counters
1133 .local_ingress_limit_reached
1134 .store(true, Ordering::Relaxed);
1135 }
1136 return Poll::Ready(Err(io::Error::other("TLS ingress limit reached")));
1137 }
1138 let mut temporary = vec![0_u8; usize::try_from(allowed).unwrap_or(MAX_SINGLE_RAW_READ)];
1139 let mut temporary_buffer = ReadBuf::new(&mut temporary);
1140 match Pin::new(&mut this.inner).poll_read(context, &mut temporary_buffer) {
1141 Poll::Ready(Ok(())) => {
1142 let read = temporary_buffer.filled().len();
1143 if read > 0 {
1144 buffer.put_slice(&temporary_buffer.filled()[..read]);
1145 let read_u64 = u64::try_from(read).unwrap_or(u64::MAX);
1146 this.counters.ingress.fetch_add(read_u64, Ordering::Relaxed);
1147 this.counters
1148 .totals
1149 .ingress
1150 .fetch_add(read_u64, Ordering::Relaxed);
1151 let _ = this.lease.observe_response_bytes(read_u64);
1152 }
1153 Poll::Ready(Ok(()))
1154 },
1155 other => other,
1156 }
1157 }
1158}
1159
1160impl AsyncWrite for CountedTlsIo<'_> {
1161 fn poll_write(
1162 self: Pin<&mut Self>,
1163 context: &mut Context<'_>,
1164 buffer: &[u8],
1165 ) -> Poll<Result<usize, io::Error>> {
1166 let this = self.get_mut();
1167 let cell_remaining =
1168 MAX_CELL_EGRESS_TLS_BYTES.saturating_sub(this.counters.egress.load(Ordering::Relaxed));
1169 let total_remaining = MAX_TOTAL_EGRESS_TLS_BYTES
1170 .saturating_sub(this.counters.totals.egress.load(Ordering::Relaxed));
1171 let allowed = usize::try_from(cell_remaining.min(total_remaining))
1172 .unwrap_or(usize::MAX)
1173 .min(buffer.len());
1174 if !buffer.is_empty() && allowed == 0 {
1175 this.counters
1176 .local_egress_limit_reached
1177 .store(true, Ordering::Relaxed);
1178 return Poll::Ready(Err(io::Error::other("TLS egress limit reached")));
1179 }
1180 match Pin::new(&mut this.inner).poll_write(context, &buffer[..allowed]) {
1181 Poll::Ready(Ok(written)) => {
1182 let written = u64::try_from(written).unwrap_or(u64::MAX);
1183 this.counters.egress.fetch_add(written, Ordering::Relaxed);
1184 this.counters
1185 .totals
1186 .egress
1187 .fetch_add(written, Ordering::Relaxed);
1188 Poll::Ready(Ok(usize::try_from(written).unwrap_or(usize::MAX)))
1189 },
1190 other => other,
1191 }
1192 }
1193
1194 fn poll_flush(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
1195 Pin::new(&mut self.get_mut().inner).poll_flush(context)
1196 }
1197
1198 fn poll_shutdown(
1199 self: Pin<&mut Self>,
1200 context: &mut Context<'_>,
1201 ) -> Poll<Result<(), io::Error>> {
1202 Pin::new(&mut self.get_mut().inner).poll_shutdown(context)
1203 }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::*;
1209
1210 struct OwnedTlsMaterial {
1211 root_der: Vec<u8>,
1212 leaf_der: Vec<u8>,
1213 key_der: Vec<u8>,
1214 }
1215
1216 struct OwnedTlsAccept {
1217 protocol: rustls::ProtocolVersion,
1218 alpn_selected: bool,
1219 application_bytes: usize,
1220 }
1221
1222 fn owned_tls_material(dns_name: &str) -> OwnedTlsMaterial {
1223 use rcgen::{
1224 date_time_ymd, BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose,
1225 IsCa, KeyPair, KeyUsagePurpose,
1226 };
1227
1228 let mut root_params = CertificateParams::new(Vec::<String>::new()).unwrap();
1229 root_params
1230 .distinguished_name
1231 .push(DnType::CommonName, "Termivar active TLS test root");
1232 root_params.not_before = date_time_ymd(2020, 1, 1);
1233 root_params.not_after = date_time_ymd(2100, 1, 1);
1234 root_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
1235 root_params.key_usages = vec![
1236 KeyUsagePurpose::KeyCertSign,
1237 KeyUsagePurpose::DigitalSignature,
1238 ];
1239 let root_key = KeyPair::generate().unwrap();
1240 let root = root_params.self_signed(&root_key).unwrap();
1241
1242 let mut leaf_params = CertificateParams::new(vec![dns_name.to_owned()]).unwrap();
1243 leaf_params
1244 .distinguished_name
1245 .push(DnType::CommonName, dns_name);
1246 leaf_params.not_before = date_time_ymd(2020, 1, 1);
1247 leaf_params.not_after = date_time_ymd(2100, 1, 1);
1248 leaf_params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
1249 leaf_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
1250 let leaf_key = KeyPair::generate().unwrap();
1251 let leaf = leaf_params.signed_by(&leaf_key, &root, &root_key).unwrap();
1252
1253 OwnedTlsMaterial {
1254 root_der: root.der().to_vec(),
1255 leaf_der: leaf.der().to_vec(),
1256 key_der: leaf_key.serialize_der(),
1257 }
1258 }
1259
1260 async fn owned_tls_server(
1261 material: &OwnedTlsMaterial,
1262 connection_count: usize,
1263 ) -> (SocketAddr, tokio::task::JoinHandle<Vec<OwnedTlsAccept>>) {
1264 use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
1265 use tokio::io::AsyncReadExt;
1266 use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
1267
1268 let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
1269 .await
1270 .unwrap();
1271 let address = listener.local_addr().unwrap();
1272 let certificates = vec![CertificateDer::from(material.leaf_der.clone())];
1273 let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(material.key_der.clone()));
1274 let server = ServerConfig::builder()
1275 .with_no_client_auth()
1276 .with_single_cert(certificates, key)
1277 .unwrap();
1278 let acceptor = TlsAcceptor::from(Arc::new(server));
1279 let task = tokio::spawn(async move {
1280 let mut accepted = Vec::with_capacity(connection_count);
1281 for _ in 0..connection_count {
1282 let (stream, peer) = listener.accept().await.unwrap();
1283 assert!(peer.ip().is_loopback());
1284 let mut stream = acceptor.accept(stream).await.unwrap();
1285 let connection = stream.get_ref().1;
1286 let protocol = connection.protocol_version().unwrap();
1287 let alpn_selected = connection.alpn_protocol().is_some();
1288 let mut application = [0_u8; 1];
1289 let application_bytes = stream.read(&mut application).await.unwrap();
1290 accepted.push(OwnedTlsAccept {
1291 protocol,
1292 alpn_selected,
1293 application_bytes,
1294 });
1295 }
1296 accepted
1297 });
1298 (address, task)
1299 }
1300
1301 #[test]
1302 fn selected_empty_matrix_has_fixed_order_and_zero_network_legacy_rows() {
1303 let audit = WebAssessmentTlsNegotiationReviewAudit::empty();
1304 assert!(audit.is_valid());
1305 assert!(!audit.network_matrix_complete());
1306 assert_eq!(
1307 audit
1308 .cells()
1309 .iter()
1310 .map(|cell| cell.id())
1311 .collect::<Vec<_>>(),
1312 vec![
1313 "tls13_default_provider",
1314 "tls12_default_provider",
1315 "tls11_legacy_protocol",
1316 "tls10_legacy_protocol",
1317 "legacy_cipher_suites",
1318 ]
1319 );
1320 for cell in &audit.cells()[2..] {
1321 assert_eq!(cell.dispatch_status(), "not_dispatched");
1322 assert_eq!(cell.outcome(), "client_backend_unsupported");
1323 assert_eq!(cell.support(), "not_tested_client_backend_unsupported");
1324 assert_eq!(cell.transport_validation(), "not_applicable");
1325 assert_eq!(cell.accounted_ingress_tls_bytes(), 0);
1326 assert_eq!(cell.observed_egress_tls_bytes(), 0);
1327 }
1328 }
1329
1330 #[test]
1331 fn parent_unavailable_is_selected_but_never_dispatched() {
1332 let audit = WebAssessmentTlsNegotiationReviewAudit::parent_authority_unavailable();
1333 assert!(audit.is_valid());
1334 assert!(!audit.network_matrix_complete());
1335 assert_eq!(audit.terminal(), "parent_authority_unavailable");
1336 assert_eq!(audit.attempted_connection_count(), 0);
1337 assert_eq!(audit.cells()[0].outcome(), "parent_authority_unavailable");
1338 assert_eq!(audit.cells()[1].outcome(), "parent_authority_unavailable");
1339 }
1340
1341 #[test]
1342 fn cipher_and_protocol_tokens_are_closed() {
1343 assert_eq!(
1344 protocol_token(rustls::ProtocolVersion::TLSv1_3),
1345 Some("tls1.3")
1346 );
1347 assert_eq!(
1348 cipher_suite_token(rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
1349 Some("tls13_aes_256_gcm_sha384")
1350 );
1351 assert_eq!(
1352 cipher_suite_token(rustls::CipherSuite::TLS_NULL_WITH_NULL_NULL),
1353 None
1354 );
1355 }
1356
1357 #[test]
1358 fn target_preflight_matches_dns_sni_authority_without_network_access() {
1359 for target in [
1360 "https://example.test/",
1361 "https://xml-target.termivar.test:8443/review",
1362 ] {
1363 assert!(tls_negotiation_review_target_is_supported(
1364 &Url::parse(target).unwrap()
1365 ));
1366 }
1367 for target in [
1368 "http://example.test/",
1369 "https://127.0.0.1/",
1370 "https://user@example.test/",
1371 "https://-bad.example/",
1372 "https://bad-.example/",
1373 ] {
1374 assert!(!tls_negotiation_review_target_is_supported(
1375 &Url::parse(target).unwrap()
1376 ));
1377 }
1378 }
1379
1380 #[test]
1381 fn address_selection_consumes_at_most_sixteen_answers_in_stable_order() {
1382 use std::cell::Cell;
1383
1384 let consumed = Cell::new(0_usize);
1385 let answers = (1_u8..=17).rev().map(|last_octet| {
1386 consumed.set(consumed.get() + 1);
1387 SocketAddr::from(([192, 0, 2, last_octet], 443))
1388 });
1389 assert_eq!(
1390 select_resolved_address(answers),
1391 Some(SocketAddr::from(([192, 0, 2, 2], 443)))
1392 );
1393 assert_eq!(consumed.get(), MAX_RESOLVED_ADDRESSES);
1394
1395 let duplicate = SocketAddr::from(([192, 0, 2, 20], 443));
1396 let distinct_after_limit = SocketAddr::from(([192, 0, 2, 1], 443));
1397 assert_eq!(
1398 select_resolved_address(
1399 std::iter::repeat_n(duplicate, MAX_RESOLVED_ADDRESSES)
1400 .chain(std::iter::once(distinct_after_limit))
1401 ),
1402 Some(duplicate)
1403 );
1404 }
1405
1406 #[test]
1407 fn tls_byte_limit_classification_includes_close_notify_egress_failure() {
1408 let accounting = RequestAccountingBroker::new(crate::RuntimeBudget::default());
1409 let mut lease = accounting
1410 .try_begin(
1411 TLS_NEGOTIATION_REVIEW_ACTION_TLS13,
1412 DecisionExecutionStage::Active,
1413 None,
1414 )
1415 .unwrap();
1416 let totals = Arc::new(TotalByteCounters::default());
1417 let counters = CellByteCounters::new(Arc::clone(&totals));
1418
1419 assert!(!tls_byte_limit_reached(&lease, &counters, &totals));
1420 counters
1421 .local_egress_limit_reached
1422 .store(true, Ordering::Relaxed);
1423 assert!(tls_byte_limit_reached(&lease, &counters, &totals));
1424 lease.finish(TransportDispatchOutcome::ResponseBudgetReached);
1425 }
1426
1427 #[test]
1428 fn complete_matrix_requires_both_parent_accounted_network_dispatches() {
1429 let mut audit = WebAssessmentTlsNegotiationReviewAudit::empty();
1430 audit.attempted_connection_count = 2;
1431 for cell in audit.cells.iter_mut().take(NETWORK_CELL_COUNT) {
1432 cell.dispatch_status = DispatchStatus::Dispatched;
1433 cell.outcome = CellOutcome::TransportFailed;
1434 }
1435
1436 assert!(audit.is_valid());
1437 assert!(audit.network_matrix_complete());
1438
1439 audit.attempted_connection_count = 1;
1440 audit.cells[1] =
1441 WebAssessmentTlsNegotiationReviewCell::pending(CellId::Tls12DefaultProvider);
1442 assert!(audit.is_valid());
1443 assert!(!audit.network_matrix_complete());
1444 }
1445
1446 #[tokio::test]
1447 async fn owned_tls_matrix_negotiates_two_versions_without_application_data() {
1448 let dns_name = "active-tls.termivar.test";
1449 let material = owned_tls_material(dns_name);
1450 let (address, server) = owned_tls_server(&material, NETWORK_CELL_COUNT).await;
1451 let accounting = RequestAccountingBroker::new(
1452 crate::RuntimeBudget::default()
1453 .with_max_total_requests(NETWORK_CELL_COUNT as u32)
1454 .with_max_active_verifications(NETWORK_CELL_COUNT as u16),
1455 );
1456 let target = Url::parse(&format!("https://{dns_name}:{}/review", address.port())).unwrap();
1457 let runtime = TlsNegotiationReviewRuntime::new(
1458 &target,
1459 accounting.clone(),
1460 CancellationToken::new(),
1461 None,
1462 Some(TlsNegotiationOwnedTransportProfile::new(
1463 address,
1464 material.root_der,
1465 )),
1466 )
1467 .unwrap();
1468
1469 let audit = runtime.execute().await;
1470 let accepted = tokio::time::timeout(Duration::from_secs(5), server)
1471 .await
1472 .expect("owned TLS server completed")
1473 .unwrap();
1474
1475 assert!(audit.is_valid());
1476 assert!(audit.network_matrix_complete());
1477 assert_eq!(audit.terminal(), "completed");
1478 assert_eq!(audit.attempted_connection_count(), 2);
1479 assert_eq!(audit.completed_handshake_count(), 2);
1480 assert!(audit.accounted_ingress_tls_bytes() > 0);
1481 assert!(audit.observed_egress_tls_bytes() > 0);
1482 assert_eq!(
1483 audit
1484 .cells()
1485 .iter()
1486 .take(NETWORK_CELL_COUNT)
1487 .map(|cell| (cell.outcome(), cell.negotiated_protocol()))
1488 .collect::<Vec<_>>(),
1489 vec![
1490 ("negotiated", Some("tls1.3")),
1491 ("negotiated", Some("tls1.2"))
1492 ]
1493 );
1494 assert_eq!(
1495 accepted
1496 .iter()
1497 .map(|fact| fact.protocol)
1498 .collect::<Vec<_>>(),
1499 vec![
1500 rustls::ProtocolVersion::TLSv1_3,
1501 rustls::ProtocolVersion::TLSv1_2
1502 ]
1503 );
1504 assert!(accepted
1505 .iter()
1506 .all(|fact| !fact.alpn_selected && fact.application_bytes == 0));
1507
1508 let transport = accounting.dispatch_audit();
1509 assert_eq!(transport.receipts().len(), NETWORK_CELL_COUNT);
1510 assert_eq!(transport.omitted_receipt_count(), 0);
1511 assert!(transport.receipts().iter().all(|receipt| {
1512 receipt.stage() == DecisionExecutionStage::Active
1513 && receipt.request_body_bytes() == 0
1514 && receipt.response_bytes() > 0
1515 && receipt.outcome() == TransportDispatchOutcome::Completed
1516 }));
1517 }
1518
1519 #[tokio::test]
1520 async fn cancellation_and_parent_budget_stop_before_any_connection() {
1521 let dns_name = "active-tls.termivar.test";
1522 let material = owned_tls_material(dns_name);
1523 let fixed_address = SocketAddr::from(([127, 0, 0, 1], 9));
1524
1525 let cancelled_accounting = RequestAccountingBroker::new(crate::RuntimeBudget::default());
1526 let cancellation = CancellationToken::new();
1527 cancellation.cancel();
1528 let cancelled = TlsNegotiationReviewRuntime::new(
1529 &Url::parse(&format!("https://{dns_name}/")).unwrap(),
1530 cancelled_accounting.clone(),
1531 cancellation,
1532 None,
1533 Some(TlsNegotiationOwnedTransportProfile::new(
1534 fixed_address,
1535 material.root_der.clone(),
1536 )),
1537 )
1538 .unwrap()
1539 .execute()
1540 .await;
1541 assert!(cancelled.is_valid());
1542 assert_eq!(cancelled.terminal(), "cancelled");
1543 assert_eq!(cancelled.attempted_connection_count(), 0);
1544 assert!(cancelled_accounting.dispatch_audit().receipts().is_empty());
1545
1546 let exhausted_accounting = RequestAccountingBroker::new(
1547 crate::RuntimeBudget::default()
1548 .with_max_total_requests(0)
1549 .with_max_active_verifications(0),
1550 );
1551 let exhausted = TlsNegotiationReviewRuntime::new(
1552 &Url::parse(&format!("https://{dns_name}/")).unwrap(),
1553 exhausted_accounting.clone(),
1554 CancellationToken::new(),
1555 None,
1556 Some(TlsNegotiationOwnedTransportProfile::new(
1557 fixed_address,
1558 material.root_der,
1559 )),
1560 )
1561 .unwrap()
1562 .execute()
1563 .await;
1564 assert!(exhausted.is_valid());
1565 assert_eq!(exhausted.terminal(), "budget_exhausted");
1566 assert_eq!(exhausted.attempted_connection_count(), 0);
1567 assert_eq!(exhausted.cells()[0].outcome(), "budget_exhausted");
1568 assert!(exhausted_accounting.dispatch_audit().receipts().is_empty());
1569 }
1570
1571 #[tokio::test]
1572 async fn failed_first_handshake_cannot_reset_the_shared_active_limit() {
1573 let dns_name = "active-tls.termivar.test";
1574 let material = owned_tls_material(dns_name);
1575 let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
1576 .await
1577 .unwrap();
1578 let address = listener.local_addr().unwrap();
1579 let server = tokio::spawn(async move {
1580 let (stream, peer) = listener.accept().await.unwrap();
1581 assert!(peer.ip().is_loopback());
1582 drop(stream);
1583 });
1584 let accounting = RequestAccountingBroker::new(
1585 crate::RuntimeBudget::default()
1586 .with_max_total_requests(2)
1587 .with_max_active_verifications(1),
1588 );
1589 let runtime = TlsNegotiationReviewRuntime::new(
1590 &Url::parse(&format!("https://{dns_name}:{}/", address.port())).unwrap(),
1591 accounting.clone(),
1592 CancellationToken::new(),
1593 None,
1594 Some(TlsNegotiationOwnedTransportProfile::new(
1595 address,
1596 material.root_der,
1597 )),
1598 )
1599 .unwrap();
1600
1601 let audit = runtime.execute().await;
1602 tokio::time::timeout(Duration::from_secs(5), server)
1603 .await
1604 .expect("rejecting server completed")
1605 .unwrap();
1606
1607 assert!(audit.is_valid());
1608 assert_eq!(audit.terminal(), "budget_exhausted");
1609 assert_eq!(audit.attempted_connection_count(), 1);
1610 assert_eq!(audit.cells()[0].dispatch_status(), "dispatched");
1611 assert_eq!(audit.cells()[0].outcome(), "transport_failed");
1612 assert_eq!(audit.cells()[1].dispatch_status(), "not_dispatched");
1613 assert_eq!(audit.cells()[1].outcome(), "budget_exhausted");
1614 let transport = accounting.dispatch_audit();
1615 assert_eq!(transport.receipts().len(), 1);
1616 assert_eq!(
1617 transport.receipts()[0].outcome(),
1618 TransportDispatchOutcome::TransportFailure
1619 );
1620 }
1621
1622 #[tokio::test]
1623 async fn receipt_capacity_exhaustion_refuses_before_dns_or_transport() {
1624 let budget = crate::RuntimeBudget::default().with_max_total_requests(
1625 u32::try_from(HARD_MAX_TRANSPORT_DISPATCH_RECEIPTS + 2).unwrap(),
1626 );
1627 let accounting = RequestAccountingBroker::new(budget);
1628 for _ in 0..HARD_MAX_TRANSPORT_DISPATCH_RECEIPTS - 1 {
1629 let mut lease = accounting
1630 .try_begin("fixture.passive", DecisionExecutionStage::Passive, None)
1631 .unwrap();
1632 lease.finish(TransportDispatchOutcome::Completed);
1633 }
1634 let target = Url::parse("https://receipt-capacity.invalid/").unwrap();
1635 let runtime = TlsNegotiationReviewRuntime::new(
1636 &target,
1637 accounting.clone(),
1638 CancellationToken::new(),
1639 None,
1640 None,
1641 )
1642 .unwrap();
1643
1644 let audit = runtime.execute().await;
1645
1646 assert!(audit.is_valid());
1647 assert!(!audit.network_matrix_complete());
1648 assert_eq!(audit.terminal(), "budget_exhausted");
1649 assert_eq!(audit.attempted_connection_count(), 0);
1650 assert_eq!(audit.cells()[0].dispatch_status(), "not_dispatched");
1651 assert_eq!(audit.cells()[0].outcome(), "budget_exhausted");
1652 assert_eq!(audit.cells()[1].outcome(), "not_reached");
1653 let transport = accounting.dispatch_audit();
1654 assert_eq!(
1655 transport.receipts().len(),
1656 HARD_MAX_TRANSPORT_DISPATCH_RECEIPTS - 1
1657 );
1658 assert_eq!(transport.omitted_receipt_count(), 0);
1659 assert!(transport.receipts().iter().all(|receipt| {
1660 receipt.action_id() != TLS_NEGOTIATION_REVIEW_ACTION_TLS13
1661 && receipt.action_id() != TLS_NEGOTIATION_REVIEW_ACTION_TLS12
1662 }));
1663 }
1664}