1use std::collections::{HashMap, VecDeque};
11
12use bytes::Bytes;
13use sipx_sip::headers::Date;
14use sipx_sip::identity::{
15 CanonicalIdentity, Es256SigningKey, Es256VerifyingKey, IdentityError, IdentityHeader,
16 date_from_timestamp, date_timestamp, passport_issued_at, request_identities, sign_passport,
17 verify_passport,
18};
19use sipx_sip::{Header, HeaderName, Request, Uri};
20use thiserror::Error;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Freshness {
25 seconds: u64,
26}
27
28impl Freshness {
29 #[must_use]
31 pub const fn from_seconds(seconds: u64) -> Self {
32 Self { seconds }
33 }
34
35 fn accepts(self, left: i64, right: i64) -> bool {
36 left.abs_diff(right) <= self.seconds
37 }
38}
39
40impl Default for Freshness {
41 fn default() -> Self {
42 Self::from_seconds(60)
43 }
44}
45
46pub trait Authority {
48 fn authorizes(&self, origin: &CanonicalIdentity) -> bool;
50}
51
52impl<F> Authority for F
53where
54 F: Fn(&CanonicalIdentity) -> bool,
55{
56 fn authorizes(&self, origin: &CanonicalIdentity) -> bool {
57 self(origin)
58 }
59}
60
61pub struct SigningCredential {
63 key: Es256SigningKey,
64 info: String,
65 not_before: i64,
66 not_after: i64,
67}
68
69impl std::fmt::Debug for SigningCredential {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter
72 .debug_struct("SigningCredential")
73 .field("info", &self.info)
74 .field("not_before", &self.not_before)
75 .field("not_after", &self.not_after)
76 .field("key", &"[REDACTED]")
77 .finish()
78 }
79}
80
81impl SigningCredential {
82 pub fn from_pkcs8_pem(
84 pem: &str,
85 info: impl Into<String>,
86 not_before: i64,
87 not_after: i64,
88 ) -> Result<Self, AuthenticationError> {
89 let info = info.into();
90 validate_info(&info)?;
91 if not_before > not_after {
92 return Err(AuthenticationError::Credential);
93 }
94 Ok(Self {
95 key: Es256SigningKey::from_pkcs8_pem(pem)
96 .map_err(|_| AuthenticationError::Credential)?,
97 info,
98 not_before,
99 not_after,
100 })
101 }
102
103 #[must_use]
105 pub fn verifying_key(&self) -> Es256VerifyingKey {
106 self.key.verifying_key()
107 }
108
109 fn valid_at(&self, timestamp: i64) -> bool {
110 (self.not_before..=self.not_after).contains(×tamp)
111 }
112}
113
114fn validate_info(info: &str) -> Result<(), AuthenticationError> {
115 Uri::parse(Bytes::copy_from_slice(info.as_bytes()))
116 .map(|_| ())
117 .map_err(|_| AuthenticationError::Credential)
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
122#[non_exhaustive]
123pub enum AuthenticationError {
124 #[error("the request does not carry usable From and To identities")]
126 Identity,
127 #[error("the authentication service is not authoritative for the origin")]
129 NotAuthoritative,
130 #[error("the request carries a stale or invalid Date")]
132 StaleDate,
133 #[error("the signing credential is not valid for this request")]
135 Credential,
136 #[error("the Identity header could not be built")]
138 Build,
139}
140
141#[derive(Debug)]
143pub struct AuthenticationService<A> {
144 authority: A,
145 credential: SigningCredential,
146 freshness: Freshness,
147}
148
149impl<A: Authority> AuthenticationService<A> {
150 #[must_use]
152 pub fn new(authority: A, credential: SigningCredential) -> Self {
153 Self {
154 authority,
155 credential,
156 freshness: Freshness::default(),
157 }
158 }
159
160 #[must_use]
162 pub const fn with_freshness(mut self, freshness: Freshness) -> Self {
163 self.freshness = freshness;
164 self
165 }
166
167 pub fn sign(&self, request: &mut Request, now: i64) -> Result<(), AuthenticationError> {
171 let (origin, destination) =
172 request_identities(request).map_err(|_| AuthenticationError::Identity)?;
173 if !self.authority.authorizes(&origin) {
174 return Err(AuthenticationError::NotAuthoritative);
175 }
176
177 let date_count = request.headers.get_all(&HeaderName::Date).count();
178 if date_count > 1 {
179 return Err(AuthenticationError::StaleDate);
180 }
181 let existing_date = request.headers.typed::<Date>();
182 let (date, issued_at, add_date) = match existing_date {
183 None => (
184 date_from_timestamp(now).map_err(|_| AuthenticationError::StaleDate)?,
185 now,
186 true,
187 ),
188 Some(Ok(date)) => {
189 let timestamp =
190 date_timestamp(&date).map_err(|_| AuthenticationError::StaleDate)?;
191 if !self.freshness.accepts(now, timestamp) {
192 return Err(AuthenticationError::StaleDate);
193 }
194 (date, timestamp, false)
195 }
196 Some(Err(_)) => return Err(AuthenticationError::StaleDate),
197 };
198 if !self.credential.valid_at(now) || !self.credential.valid_at(issued_at) {
199 return Err(AuthenticationError::Credential);
200 }
201
202 let identity = sign_passport(
203 &self.credential.key,
204 &origin,
205 &destination,
206 issued_at,
207 &self.credential.info,
208 );
209 let header = Header::build(HeaderName::Identity, identity.to_bytes())
210 .map_err(|_| AuthenticationError::Build)?;
211 if add_date {
212 request.headers.push(
213 Header::build(HeaderName::Date, Bytes::from(date.0))
214 .map_err(|_| AuthenticationError::Build)?,
215 );
216 }
217 request.headers.push(header);
218 Ok(())
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct VerificationCredential {
225 key: Es256VerifyingKey,
226 not_before: i64,
227 not_after: i64,
228}
229
230impl VerificationCredential {
231 pub fn from_public_key_pem(
233 pem: &str,
234 not_before: i64,
235 not_after: i64,
236 ) -> Result<Self, IdentityError> {
237 Self::new(
238 Es256VerifyingKey::from_public_key_pem(pem)?,
239 not_before,
240 not_after,
241 )
242 }
243
244 pub fn new(
246 key: Es256VerifyingKey,
247 not_before: i64,
248 not_after: i64,
249 ) -> Result<Self, IdentityError> {
250 if not_before > not_after {
251 return Err(IdentityError::Credential);
252 }
253 Ok(Self {
254 key,
255 not_before,
256 not_after,
257 })
258 }
259
260 fn valid_at(&self, timestamp: i64) -> bool {
261 (self.not_before..=self.not_after).contains(×tamp)
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
267#[non_exhaustive]
268pub enum CredentialError {
269 #[error("credential unavailable")]
271 Unavailable,
272 #[error("credential unsupported")]
274 Unsupported,
275}
276
277pub trait CredentialFetcher {
279 fn fetch(&mut self, info: &str, at: i64) -> Result<VerificationCredential, CredentialError>;
283
284 fn authorizes(&self, credential: &VerificationCredential, origin: &CanonicalIdentity) -> bool;
286}
287
288#[derive(Debug)]
290pub struct CachedCredentials<F> {
291 fetcher: F,
292 capacity: usize,
293 entries: HashMap<String, VerificationCredential>,
294 order: VecDeque<String>,
295}
296
297impl<F> CachedCredentials<F> {
298 #[must_use]
300 pub fn new(fetcher: F, capacity: usize) -> Self {
301 Self {
302 fetcher,
303 capacity,
304 entries: HashMap::new(),
305 order: VecDeque::new(),
306 }
307 }
308
309 pub fn into_inner(self) -> F {
311 self.fetcher
312 }
313}
314
315impl<F: CredentialFetcher> CredentialFetcher for CachedCredentials<F> {
316 fn fetch(&mut self, info: &str, at: i64) -> Result<VerificationCredential, CredentialError> {
317 if let Some(credential) = self.entries.get(info) {
318 if credential.valid_at(at) {
319 return Ok(credential.clone());
320 }
321 self.entries.remove(info);
322 self.order.retain(|cached| cached != info);
323 }
324 let credential = self.fetcher.fetch(info, at)?;
325 if self.capacity > 0 {
326 while self.entries.len() >= self.capacity {
327 let Some(oldest) = self.order.pop_front() else {
328 break;
329 };
330 self.entries.remove(&oldest);
331 }
332 self.entries.insert(info.to_owned(), credential.clone());
333 self.order.push_back(info.to_owned());
334 }
335 Ok(credential)
336 }
337
338 fn authorizes(&self, credential: &VerificationCredential, origin: &CanonicalIdentity) -> bool {
339 self.fetcher.authorizes(credential, origin)
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub enum Verification {
346 Verified {
348 origin: CanonicalIdentity,
350 info: String,
352 },
353 Unverified,
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
359#[non_exhaustive]
360pub enum VerificationFailure {
361 #[error("428 Use Identity Header")]
363 MissingIdentity,
364 #[error("436 Bad Identity Info")]
366 BadIdentityInfo,
367 #[error("437 Unsupported Credential")]
369 UnsupportedCredential,
370 #[error("403 Stale Date")]
372 StaleDate,
373 #[error("438 Invalid Identity Header")]
375 InvalidIdentity,
376}
377
378impl VerificationFailure {
379 #[must_use]
381 pub const fn status(self) -> u16 {
382 match self {
383 Self::MissingIdentity => 428,
384 Self::BadIdentityInfo => 436,
385 Self::UnsupportedCredential => 437,
386 Self::StaleDate => 403,
387 Self::InvalidIdentity => 438,
388 }
389 }
390
391 #[must_use]
393 pub const fn reason(self) -> &'static str {
394 match self {
395 Self::MissingIdentity => "Use Identity Header",
396 Self::BadIdentityInfo => "Bad Identity Info",
397 Self::UnsupportedCredential => "Unsupported Credential",
398 Self::StaleDate => "Stale Date",
399 Self::InvalidIdentity => "Invalid Identity Header",
400 }
401 }
402}
403
404#[derive(Debug)]
406pub struct VerificationService<S> {
407 source: S,
408 freshness: Freshness,
409}
410
411impl<F: CredentialFetcher> VerificationService<CachedCredentials<F>> {
412 #[must_use]
414 pub fn new(fetcher: F) -> Self {
415 Self {
416 source: CachedCredentials::new(fetcher, 64),
417 freshness: Freshness::default(),
418 }
419 }
420}
421
422impl<S: CredentialFetcher> VerificationService<S> {
423 #[must_use]
425 pub fn with_source(source: S) -> Self {
426 Self {
427 source,
428 freshness: Freshness::default(),
429 }
430 }
431
432 #[must_use]
434 pub const fn with_freshness(mut self, freshness: Freshness) -> Self {
435 self.freshness = freshness;
436 self
437 }
438
439 pub fn into_source(self) -> S {
441 self.source
442 }
443
444 pub fn verify(
446 &mut self,
447 request: &Request,
448 now: i64,
449 required: bool,
450 ) -> Result<Verification, VerificationFailure> {
451 let rows: Vec<_> = request.headers.typed_all::<IdentityHeader>().collect();
452 if rows.is_empty() {
453 return if required {
454 Err(VerificationFailure::MissingIdentity)
455 } else {
456 Ok(Verification::Unverified)
457 };
458 }
459
460 let mut best = None;
461 let mut usable = false;
462 for row in rows {
463 let Ok(header) = row else {
464 promote(&mut best, VerificationFailure::InvalidIdentity);
465 continue;
466 };
467 if header.passport_type.is_some() {
469 continue;
470 }
471 usable = true;
472 if header.algorithm != "ES256" {
473 promote(&mut best, VerificationFailure::InvalidIdentity);
474 continue;
475 }
476
477 let Ok((origin, destination)) = request_identities(request) else {
479 promote(&mut best, VerificationFailure::InvalidIdentity);
480 continue;
481 };
482
483 let credential = match self.source.fetch(&header.info, now) {
485 Ok(credential) => credential,
486 Err(CredentialError::Unavailable) => {
487 promote(&mut best, VerificationFailure::BadIdentityInfo);
488 continue;
489 }
490 Err(CredentialError::Unsupported) => {
491 promote(&mut best, VerificationFailure::UnsupportedCredential);
492 continue;
493 }
494 };
495 if !self.source.authorizes(&credential, &origin) {
496 promote(&mut best, VerificationFailure::UnsupportedCredential);
497 continue;
498 }
499
500 if request.headers.get_all(&HeaderName::Date).count() != 1 {
502 promote(&mut best, VerificationFailure::InvalidIdentity);
503 continue;
504 }
505 let Some(Ok(date)) = request.headers.typed::<Date>() else {
506 promote(&mut best, VerificationFailure::InvalidIdentity);
507 continue;
508 };
509 let Ok(date) = date_timestamp(&date) else {
510 promote(&mut best, VerificationFailure::InvalidIdentity);
511 continue;
512 };
513 let issued_at = match passport_issued_at(&header) {
514 Ok(Some(issued_at)) => issued_at,
515 Ok(None) => date,
516 Err(_) => {
517 promote(&mut best, VerificationFailure::InvalidIdentity);
518 continue;
519 }
520 };
521 if !self.freshness.accepts(now, date) || !self.freshness.accepts(now, issued_at) {
522 promote(&mut best, VerificationFailure::StaleDate);
523 continue;
524 }
525 if !credential.valid_at(date) || !credential.valid_at(now) {
526 promote(&mut best, VerificationFailure::UnsupportedCredential);
527 continue;
528 }
529
530 if verify_passport(&header, &credential.key, &origin, &destination, date).is_ok() {
535 return Ok(Verification::Verified {
536 origin,
537 info: header.info,
538 });
539 }
540 promote(&mut best, VerificationFailure::InvalidIdentity);
541 }
542
543 if usable {
544 Err(best.unwrap_or(VerificationFailure::InvalidIdentity))
545 } else if required {
546 Err(best.unwrap_or(VerificationFailure::MissingIdentity))
549 } else {
550 Ok(Verification::Unverified)
551 }
552 }
553}
554
555fn promote(best: &mut Option<VerificationFailure>, candidate: VerificationFailure) {
556 let rank = |failure: VerificationFailure| match failure {
557 VerificationFailure::MissingIdentity => 0,
558 VerificationFailure::BadIdentityInfo => 1,
559 VerificationFailure::UnsupportedCredential => 2,
560 VerificationFailure::StaleDate => 3,
561 VerificationFailure::InvalidIdentity => 4,
562 };
563 if best.is_none_or(|current| rank(candidate) > rank(current)) {
564 *best = Some(candidate);
565 }
566}
567
568#[cfg(test)]
569#[allow(
570 clippy::unwrap_used,
571 clippy::expect_used,
572 clippy::panic,
573 clippy::indexing_slicing
574)]
575mod tests {
576 use super::*;
577 use sipx_sip::Method;
578 use sipx_sip::build::RequestBuilder;
579
580 const PRIVATE: &str = "-----BEGIN PRIVATE KEY-----\n\
581MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgi7q2TZvN9VDFg8Vy\n\
582qCP06bETrR2v8MRvr89rn4i+UAahRANCAAQWfaj1HUETpoNCrOtp9KA8o0V79IuW\n\
583ARKt9C1cFPkyd3FBP4SeiNZxQhDrD0tdBHls3/wFe8++K2FrPyQF9vuh\n\
584-----END PRIVATE KEY-----";
585
586 fn request() -> Request {
587 RequestBuilder::new(
588 Method::Invite,
589 Uri::parse(Bytes::from_static(b"sip:alice@example.com")).unwrap(),
590 )
591 .header(
592 HeaderName::From,
593 Bytes::from_static(b"<sip:+12155551212@example.com;user=phone>;tag=x"),
594 )
595 .unwrap()
596 .header(
597 HeaderName::To,
598 Bytes::from_static(b"<sip:alice@example.com>"),
599 )
600 .unwrap()
601 .build()
602 }
603
604 fn credential() -> SigningCredential {
605 SigningCredential::from_pkcs8_pem(
606 PRIVATE,
607 "https://cert.example.org/passport.cer",
608 i64::MIN,
609 i64::MAX,
610 )
611 .unwrap()
612 }
613
614 #[test]
615 fn signing_adds_date_and_identity_only_after_authority() {
616 let mut denied = request();
617 let service = AuthenticationService::new(|_: &CanonicalIdentity| false, credential());
618 assert_eq!(
619 service.sign(&mut denied, 1_471_375_418),
620 Err(AuthenticationError::NotAuthoritative)
621 );
622 assert!(denied.headers.get(&HeaderName::Date).is_none());
623 assert!(denied.headers.get(&HeaderName::Identity).is_none());
624
625 let mut allowed = request();
626 let service = AuthenticationService::new(|_: &CanonicalIdentity| true, credential());
627 service.sign(&mut allowed, 1_471_375_418).unwrap();
628 assert!(
629 allowed
630 .headers
631 .typed::<Date>()
632 .is_some_and(|date| date.is_ok())
633 );
634 assert!(
635 allowed
636 .headers
637 .typed::<IdentityHeader>()
638 .is_some_and(|identity| identity.is_ok())
639 );
640 }
641
642 #[derive(Debug)]
643 struct CountingFetcher {
644 calls: usize,
645 key: Es256VerifyingKey,
646 result: Option<CredentialError>,
647 }
648
649 impl CredentialFetcher for CountingFetcher {
650 fn fetch(
651 &mut self,
652 _info: &str,
653 _at: i64,
654 ) -> Result<VerificationCredential, CredentialError> {
655 self.calls += 1;
656 if let Some(error) = self.result {
657 return Err(error);
658 }
659 VerificationCredential::new(self.key.clone(), i64::MIN, i64::MAX)
660 .map_err(|_| CredentialError::Unsupported)
661 }
662
663 fn authorizes(
664 &self,
665 _credential: &VerificationCredential,
666 _origin: &CanonicalIdentity,
667 ) -> bool {
668 true
669 }
670 }
671
672 fn signed() -> (Request, Es256VerifyingKey) {
673 let credential = credential();
674 let key = credential.verifying_key();
675 let service = AuthenticationService::new(|_: &CanonicalIdentity| true, credential);
676 let mut request = request();
677 service.sign(&mut request, 1_471_375_418).unwrap();
678 (request, key)
679 }
680
681 #[test]
682 fn unsupported_ppt_is_428_and_does_not_fetch() {
683 let (mut request, key) = signed();
684 let parsed = request.headers.typed::<IdentityHeader>().unwrap().unwrap();
685 request.headers.remove_all(&HeaderName::Identity);
686 let mut unsupported = parsed;
687 unsupported.passport_type = Some("unknown".to_owned());
688 request
689 .headers
690 .push(Header::build(HeaderName::Identity, unsupported.to_bytes()).unwrap());
691 let fetcher = CountingFetcher {
692 calls: 0,
693 key,
694 result: None,
695 };
696 let mut verifier = VerificationService::new(fetcher);
697 assert_eq!(
698 verifier.verify(&request, 1_471_375_418, true),
699 Err(VerificationFailure::MissingIdentity)
700 );
701 assert_eq!(verifier.into_source().into_inner().calls, 0);
702 }
703
704 #[test]
705 fn acquisition_failures_keep_their_distinct_statuses() {
706 let (request, key) = signed();
707 for (error, status) in [
708 (CredentialError::Unavailable, 436),
709 (CredentialError::Unsupported, 437),
710 ] {
711 let fetcher = CountingFetcher {
712 calls: 0,
713 key: key.clone(),
714 result: Some(error),
715 };
716 let mut verifier = VerificationService::new(fetcher);
717 assert_eq!(
718 verifier
719 .verify(&request, 1_471_375_418, true)
720 .unwrap_err()
721 .status(),
722 status
723 );
724 }
725 }
726
727 #[test]
728 fn stale_date_is_403_after_credential_acquisition() {
729 let (request, key) = signed();
730 let fetcher = CountingFetcher {
731 calls: 0,
732 key,
733 result: None,
734 };
735 let mut verifier = VerificationService::new(fetcher);
736 let failure = verifier.verify(&request, 1_471_375_479, true).unwrap_err();
737 assert_eq!(failure.status(), 403);
738 assert_eq!(verifier.into_source().into_inner().calls, 1);
739 }
740
741 #[test]
742 fn successful_credentials_are_cached_by_exact_info_uri() {
743 let (request, key) = signed();
744 let fetcher = CountingFetcher {
745 calls: 0,
746 key,
747 result: None,
748 };
749 let mut verifier = VerificationService::new(fetcher);
750 verifier.verify(&request, 1_471_375_418, true).unwrap();
751 verifier.verify(&request, 1_471_375_418, true).unwrap();
752 assert_eq!(verifier.into_source().into_inner().calls, 1);
753 }
754
755 #[test]
756 fn cached_credentials_are_bounded_and_evict_by_exact_info_uri() {
757 let (_, key) = signed();
758 let fetcher = CountingFetcher {
759 calls: 0,
760 key,
761 result: None,
762 };
763 let mut cache = CachedCredentials::new(fetcher, 1);
764 cache.fetch("https://cert.example.org/one", 1).unwrap();
765 cache.fetch("https://cert.example.org/one", 1).unwrap();
766 cache.fetch("https://cert.example.org/two", 1).unwrap();
767 cache.fetch("https://cert.example.org/one", 1).unwrap();
768 assert_eq!(cache.entries.len(), 1);
769 assert_eq!(cache.into_inner().calls, 3);
770 }
771
772 #[test]
773 fn an_expired_cached_credential_is_refetched_at_the_same_uri() {
774 #[derive(Debug)]
775 struct RotatingFetcher {
776 calls: usize,
777 key: Es256VerifyingKey,
778 }
779
780 impl CredentialFetcher for RotatingFetcher {
781 fn fetch(
782 &mut self,
783 _info: &str,
784 _at: i64,
785 ) -> Result<VerificationCredential, CredentialError> {
786 self.calls += 1;
787 let not_after = if self.calls == 1 { 10 } else { i64::MAX };
788 VerificationCredential::new(self.key.clone(), i64::MIN, not_after)
789 .map_err(|_| CredentialError::Unsupported)
790 }
791
792 fn authorizes(
793 &self,
794 _credential: &VerificationCredential,
795 _origin: &CanonicalIdentity,
796 ) -> bool {
797 true
798 }
799 }
800
801 let (_, key) = signed();
802 let fetcher = RotatingFetcher { calls: 0, key };
803 let mut cache = CachedCredentials::new(fetcher, 1);
804 cache.fetch("https://cert.example.org/one", 10).unwrap();
805 cache.fetch("https://cert.example.org/one", 11).unwrap();
806 assert_eq!(cache.entries.len(), 1);
807 assert_eq!(cache.into_inner().calls, 2);
808 }
809
810 #[test]
811 fn malformed_identity_is_438_when_identity_is_required() {
812 let (_, key) = signed();
813 let mut request = request();
814 request.headers.push(
815 Header::build(HeaderName::Identity, Bytes::from_static(b"not-a-passport")).unwrap(),
816 );
817 let fetcher = CountingFetcher {
818 calls: 0,
819 key,
820 result: None,
821 };
822 let mut verifier = VerificationService::new(fetcher);
823 assert_eq!(
824 verifier.verify(&request, 1_471_375_418, true),
825 Err(VerificationFailure::InvalidIdentity)
826 );
827 assert_eq!(verifier.into_source().into_inner().calls, 0);
828 }
829
830 #[test]
831 fn full_passport_iat_must_equal_the_sip_date() {
832 let (mut request, key) = signed();
833 request.headers.remove_all(&HeaderName::Date);
834 let different_but_fresh = date_from_timestamp(1_471_375_419).unwrap();
835 request
836 .headers
837 .push(Header::build(HeaderName::Date, Bytes::from(different_but_fresh.0)).unwrap());
838 let fetcher = CountingFetcher {
839 calls: 0,
840 key,
841 result: None,
842 };
843 let mut verifier = VerificationService::new(fetcher);
844 assert_eq!(
845 verifier.verify(&request, 1_471_375_419, true),
846 Err(VerificationFailure::InvalidIdentity)
847 );
848 }
849}