1use std::{
4 sync::{Arc, Mutex, MutexGuard},
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use sha2::{Digest, Sha256};
9use x509_parser::{
10 certificate::X509CertificateParser,
11 extensions::{GeneralName, SubjectAlternativeName},
12 nom::Parser,
13 oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME,
14 prelude::FromDer,
15};
16
17pub const TLS_OBSERVATION_AUDIT_SCHEMA: &str = "security.tls-observation-audit/v1";
19pub const TLS_OBSERVATION_POLICY_ID: &str = "termivar.existing-connection-tls-observation/v1";
21pub const TLS_OBSERVATION_BACKEND_LIMIT: &str = "not_exposed_by_backend";
23pub const TLS_OBSERVATION_REVOCATION_STATUS: &str = "not_checked";
25pub const TLS_OBSERVATION_VALIDATION_SCOPE: &str = "successful_https_response_connection/v1";
27pub const TLS_OBSERVATION_SOURCE_SCOPE: &str = "assessment_exact_origin_existing_connections/v1";
29pub const TLS_OBSERVATION_CLOCK_ASSURANCE: &str = "local_system_clock_not_independently_verified";
31
32pub const MAX_TLS_LEAF_CERTIFICATE_BYTES: usize = 64 * 1024;
34pub const MAX_TLS_RETAINED_LEAF_OBSERVATIONS: usize = 16;
36pub const MAX_TLS_SAN_ENTRIES_PER_CERTIFICATE: usize = 256;
38pub const MAX_TLS_CERTIFICATE_EXTENSIONS: usize = 64;
40pub const MAX_TLS_SUBJECT_ALTERNATIVE_NAME_BYTES: usize = 16 * 1024;
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum TlsObservationTargetScheme {
46 Http,
48 Https,
50 Other,
52}
53
54impl TlsObservationTargetScheme {
55 pub(crate) fn from_scheme(value: &str) -> Self {
56 match value {
57 "http" => Self::Http,
58 "https" => Self::Https,
59 _ => Self::Other,
60 }
61 }
62
63 pub const fn as_str(self) -> &'static str {
65 match self {
66 Self::Http => "http",
67 Self::Https => "https",
68 Self::Other => "other",
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub enum TlsCertificateTimeStatus {
76 ValidAtObservation,
78 NotYetValidAtObservation,
80 ExpiredAtObservation,
82}
83
84impl TlsCertificateTimeStatus {
85 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::ValidAtObservation => "valid_at_observation",
89 Self::NotYetValidAtObservation => "not_yet_valid_at_observation",
90 Self::ExpiredAtObservation => "expired_at_observation",
91 }
92 }
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct TlsLeafObservation {
98 sha256: String,
99 byte_length: u64,
100 not_before_epoch_seconds: i64,
101 not_after_epoch_seconds: i64,
102 observed_at_epoch_seconds: i64,
103 dns_san_count: u16,
104 ip_san_count: u16,
105 other_san_count: u16,
106 san_count_truncated: bool,
107 certificate_time_status: TlsCertificateTimeStatus,
108 response_occurrence_count: u64,
109}
110
111impl TlsLeafObservation {
112 pub fn sha256(&self) -> &str {
114 &self.sha256
115 }
116 pub const fn byte_length(&self) -> u64 {
118 self.byte_length
119 }
120 pub const fn not_before_epoch_seconds(&self) -> i64 {
122 self.not_before_epoch_seconds
123 }
124 pub const fn not_after_epoch_seconds(&self) -> i64 {
126 self.not_after_epoch_seconds
127 }
128 pub const fn observed_at_epoch_seconds(&self) -> i64 {
130 self.observed_at_epoch_seconds
131 }
132 pub const fn dns_san_count(&self) -> u16 {
134 self.dns_san_count
135 }
136 pub const fn ip_san_count(&self) -> u16 {
138 self.ip_san_count
139 }
140 pub const fn other_san_count(&self) -> u16 {
142 self.other_san_count
143 }
144 pub const fn san_count_truncated(&self) -> bool {
146 self.san_count_truncated
147 }
148 pub const fn certificate_time_status(&self) -> TlsCertificateTimeStatus {
150 self.certificate_time_status
151 }
152 pub const fn response_occurrence_count(&self) -> u64 {
154 self.response_occurrence_count
155 }
156 pub const fn standard_transport_validation_succeeded(&self) -> bool {
158 true
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct WebAssessmentTlsObservationAudit {
165 target_scheme: TlsObservationTargetScheme,
166 successful_https_response_count: u64,
167 plaintext_response_count: u64,
168 tls_info_unavailable_count: u64,
169 malformed_certificate_count: u64,
170 certificate_limit_rejection_count: u64,
171 unretained_leaf_response_count: u64,
172 leaf_observations: Vec<TlsLeafObservation>,
173}
174
175impl WebAssessmentTlsObservationAudit {
176 pub const fn schema(&self) -> &'static str {
178 TLS_OBSERVATION_AUDIT_SCHEMA
179 }
180 pub const fn policy(&self) -> &'static str {
182 TLS_OBSERVATION_POLICY_ID
183 }
184 pub const fn selected(&self) -> bool {
186 true
187 }
188 pub const fn additional_request_count(&self) -> u64 {
190 0
191 }
192 pub const fn target_scheme(&self) -> TlsObservationTargetScheme {
194 self.target_scheme
195 }
196 pub const fn observation_source_scope(&self) -> &'static str {
198 TLS_OBSERVATION_SOURCE_SCOPE
199 }
200 pub const fn observation_clock_assurance(&self) -> &'static str {
202 TLS_OBSERVATION_CLOCK_ASSURANCE
203 }
204 pub const fn successful_https_response_count(&self) -> u64 {
206 self.successful_https_response_count
207 }
208 pub const fn plaintext_response_count(&self) -> u64 {
210 self.plaintext_response_count
211 }
212 pub const fn tls_info_unavailable_count(&self) -> u64 {
214 self.tls_info_unavailable_count
215 }
216 pub const fn malformed_certificate_count(&self) -> u64 {
218 self.malformed_certificate_count
219 }
220 pub const fn certificate_limit_rejection_count(&self) -> u64 {
222 self.certificate_limit_rejection_count
223 }
224 pub const fn unretained_leaf_response_count(&self) -> u64 {
226 self.unretained_leaf_response_count
227 }
228 pub fn leaf_observations(&self) -> &[TlsLeafObservation] {
230 &self.leaf_observations
231 }
232 pub const fn protocol(&self) -> &'static str {
234 TLS_OBSERVATION_BACKEND_LIMIT
235 }
236 pub const fn cipher_suite(&self) -> &'static str {
238 TLS_OBSERVATION_BACKEND_LIMIT
239 }
240 pub const fn alpn_protocol(&self) -> &'static str {
242 TLS_OBSERVATION_BACKEND_LIMIT
243 }
244 pub const fn full_chain(&self) -> &'static str {
246 TLS_OBSERVATION_BACKEND_LIMIT
247 }
248 pub const fn connection_reuse(&self) -> &'static str {
250 TLS_OBSERVATION_BACKEND_LIMIT
251 }
252 pub const fn session_resumption(&self) -> &'static str {
254 TLS_OBSERVATION_BACKEND_LIMIT
255 }
256 pub const fn handshake_kind(&self) -> &'static str {
258 TLS_OBSERVATION_BACKEND_LIMIT
259 }
260 pub const fn revocation(&self) -> &'static str {
262 TLS_OBSERVATION_REVOCATION_STATUS
263 }
264 pub const fn standard_transport_validation_scope(&self) -> &'static str {
266 TLS_OBSERVATION_VALIDATION_SCOPE
267 }
268}
269
270#[derive(Default)]
271struct TlsObservationState {
272 successful_https_response_count: u64,
273 plaintext_response_count: u64,
274 tls_info_unavailable_count: u64,
275 malformed_certificate_count: u64,
276 certificate_limit_rejection_count: u64,
277 unretained_leaf_response_count: u64,
278 leaf_observations: Vec<TlsLeafObservation>,
279}
280
281#[derive(Clone)]
283pub(crate) struct TlsObservationCollector {
284 target_scheme: TlsObservationTargetScheme,
285 state: Arc<Mutex<TlsObservationState>>,
286}
287
288impl TlsObservationCollector {
289 pub(crate) fn new(target_scheme: &str) -> Self {
290 Self {
291 target_scheme: TlsObservationTargetScheme::from_scheme(target_scheme),
292 state: Arc::new(Mutex::new(TlsObservationState::default())),
293 }
294 }
295
296 pub(crate) fn observe_plaintext_response(&self) {
297 let mut state = self.state();
298 state.plaintext_response_count = state.plaintext_response_count.saturating_add(1);
299 }
300
301 pub(crate) fn observe_https_response(&self, peer_leaf_der: Option<&[u8]>) {
302 self.observe_https_response_at(peer_leaf_der, epoch_seconds(SystemTime::now()));
303 }
304
305 fn observe_https_response_at(&self, peer_leaf_der: Option<&[u8]>, observed_at: i64) {
306 let mut state = self.state();
307 state.successful_https_response_count =
308 state.successful_https_response_count.saturating_add(1);
309 let Some(der) = peer_leaf_der else {
310 state.tls_info_unavailable_count = state.tls_info_unavailable_count.saturating_add(1);
311 return;
312 };
313 if der.len() > MAX_TLS_LEAF_CERTIFICATE_BYTES {
314 state.certificate_limit_rejection_count =
315 state.certificate_limit_rejection_count.saturating_add(1);
316 return;
317 }
318 let observation = match parse_leaf(der, observed_at) {
319 Ok(observation) => observation,
320 Err(LeafParseError::Malformed) => {
321 state.malformed_certificate_count =
322 state.malformed_certificate_count.saturating_add(1);
323 return;
324 },
325 Err(LeafParseError::LimitRejected) => {
326 state.certificate_limit_rejection_count =
327 state.certificate_limit_rejection_count.saturating_add(1);
328 return;
329 },
330 };
331 if let Some(existing) = state.leaf_observations.iter_mut().find(|existing| {
332 existing.sha256 == observation.sha256 && existing.byte_length == observation.byte_length
333 }) {
334 existing.response_occurrence_count =
335 existing.response_occurrence_count.saturating_add(1);
336 } else if state.leaf_observations.len() < MAX_TLS_RETAINED_LEAF_OBSERVATIONS {
337 state.leaf_observations.push(observation);
338 } else {
339 state.unretained_leaf_response_count =
340 state.unretained_leaf_response_count.saturating_add(1);
341 }
342 }
343
344 pub(crate) fn audit(&self) -> WebAssessmentTlsObservationAudit {
345 let state = self.state();
346 WebAssessmentTlsObservationAudit {
347 target_scheme: self.target_scheme,
348 successful_https_response_count: state.successful_https_response_count,
349 plaintext_response_count: state.plaintext_response_count,
350 tls_info_unavailable_count: state.tls_info_unavailable_count,
351 malformed_certificate_count: state.malformed_certificate_count,
352 certificate_limit_rejection_count: state.certificate_limit_rejection_count,
353 unretained_leaf_response_count: state.unretained_leaf_response_count,
354 leaf_observations: state.leaf_observations.clone(),
355 }
356 }
357
358 #[cfg(test)]
359 pub(crate) fn shares_state_with(&self, other: &Self) -> bool {
360 Arc::ptr_eq(&self.state, &other.state)
361 }
362
363 fn state(&self) -> MutexGuard<'_, TlsObservationState> {
364 self.state
365 .lock()
366 .unwrap_or_else(std::sync::PoisonError::into_inner)
367 }
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
371enum LeafParseError {
372 Malformed,
373 LimitRejected,
374}
375
376fn parse_leaf(der: &[u8], observed_at: i64) -> Result<TlsLeafObservation, LeafParseError> {
377 let mut parser = X509CertificateParser::new().with_deep_parse_extensions(false);
378 let (remainder, certificate) = parser.parse(der).map_err(|_| LeafParseError::Malformed)?;
379 if !remainder.is_empty() {
380 return Err(LeafParseError::Malformed);
381 }
382 let extensions = certificate.extensions();
383 if extensions.len() > MAX_TLS_CERTIFICATE_EXTENSIONS {
384 return Err(LeafParseError::LimitRejected);
385 }
386 let not_before = certificate.validity().not_before.timestamp();
387 let not_after = certificate.validity().not_after.timestamp();
388 if not_after < not_before {
389 return Err(LeafParseError::Malformed);
390 }
391 let raw_san = select_unique_san_value(
392 extensions.len(),
393 extensions
394 .iter()
395 .filter(|extension| extension.oid == OID_X509_EXT_SUBJECT_ALT_NAME)
396 .map(|extension| extension.value),
397 )?;
398 let (dns_san_count, ip_san_count, other_san_count, san_count_truncated) =
399 classify_san(raw_san)?;
400 let certificate_time_status = if observed_at < not_before {
401 TlsCertificateTimeStatus::NotYetValidAtObservation
402 } else if observed_at > not_after {
403 TlsCertificateTimeStatus::ExpiredAtObservation
404 } else {
405 TlsCertificateTimeStatus::ValidAtObservation
406 };
407 Ok(TlsLeafObservation {
408 sha256: format!("{:x}", Sha256::digest(der)),
409 byte_length: u64::try_from(der.len()).unwrap_or(u64::MAX),
410 not_before_epoch_seconds: not_before,
411 not_after_epoch_seconds: not_after,
412 observed_at_epoch_seconds: observed_at,
413 dns_san_count,
414 ip_san_count,
415 other_san_count,
416 san_count_truncated,
417 certificate_time_status,
418 response_occurrence_count: 1,
419 })
420}
421
422fn select_unique_san_value<'a>(
423 extension_count: usize,
424 mut san_values: impl Iterator<Item = &'a [u8]>,
425) -> Result<Option<&'a [u8]>, LeafParseError> {
426 if extension_count > MAX_TLS_CERTIFICATE_EXTENSIONS {
427 return Err(LeafParseError::LimitRejected);
428 }
429 let first = san_values.next();
430 if san_values.next().is_some() {
431 return Err(LeafParseError::Malformed);
432 }
433 Ok(first)
434}
435
436fn classify_san(raw_san: Option<&[u8]>) -> Result<(u16, u16, u16, bool), LeafParseError> {
437 let Some(raw_san) = raw_san else {
438 return Ok((0, 0, 0, false));
439 };
440 if raw_san.len() > MAX_TLS_SUBJECT_ALTERNATIVE_NAME_BYTES {
441 return Err(LeafParseError::LimitRejected);
442 }
443 let (remainder, san) =
444 SubjectAlternativeName::from_der(raw_san).map_err(|_| LeafParseError::Malformed)?;
445 if !remainder.is_empty() {
446 return Err(LeafParseError::Malformed);
447 }
448 let mut dns = 0_u16;
449 let mut ip = 0_u16;
450 let mut other = 0_u16;
451 let mut truncated = false;
452 for (index, name) in san.general_names.iter().enumerate() {
453 if index >= MAX_TLS_SAN_ENTRIES_PER_CERTIFICATE {
454 truncated = true;
455 break;
456 }
457 match name {
458 GeneralName::DNSName(_) => dns = dns.saturating_add(1),
459 GeneralName::IPAddress(value) if matches!(value.len(), 4 | 16) => {
460 ip = ip.saturating_add(1);
461 },
462 _ => other = other.saturating_add(1),
463 }
464 }
465 Ok((dns, ip, other, truncated))
466}
467
468fn epoch_seconds(now: SystemTime) -> i64 {
469 match now.duration_since(UNIX_EPOCH) {
470 Ok(value) => i64::try_from(value.as_secs()).unwrap_or(i64::MAX),
471 Err(value) => -i64::try_from(value.duration().as_secs()).unwrap_or(i64::MAX),
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use base64::{engine::general_purpose::STANDARD, Engine as _};
479
480 const EXPIRED_LEAF: &str = concat!(
481 "MIIFWzCCBEOgAwIBAgISAyBIAwu7NBD5CTxX8suDCMgFMA0GCSqGSIb3DQEBCwUAMEox",
482 "CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQn",
483 "cyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA3MTIxMTEyMzBaFw0xOTEwMTAxMTEy",
484 "MzBaMB0xGzAZBgNVBAMTEmxpc3RzLmZvci1vdXIuaW5mbzCCASIwDQYJKoZIhvcNAQEB",
485 "BQADggEPADCCAQoCggEBAMVoti34X46DaI2nX24C+aZ2OfkmhKbidiXiRTon1MLSMGl1",
486 "oNW9MyRyYYCzP4j6DNKChJnr8ZnVShh2oZD+yHWP9lpnXMGkbsUxejRMU9hnaAB50pXR",
487 "IDAzavkVFCguFlJ8nKkv/Y1Avlw7tc2aZOd3lOZBEr8gJ8mRDGqqsNU+Z12I6slEstz",
488 "GMpsq6AewCVw4lMjdWWgugzUrxQTRAsG87on6gOiQH2cMODN3L7Fq4KOLQIjb3/luQh",
489 "AQhpdKmEGFLin3c+f5or3thCDuwwDtOU1lZf+8t9S8pZPLrZrIs6H2xjXqCRuUY7iRN",
490 "bO18Ukc6rlDYhBj9LT+cpmBbHECAwEAAaOCAmYwggJiMA4GA1UdDwEB/wQEAwIFoDAd",
491 "BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E",
492 "FgQUJj2pvRtl3GloH3He6FX1ds3X0VEwHwYDVR0jBBgwFoAUqEpqYwR93brm0Tm3pkVl",
493 "7/Oo7KEwbwYIKwYBBQUHAQEEYzBhMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcC5pbnQt",
494 "eDMubGV0c2VuY3J5cHQub3JnMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDMu",
495 "bGV0c2VuY3J5cHQub3JnLzAdBgNVHREEFjAUghJsaXN0cy5mb3Itb3VyLmluZm8wTAYD",
496 "VR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0",
497 "cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdgAp",
498 "PFGWVMg5ZbqqUPxYB9S3b79Yeily3KTDDPTlRUf0eAAAAWvmGV7yAAAEAwBHMEUCICQL",
499 "2Sm14aCMLxX9a9RbySgyBfichMRdbu6QA2Mbrl4eAiEA1vgJ7snqUWCgoqEE3SEfK3io",
500 "MopzWBsPvG6LdCuCMRAAdQBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAA",
501 "AWvmGV9oAAAEAwBGMEQCIExGqw3Lo0nSCyUuTRf92FgGASwWYji5UGnXuYnpJrAvAiBw",
502 "8AWVag8fzZ4ogAhY9EFRNdLrUcBjStipL888vyuxKzANBgkqhkiG9w0BAQsFAAOCAQEA",
503 "F8BBLDvSWZg57B6aDtzfUTSGetCYs3k0vJqCJlL+Pz7/UruCSsojQzp5R6jvvgYQ83Ma",
504 "Idwe2mgt+OCQB5v7ylctyBzBmYIw9nPnxEC7HlcJL2K/k5ZjJFRnv4kV1Si8+TIpEAV0",
505 "ksf39KGKemG8kGi4GXV1v03zSv0p8aCarpuoSKBJ4qlB0CvmS2MqV4KnzO0O2h0c/ZQ4",
506 "jg7l53eiN7VPdRMMO1DRw+MaW6I/hEZp+oZQ7hhKXgKUBvF4IGwyrfyIZ8AeWKG4IP98",
507 "COgyRbz7qtrAVevRKCM0ZC2t04A2Fcix40FKEeiE093Aj3cweMYxNLPgwgQP8Xu3kA5QEw=="
508 );
509
510 fn der_length(length: usize) -> Vec<u8> {
511 if length < 128 {
512 return vec![length as u8];
513 }
514 let bytes = length.to_be_bytes();
515 let first = bytes.iter().position(|byte| *byte != 0).unwrap();
516 let significant = &bytes[first..];
517 let mut encoded = vec![0x80 | significant.len() as u8];
518 encoded.extend_from_slice(significant);
519 encoded
520 }
521
522 fn san_sequence(entries: &[(u8, Vec<u8>)]) -> Vec<u8> {
523 let mut content = Vec::new();
524 for (tag, value) in entries {
525 content.push(*tag);
526 content.extend(der_length(value.len()));
527 content.extend(value);
528 }
529 let mut encoded = vec![0x30];
530 encoded.extend(der_length(content.len()));
531 encoded.extend(content);
532 encoded
533 }
534
535 #[test]
536 fn reduces_and_deduplicates_leaf_without_names() {
537 let der = STANDARD.decode(EXPIRED_LEAF).unwrap();
538 let collector = TlsObservationCollector::new("https");
539 collector.observe_https_response_at(Some(&der), 1_700_000_000);
540 collector.observe_https_response_at(Some(&der), 1_700_000_001);
541 let audit = collector.audit();
542 assert_eq!(audit.successful_https_response_count(), 2);
543 assert_eq!(audit.leaf_observations().len(), 1);
544 let leaf = &audit.leaf_observations()[0];
545 assert_eq!(leaf.byte_length(), 1_375);
546 assert_eq!(leaf.dns_san_count(), 1);
547 assert_eq!(leaf.response_occurrence_count(), 2);
548 assert_eq!(
549 leaf.certificate_time_status(),
550 TlsCertificateTimeStatus::ExpiredAtObservation
551 );
552 assert!(!format!("{audit:?}").contains("lists.for-our.info"));
553 assert_eq!(audit.connection_reuse(), TLS_OBSERVATION_BACKEND_LIMIT);
554 assert_eq!(audit.session_resumption(), TLS_OBSERVATION_BACKEND_LIMIT);
555 assert_eq!(audit.handshake_kind(), TLS_OBSERVATION_BACKEND_LIMIT);
556 }
557
558 #[test]
559 fn unavailable_malformed_oversized_and_plaintext_are_distinct() {
560 let collector = TlsObservationCollector::new("https");
561 collector.observe_plaintext_response();
562 collector.observe_https_response_at(None, 0);
563 collector.observe_https_response_at(Some(b"not a certificate"), 0);
564 collector.observe_https_response_at(Some(&vec![0; MAX_TLS_LEAF_CERTIFICATE_BYTES + 1]), 0);
565 let audit = collector.audit();
566 assert_eq!(audit.plaintext_response_count(), 1);
567 assert_eq!(audit.successful_https_response_count(), 3);
568 assert_eq!(audit.tls_info_unavailable_count(), 1);
569 assert_eq!(audit.malformed_certificate_count(), 1);
570 assert_eq!(audit.certificate_limit_rejection_count(), 1);
571 assert_eq!(audit.alpn_protocol(), TLS_OBSERVATION_BACKEND_LIMIT);
572 assert_eq!(audit.session_resumption(), TLS_OBSERVATION_BACKEND_LIMIT);
573 }
574
575 #[test]
576 fn clones_share_one_bounded_state() {
577 let first = TlsObservationCollector::new("http");
578 let second = first.clone();
579 assert!(first.shares_state_with(&second));
580 first.observe_plaintext_response();
581 second.observe_plaintext_response();
582 assert_eq!(first.audit().plaintext_response_count(), 2);
583 assert_eq!(second.audit().target_scheme().as_str(), "http");
584 }
585
586 #[test]
587 fn shallow_extension_and_san_work_bounds_reject_mutations() {
588 let san = san_sequence(&[(0x82, b"example.test".to_vec())]);
589 assert!(select_unique_san_value(
590 MAX_TLS_CERTIFICATE_EXTENSIONS,
591 std::iter::once(san.as_slice())
592 )
593 .is_ok());
594 assert_eq!(
595 select_unique_san_value(MAX_TLS_CERTIFICATE_EXTENSIONS + 1, std::iter::empty()),
596 Err(LeafParseError::LimitRejected)
597 );
598 assert_eq!(
599 select_unique_san_value(2, [san.as_slice(), san.as_slice()].into_iter()),
600 Err(LeafParseError::Malformed)
601 );
602 assert_eq!(
603 classify_san(Some(&vec![0; MAX_TLS_SUBJECT_ALTERNATIVE_NAME_BYTES + 1])),
604 Err(LeafParseError::LimitRejected)
605 );
606
607 let entries = (0..=MAX_TLS_SAN_ENTRIES_PER_CERTIFICATE)
608 .map(|_| (0x82, b"x".to_vec()))
609 .collect::<Vec<_>>();
610 assert_eq!(
611 classify_san(Some(&san_sequence(&entries))),
612 Ok((MAX_TLS_SAN_ENTRIES_PER_CERTIFICATE as u16, 0, 0, true))
613 );
614
615 let invalid_ip = san_sequence(&[(0x87, vec![127, 0, 1])]);
616 assert_eq!(classify_san(Some(&invalid_ip)), Ok((0, 0, 1, false)));
617 let mut trailing = san;
618 trailing.push(0);
619 assert_eq!(
620 classify_san(Some(&trailing)),
621 Err(LeafParseError::Malformed)
622 );
623 }
624}