1use std::collections::{BTreeMap, HashMap};
217use std::str::FromStr;
218use std::sync::{Arc, RwLock};
219use std::time::{Duration, Instant, SystemTime};
220
221use async_trait::async_trait;
222use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
223use base64::Engine as _;
224use rvoip_core_traits::identity::IdentityAssurance;
225
226use crate::errors::{Result, SessionError};
227use crate::types::Credentials;
228
229pub use rvoip_auth_core::{
231 AAuthValidator, ApiKeyVerifier, AuthAuditEvent, AuthAuditOutcome, AuthAuditScheme,
232 AuthAuditSink, AuthFailureReason, AuthRateLimitKey, AuthRateLimitKind, AuthRateLimitVerdict,
233 AuthRateLimiter, BearerAuthError, BearerValidator, CredentialAuthError, DigestAlgorithm,
234 DigestAuthenticator, DigestChallenge, DigestChallengeDetails, DigestClient as DigestAuth,
235 DigestComputed, DigestNonceStatus, DigestReplayStore, DigestResponse, DigestSecret,
236 DigestSecretProvider, JwksJwtValidator, JwtValidator, OAuth2IntrospectionValidator,
237 PasswordVerifier, TokenRevocationChecker, TokenRevocationContext, TokenRevocationStatus,
238};
239
240#[non_exhaustive]
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum SipAuthScheme {
250 Digest,
252 Bearer,
254 Basic,
256 Aka,
258 Other(String),
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum SipAuthSource {
265 Origin,
267 Proxy,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct AuthIdentity {
279 pub scheme: SipAuthScheme,
281 pub username: Option<String>,
283 pub subject: Option<String>,
285 pub realm: Option<String>,
287 pub scopes: Vec<String>,
289 pub source: SipAuthSource,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum SipAuthDecision {
297 Authorized(AuthIdentity),
299 Rejected {
301 challenges: Vec<SipAuthChallenge>,
303 },
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct SipAuthChallenge {
312 pub scheme: SipAuthScheme,
314 pub value: String,
316 pub source: SipAuthSource,
318}
319
320#[derive(Debug, Clone, Default, PartialEq, Eq)]
326pub struct SipAuthContext {
327 pub peer: Option<String>,
329 pub metadata: BTreeMap<String, String>,
331}
332
333impl SipAuthContext {
334 pub fn new() -> Self {
336 Self::default()
337 }
338
339 pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
341 self.peer = Some(peer.into());
342 self
343 }
344
345 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
347 self.metadata.insert(key.into(), value.into());
348 self
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum AuditFailurePolicy {
355 FailOpen,
357 FailClosed,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362enum AuthAttemptScheme {
363 Digest,
364 Bearer,
365 Basic,
366 Aka,
367 Unknown,
368 Missing,
369}
370
371impl AuthAttemptScheme {
372 fn audit_scheme(self) -> AuthAuditScheme {
373 match self {
374 Self::Digest => AuthAuditScheme::Digest,
375 Self::Bearer => AuthAuditScheme::Bearer,
376 Self::Basic => AuthAuditScheme::Basic,
377 Self::Aka => AuthAuditScheme::Aka,
378 Self::Unknown | Self::Missing => AuthAuditScheme::Other("sip".to_string()),
379 }
380 }
381
382 fn rate_limit_kind(self) -> AuthRateLimitKind {
383 match self {
384 Self::Digest => AuthRateLimitKind::Digest,
385 Self::Bearer => AuthRateLimitKind::BearerToken,
386 Self::Basic => AuthRateLimitKind::BasicPassword,
387 Self::Aka => AuthRateLimitKind::SipRequest,
388 Self::Unknown | Self::Missing => AuthRateLimitKind::SipRequest,
389 }
390 }
391}
392
393#[derive(Debug, Clone)]
399pub struct SipAuthPolicy {
400 enabled_schemes: Option<Vec<SipAuthScheme>>,
401 minimum_digest_algorithm: Option<DigestAlgorithm>,
402 allow_basic_over_cleartext: bool,
403 allow_bearer_over_cleartext: bool,
404 require_digest_replay_store: bool,
405 audit_failure_policy: AuditFailurePolicy,
406}
407
408impl SipAuthPolicy {
409 pub fn new() -> Self {
415 Self::default()
416 }
417
418 pub fn allow_only(mut self, schemes: impl IntoIterator<Item = SipAuthScheme>) -> Self {
420 self.enabled_schemes = Some(schemes.into_iter().collect());
421 self
422 }
423
424 pub fn with_minimum_digest_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
429 self.minimum_digest_algorithm = Some(algorithm);
430 self
431 }
432
433 pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
435 self.allow_basic_over_cleartext = allow;
436 self
437 }
438
439 pub fn allow_bearer_over_cleartext(mut self, allow: bool) -> Self {
441 self.allow_bearer_over_cleartext = allow;
442 self
443 }
444
445 pub fn require_digest_replay_store(mut self, require: bool) -> Self {
447 self.require_digest_replay_store = require;
448 self
449 }
450
451 pub fn with_audit_failure_policy(mut self, policy: AuditFailurePolicy) -> Self {
453 self.audit_failure_policy = policy;
454 self
455 }
456
457 fn scheme_allowed(&self, scheme: SipAuthScheme) -> bool {
458 self.enabled_schemes
459 .as_ref()
460 .is_none_or(|schemes| schemes.iter().any(|candidate| *candidate == scheme))
461 }
462
463 fn digest_algorithm_allowed(&self, algorithm: DigestAlgorithm) -> bool {
464 self.minimum_digest_algorithm.is_none_or(|minimum| {
465 digest_algorithm_strength(algorithm) >= digest_algorithm_strength(minimum)
466 })
467 }
468}
469
470impl Default for SipAuthPolicy {
471 fn default() -> Self {
472 Self {
473 enabled_schemes: None,
474 minimum_digest_algorithm: None,
475 allow_basic_over_cleartext: false,
476 allow_bearer_over_cleartext: false,
477 require_digest_replay_store: false,
478 audit_failure_policy: AuditFailurePolicy::FailOpen,
479 }
480 }
481}
482
483#[derive(Debug, Clone, Default, PartialEq, Eq)]
489pub struct SipTransportSecurityContext {
490 pub transport: Option<String>,
492 pub local_addr: Option<String>,
494 pub remote_addr: Option<String>,
496 pub secure: bool,
499}
500
501impl SipTransportSecurityContext {
502 pub fn unknown() -> Self {
505 Self::default()
506 }
507
508 pub fn from_is_tls(is_tls: bool) -> Self {
510 if is_tls {
511 Self::secure("TLS")
512 } else {
513 Self::unknown()
514 }
515 }
516
517 pub fn from_transport_name(transport: impl Into<String>) -> Self {
519 let transport = transport.into();
520 let secure = transport_name_is_secure(&transport);
521 Self {
522 transport: Some(transport),
523 local_addr: None,
524 remote_addr: None,
525 secure,
526 }
527 }
528
529 pub fn from_transport_context(
531 context: &rvoip_infra_common::events::cross_crate::SipTransportContext,
532 ) -> Self {
533 Self {
534 transport: Some(context.transport.clone()),
535 local_addr: Some(context.local_addr.clone()),
536 remote_addr: Some(context.remote_addr.clone()),
537 secure: context.secure,
538 }
539 }
540
541 pub fn from_request_uri_hint(request_uri: &str) -> Self {
546 if request_uri.to_ascii_lowercase().starts_with("sips:") {
547 Self::secure("SIPS-URI")
548 } else {
549 Self::unknown()
550 }
551 }
552
553 pub fn from_request_uri_transport_hint(request_uri: &str) -> Self {
559 let Ok(uri) = rvoip_sip_core::Uri::from_str(request_uri) else {
560 return Self::from_request_uri_hint(request_uri);
561 };
562 let transport = rvoip_sip_transport::resolver::select_transport_for_uri(&uri);
563 Self::from_transport_name(transport.to_string())
564 }
565
566 pub fn secure(transport: impl Into<String>) -> Self {
568 Self {
569 transport: Some(transport.into()),
570 local_addr: None,
571 remote_addr: None,
572 secure: true,
573 }
574 }
575
576 pub fn with_addrs(
578 mut self,
579 local_addr: impl Into<String>,
580 remote_addr: impl Into<String>,
581 ) -> Self {
582 self.local_addr = Some(local_addr.into());
583 self.remote_addr = Some(remote_addr.into());
584 self
585 }
586
587 pub fn is_secure(&self) -> bool {
589 self.secure
590 }
591}
592
593fn transport_name_is_secure(transport: &str) -> bool {
594 matches!(
595 transport.trim().to_ascii_uppercase().as_str(),
596 "TLS" | "WSS" | "SIPS" | "SIPS-URI"
597 )
598}
599
600#[non_exhaustive]
607#[derive(Debug, Clone)]
608pub enum SipClientAuth {
609 Digest(Credentials),
611 BearerToken(String),
614 BearerTokenCleartextAllowed(String),
620 Basic {
622 username: String,
624 password: String,
626 allow_cleartext: bool,
628 },
629 Aka(AkaClientConfig),
631 Composite(Vec<SipClientAuth>),
635}
636
637impl SipClientAuth {
638 pub fn digest(username: impl Into<String>, password: impl Into<String>) -> Self {
640 Self::Digest(Credentials::new(username, password))
641 }
642
643 pub fn bearer_token(token: impl Into<String>) -> Self {
648 Self::BearerToken(token.into())
649 }
650
651 pub fn allow_bearer_over_cleartext(self, allow: bool) -> Self {
657 match self {
658 Self::BearerToken(token) if allow => Self::BearerTokenCleartextAllowed(token),
659 Self::BearerTokenCleartextAllowed(token) if !allow => Self::BearerToken(token),
660 Self::Composite(auths) => Self::Composite(
661 auths
662 .into_iter()
663 .map(|auth| auth.allow_bearer_over_cleartext(allow))
664 .collect(),
665 ),
666 _ => self,
667 }
668 }
669
670 pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
677 Self::Basic {
678 username: username.into(),
679 password: password.into(),
680 allow_cleartext: false,
681 }
682 }
683
684 pub fn aka(config: AkaClientConfig) -> Self {
687 Self::Aka(config)
688 }
689
690 pub fn any(auth: impl IntoIterator<Item = SipClientAuth>) -> Self {
696 Self::Composite(auth.into_iter().collect())
697 }
698
699 pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
705 if let Self::Basic {
706 allow_cleartext, ..
707 } = &mut self
708 {
709 *allow_cleartext = allow;
710 }
711 self
712 }
713
714 pub fn authorization_for_challenge(
717 &self,
718 challenge_header: &str,
719 method: &str,
720 request_uri: &str,
721 nonce_count: u32,
722 body: Option<&[u8]>,
723 is_tls: bool,
724 ) -> Result<ClientAuthHeader> {
725 self.authorization_for_challenge_with_transport_context(
726 challenge_header,
727 method,
728 request_uri,
729 nonce_count,
730 body,
731 &SipTransportSecurityContext::from_is_tls(is_tls),
732 )
733 }
734
735 pub fn authorization_for_challenge_with_transport_context(
738 &self,
739 challenge_header: &str,
740 method: &str,
741 request_uri: &str,
742 nonce_count: u32,
743 body: Option<&[u8]>,
744 transport: &SipTransportSecurityContext,
745 ) -> Result<ClientAuthHeader> {
746 match self {
747 SipClientAuth::Digest(credentials) => {
748 let challenge = extract_digest_challenge(challenge_header).ok_or_else(|| {
749 SessionError::AuthError(
750 "Digest credentials cannot answer a non-Digest challenge".to_string(),
751 )
752 })?;
753 let challenge = rvoip_auth_core::DigestAuthenticator::parse_challenge(&challenge)?;
754 let computed = rvoip_auth_core::DigestClient::compute_response_with_state(
755 &credentials.username,
756 &credentials.password,
757 &challenge,
758 method,
759 request_uri,
760 nonce_count,
761 body,
762 )?;
763 let value = rvoip_auth_core::DigestClient::format_authorization_with_state(
764 &credentials.username,
765 &challenge,
766 request_uri,
767 &computed,
768 );
769 Ok(ClientAuthHeader {
770 value,
771 scheme: SipAuthScheme::Digest,
772 digest_challenge: Some(challenge),
773 stale: parse_digest_stale(challenge_header),
774 })
775 }
776 SipClientAuth::BearerToken(token) => {
777 if !contains_auth_scheme(challenge_header, "Bearer") {
778 return Err(SessionError::AuthError(
779 "Bearer token cannot answer a non-Bearer challenge".to_string(),
780 ));
781 }
782 if !transport.is_secure() {
783 return Err(SessionError::AuthError(
784 "Bearer authentication over cleartext SIP is disabled".to_string(),
785 ));
786 }
787 if token.is_empty() {
788 return Err(SessionError::AuthError(
789 "Bearer token cannot be empty".to_string(),
790 ));
791 }
792 Ok(ClientAuthHeader {
793 value: format!("Bearer {token}"),
794 scheme: SipAuthScheme::Bearer,
795 digest_challenge: None,
796 stale: false,
797 })
798 }
799 SipClientAuth::BearerTokenCleartextAllowed(token) => {
800 if !contains_auth_scheme(challenge_header, "Bearer") {
801 return Err(SessionError::AuthError(
802 "Bearer token cannot answer a non-Bearer challenge".to_string(),
803 ));
804 }
805 if token.is_empty() {
806 return Err(SessionError::AuthError(
807 "Bearer token cannot be empty".to_string(),
808 ));
809 }
810 Ok(ClientAuthHeader {
811 value: format!("Bearer {token}"),
812 scheme: SipAuthScheme::Bearer,
813 digest_challenge: None,
814 stale: false,
815 })
816 }
817 SipClientAuth::Basic {
818 username,
819 password,
820 allow_cleartext,
821 } => {
822 if !contains_auth_scheme(challenge_header, "Basic") {
823 return Err(SessionError::AuthError(
824 "Basic credentials cannot answer a non-Basic challenge".to_string(),
825 ));
826 }
827 if !transport.is_secure() && !*allow_cleartext {
828 return Err(SessionError::AuthError(
829 "Basic authentication over cleartext SIP is disabled".to_string(),
830 ));
831 }
832 let token = BASE64_STANDARD.encode(format!("{username}:{password}"));
833 Ok(ClientAuthHeader {
834 value: format!("Basic {token}"),
835 scheme: SipAuthScheme::Basic,
836 digest_challenge: None,
837 stale: false,
838 })
839 }
840 SipClientAuth::Aka(config) => {
841 if !contains_aka_challenge(challenge_header) {
842 return Err(SessionError::AuthError(
843 "AKA credentials cannot answer a non-AKA challenge".to_string(),
844 ));
845 }
846 let response =
847 config.respond(challenge_header, method, request_uri, nonce_count)?;
848 Ok(ClientAuthHeader {
849 value: response,
850 scheme: SipAuthScheme::Aka,
851 digest_challenge: None,
852 stale: false,
853 })
854 }
855 SipClientAuth::Composite(auths) => select_composite_client_auth(
856 auths,
857 challenge_header,
858 method,
859 request_uri,
860 nonce_count,
861 body,
862 transport,
863 ),
864 }
865 }
866}
867
868impl From<Credentials> for SipClientAuth {
869 fn from(value: Credentials) -> Self {
870 Self::Digest(value)
871 }
872}
873
874#[derive(Debug, Clone, PartialEq, Eq)]
876pub struct ClientAuthHeader {
877 pub value: String,
879 pub scheme: SipAuthScheme,
881 pub digest_challenge: Option<DigestChallenge>,
883 pub stale: bool,
885}
886
887#[derive(Clone)]
893pub struct AkaClientConfig {
894 provider: Arc<dyn AkaClientProvider>,
895}
896
897impl AkaClientConfig {
898 pub fn new(provider: Arc<dyn AkaClientProvider>) -> Self {
900 Self { provider }
901 }
902
903 fn respond(
904 &self,
905 challenge_header: &str,
906 method: &str,
907 request_uri: &str,
908 nonce_count: u32,
909 ) -> Result<String> {
910 self.provider
911 .authorization(challenge_header, method, request_uri, nonce_count)
912 }
913}
914
915impl std::fmt::Debug for AkaClientConfig {
916 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917 f.debug_struct("AkaClientConfig")
918 .field("provider", &"<AkaClientProvider>")
919 .finish()
920 }
921}
922
923impl PartialEq for AkaClientConfig {
924 fn eq(&self, other: &Self) -> bool {
925 Arc::ptr_eq(&self.provider, &other.provider)
926 }
927}
928
929impl Eq for AkaClientConfig {}
930
931pub trait AkaClientProvider: Send + Sync {
937 fn authorization(
939 &self,
940 challenge_header: &str,
941 method: &str,
942 request_uri: &str,
943 nonce_count: u32,
944 ) -> Result<String>;
945}
946
947#[async_trait]
953pub trait AkaVectorProvider: Send + Sync {
954 async fn validate(
956 &self,
957 authorization: &str,
958 method: &str,
959 request_uri: &str,
960 body: Option<&[u8]>,
961 ) -> Result<Option<AuthIdentity>>;
962
963 fn challenge(&self, source: SipAuthSource) -> SipAuthChallenge;
966}
967
968#[derive(Clone)]
982pub struct SipAuthService {
983 policy: SipAuthPolicy,
984 digest: Option<SipDigestAuthService>,
985 digest_provider: Option<DigestProviderAuthStore>,
986 bearer: Option<Arc<dyn BearerValidator>>,
987 bearer_realm: Option<String>,
988 bearer_scope: Option<String>,
989 basic: Option<BasicAuthStore>,
990 aka: Option<Arc<dyn AkaVectorProvider>>,
991 allow_bearer_over_cleartext: bool,
992 allow_basic_over_cleartext: bool,
993 audit_sink: Option<Arc<dyn AuthAuditSink>>,
994 audit_failure_policy: AuditFailurePolicy,
995 rate_limiter: Option<Arc<dyn AuthRateLimiter>>,
996 digest_replay_store: Option<Arc<dyn DigestReplayStore>>,
997}
998
999impl SipAuthService {
1000 pub fn new() -> Self {
1003 Self {
1004 policy: SipAuthPolicy::default(),
1005 digest: None,
1006 digest_provider: None,
1007 bearer: None,
1008 bearer_realm: None,
1009 bearer_scope: None,
1010 basic: None,
1011 aka: None,
1012 allow_bearer_over_cleartext: false,
1013 allow_basic_over_cleartext: false,
1014 audit_sink: None,
1015 audit_failure_policy: AuditFailurePolicy::FailOpen,
1016 rate_limiter: None,
1017 digest_replay_store: None,
1018 }
1019 }
1020
1021 pub fn digest(realm: impl Into<String>) -> Self {
1023 Self::new().with_digest_service(SipDigestAuthService::new(realm))
1024 }
1025
1026 pub fn with_policy(mut self, policy: SipAuthPolicy) -> Self {
1028 self.allow_basic_over_cleartext = policy.allow_basic_over_cleartext;
1029 self.allow_bearer_over_cleartext = policy.allow_bearer_over_cleartext;
1030 self.audit_failure_policy = policy.audit_failure_policy;
1031 self.policy = policy;
1032 self
1033 }
1034
1035 pub fn with_digest_service(mut self, service: SipDigestAuthService) -> Self {
1037 self.digest = Some(service);
1038 self
1039 }
1040
1041 pub fn with_digest_provider(
1047 mut self,
1048 realm: impl Into<String>,
1049 provider: Arc<dyn DigestSecretProvider>,
1050 ) -> Self {
1051 let mut digest = DigestProviderAuthStore::new(realm, provider);
1052 if let Some(replay_store) = self.digest_replay_store.clone() {
1053 digest = digest.with_replay_store(replay_store);
1054 }
1055 self.digest_provider = Some(digest);
1056 self
1057 }
1058
1059 pub fn with_digest_provider_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
1065 if let Some(digest) = self.digest_provider.take() {
1066 self.digest_provider = Some(digest.with_algorithm(algorithm));
1067 }
1068 self
1069 }
1070
1071 pub fn with_digest_replay_store(mut self, replay_store: Arc<dyn DigestReplayStore>) -> Self {
1078 if let Some(digest) = self.digest_provider.take() {
1079 self.digest_provider = Some(digest.with_replay_store(replay_store.clone()));
1080 }
1081 self.digest_replay_store = Some(replay_store);
1082 self
1083 }
1084
1085 pub fn with_audit_sink(mut self, sink: Arc<dyn AuthAuditSink>) -> Self {
1087 self.audit_sink = Some(sink);
1088 self
1089 }
1090
1091 pub fn with_audit_failure_policy(mut self, policy: AuditFailurePolicy) -> Self {
1095 self.audit_failure_policy = policy;
1096 self
1097 }
1098
1099 pub fn with_rate_limiter(mut self, rate_limiter: Arc<dyn AuthRateLimiter>) -> Self {
1104 self.rate_limiter = Some(rate_limiter);
1105 self
1106 }
1107
1108 pub fn with_bearer_validator(
1115 mut self,
1116 realm: impl Into<String>,
1117 validator: Arc<dyn BearerValidator>,
1118 ) -> Self {
1119 self.bearer = Some(validator);
1120 self.bearer_realm = Some(realm.into());
1121 self
1122 }
1123
1124 pub fn with_bearer_scope(mut self, scope: impl Into<String>) -> Self {
1126 self.bearer_scope = Some(scope.into());
1127 self
1128 }
1129
1130 pub fn with_basic_realm(mut self, realm: impl Into<String>) -> Self {
1135 self.basic = Some(BasicAuthStore {
1136 realm: realm.into(),
1137 users: Arc::new(RwLock::new(HashMap::new())),
1138 verifier: None,
1139 });
1140 self
1141 }
1142
1143 pub fn with_basic_verifier(
1148 mut self,
1149 realm: impl Into<String>,
1150 verifier: Arc<dyn PasswordVerifier>,
1151 ) -> Self {
1152 self.basic = Some(BasicAuthStore {
1153 realm: realm.into(),
1154 users: Arc::new(RwLock::new(HashMap::new())),
1155 verifier: Some(verifier),
1156 });
1157 self
1158 }
1159
1160 pub fn add_basic_user(&mut self, username: impl Into<String>, password: impl Into<String>) {
1163 if self.basic.is_none() {
1164 self.basic = Some(BasicAuthStore {
1165 realm: "sip".to_string(),
1166 users: Arc::new(RwLock::new(HashMap::new())),
1167 verifier: None,
1168 });
1169 }
1170 if let Some(basic) = &self.basic {
1171 let mut users = basic
1172 .users
1173 .write()
1174 .unwrap_or_else(|poisoned| poisoned.into_inner());
1175 users.insert(username.into(), password.into());
1176 }
1177 }
1178
1179 pub fn with_basic_user(
1181 mut self,
1182 username: impl Into<String>,
1183 password: impl Into<String>,
1184 ) -> Self {
1185 self.add_basic_user(username, password);
1186 self
1187 }
1188
1189 pub fn allow_bearer_over_cleartext(mut self, allow: bool) -> Self {
1195 self.allow_bearer_over_cleartext = allow;
1196 self
1197 }
1198
1199 pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
1201 self.allow_basic_over_cleartext = allow;
1202 self
1203 }
1204
1205 pub fn with_aka_provider(mut self, provider: Arc<dyn AkaVectorProvider>) -> Self {
1207 self.aka = Some(provider);
1208 self
1209 }
1210
1211 pub fn add_digest_user(&mut self, username: impl Into<String>, password: impl Into<String>) {
1214 if self.digest.is_none() {
1215 self.digest = Some(SipDigestAuthService::new("sip"));
1216 }
1217 if let Some(digest) = &self.digest {
1218 digest.add_user(username, password);
1219 }
1220 }
1221
1222 pub fn with_digest_user(
1224 mut self,
1225 username: impl Into<String>,
1226 password: impl Into<String>,
1227 ) -> Self {
1228 self.add_digest_user(username, password);
1229 self
1230 }
1231
1232 pub async fn authenticate_authorization(
1235 &self,
1236 authorization: Option<&str>,
1237 method: &str,
1238 request_uri: &str,
1239 body: Option<&[u8]>,
1240 source: SipAuthSource,
1241 is_tls: bool,
1242 ) -> Result<SipAuthDecision> {
1243 self.authenticate_authorization_with_transport_context(
1244 authorization,
1245 method,
1246 request_uri,
1247 body,
1248 source,
1249 &SipTransportSecurityContext::from_is_tls(is_tls),
1250 )
1251 .await
1252 }
1253
1254 pub async fn authenticate_authorization_with_transport_context(
1257 &self,
1258 authorization: Option<&str>,
1259 method: &str,
1260 request_uri: &str,
1261 body: Option<&[u8]>,
1262 source: SipAuthSource,
1263 transport: &SipTransportSecurityContext,
1264 ) -> Result<SipAuthDecision> {
1265 self.authenticate_authorization_with_context_and_transport(
1266 authorization,
1267 method,
1268 request_uri,
1269 body,
1270 source,
1271 transport,
1272 &SipAuthContext::default(),
1273 )
1274 .await
1275 }
1276
1277 pub async fn authenticate_authorization_with_context(
1280 &self,
1281 authorization: Option<&str>,
1282 method: &str,
1283 request_uri: &str,
1284 body: Option<&[u8]>,
1285 source: SipAuthSource,
1286 is_tls: bool,
1287 context: &SipAuthContext,
1288 ) -> Result<SipAuthDecision> {
1289 self.authenticate_authorization_with_context_and_transport(
1290 authorization,
1291 method,
1292 request_uri,
1293 body,
1294 source,
1295 &SipTransportSecurityContext::from_is_tls(is_tls),
1296 context,
1297 )
1298 .await
1299 }
1300
1301 pub async fn authenticate_authorization_with_context_and_transport(
1304 &self,
1305 authorization: Option<&str>,
1306 method: &str,
1307 request_uri: &str,
1308 body: Option<&[u8]>,
1309 source: SipAuthSource,
1310 transport: &SipTransportSecurityContext,
1311 context: &SipAuthContext,
1312 ) -> Result<SipAuthDecision> {
1313 let attempt = auth_attempt_scheme(authorization);
1314 let rate_key = self.rate_limit_key(attempt, authorization, method, context);
1315
1316 let verdict = match self.check_rate_limit(&rate_key).await {
1317 Ok(verdict) => verdict,
1318 Err(err) => {
1319 let outcome = AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1320 self.audit_attempt(attempt, outcome, authorization, source, method, context)
1321 .await?;
1322 return Err(SessionError::AuthError(format!(
1323 "auth rate limiter unavailable: {err}"
1324 )));
1325 }
1326 };
1327
1328 match verdict {
1329 AuthRateLimitVerdict::Allowed => {}
1330 AuthRateLimitVerdict::Denied { .. } => {
1331 let outcome = AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected);
1332 self.record_rate_result_or_audit_unavailable(
1333 &rate_key,
1334 &outcome,
1335 attempt,
1336 authorization,
1337 source,
1338 method,
1339 context,
1340 )
1341 .await?;
1342 self.audit_attempt(attempt, outcome, authorization, source, method, context)
1343 .await?;
1344 return self.rejected_async(source).await;
1345 }
1346 }
1347
1348 let auth_result = match authorization {
1349 None => Ok((
1350 self.rejected_async(source).await?,
1351 Some(AuthFailureReason::MissingCredential),
1352 )),
1353 Some(authorization) => {
1354 let trimmed = authorization.trim();
1355 match attempt {
1356 AuthAttemptScheme::Digest => {
1357 if !self.policy.scheme_allowed(SipAuthScheme::Digest) {
1358 Ok((
1359 self.rejected_async(source).await?,
1360 Some(AuthFailureReason::PolicyRejected),
1361 ))
1362 } else if self.policy.require_digest_replay_store
1363 && self.digest_replay_store.is_none()
1364 {
1365 Ok((
1366 self.rejected_async(source).await?,
1367 Some(AuthFailureReason::PolicyRejected),
1368 ))
1369 } else if let Ok(response) =
1370 DigestAuthenticator::parse_authorization(trimmed)
1371 {
1372 if !self.policy.digest_algorithm_allowed(response.algorithm) {
1373 Ok((
1374 self.rejected_async(source).await?,
1375 Some(AuthFailureReason::PolicyRejected),
1376 ))
1377 } else {
1378 self.authenticate_digest_with_reason(
1379 trimmed,
1380 method,
1381 request_uri,
1382 body,
1383 source,
1384 )
1385 .await
1386 }
1387 } else {
1388 self.authenticate_digest_with_reason(
1389 trimmed,
1390 method,
1391 request_uri,
1392 body,
1393 source,
1394 )
1395 .await
1396 }
1397 }
1398 AuthAttemptScheme::Bearer => {
1399 if !self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1400 Ok((
1401 self.rejected_async(source).await?,
1402 Some(AuthFailureReason::PolicyRejected),
1403 ))
1404 } else if !transport.is_secure() && !self.allow_bearer_over_cleartext {
1405 Ok((
1406 self.rejected_async(source).await?,
1407 Some(AuthFailureReason::PolicyRejected),
1408 ))
1409 } else {
1410 self.authenticate_bearer_with_reason(trimmed, source).await
1411 }
1412 }
1413 AuthAttemptScheme::Basic => {
1414 if !self.policy.scheme_allowed(SipAuthScheme::Basic) {
1415 Ok((
1416 self.rejected_async(source).await?,
1417 Some(AuthFailureReason::PolicyRejected),
1418 ))
1419 } else if !transport.is_secure() && !self.allow_basic_over_cleartext {
1420 Ok((
1421 self.rejected_async(source).await?,
1422 Some(AuthFailureReason::PolicyRejected),
1423 ))
1424 } else {
1425 self.authenticate_basic_with_reason(trimmed, source, transport)
1426 .await
1427 }
1428 }
1429 AuthAttemptScheme::Aka => {
1430 if !self.policy.scheme_allowed(SipAuthScheme::Aka) {
1431 Ok((
1432 self.rejected_async(source).await?,
1433 Some(AuthFailureReason::PolicyRejected),
1434 ))
1435 } else {
1436 self.authenticate_aka_with_reason(
1437 trimmed,
1438 method,
1439 request_uri,
1440 body,
1441 source,
1442 )
1443 .await
1444 }
1445 }
1446 AuthAttemptScheme::Unknown | AuthAttemptScheme::Missing => Ok((
1447 self.rejected_async(source).await?,
1448 Some(AuthFailureReason::UnsupportedScheme),
1449 )),
1450 }
1451 }
1452 };
1453
1454 let (result, failure_reason) = match auth_result {
1455 Ok(result) => result,
1456 Err(err) => {
1457 let outcome = AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1458 self.record_rate_result_or_audit_unavailable(
1459 &rate_key,
1460 &outcome,
1461 attempt,
1462 authorization,
1463 source,
1464 method,
1465 context,
1466 )
1467 .await?;
1468 self.audit_attempt(attempt, outcome, authorization, source, method, context)
1469 .await?;
1470 return Err(err);
1471 }
1472 };
1473
1474 let outcome = auth_outcome_for_decision(&result, failure_reason);
1475 self.record_rate_result_or_audit_unavailable(
1476 &rate_key,
1477 &outcome,
1478 attempt,
1479 authorization,
1480 source,
1481 method,
1482 context,
1483 )
1484 .await?;
1485 self.audit_attempt(attempt, outcome, authorization, source, method, context)
1486 .await?;
1487 Ok(result)
1488 }
1489
1490 fn rate_limit_key(
1491 &self,
1492 attempt: AuthAttemptScheme,
1493 authorization: Option<&str>,
1494 method: &str,
1495 context: &SipAuthContext,
1496 ) -> AuthRateLimitKey {
1497 let (subject, realm) = subject_realm_from_authorization(authorization);
1498 let kind = if method.eq_ignore_ascii_case("REGISTER") {
1499 AuthRateLimitKind::SipRegister
1500 } else {
1501 attempt.rate_limit_kind()
1502 };
1503 let mut key = AuthRateLimitKey::new(kind);
1504 if let Some(subject) = subject {
1505 key = key.with_subject(subject);
1506 }
1507 if let Some(realm) = realm {
1508 key = key.with_realm(realm);
1509 }
1510 if let Some(peer) = context.peer.as_ref() {
1511 key = key.with_peer(peer.clone());
1512 }
1513 key
1514 }
1515
1516 async fn check_rate_limit(
1517 &self,
1518 key: &AuthRateLimitKey,
1519 ) -> std::result::Result<AuthRateLimitVerdict, CredentialAuthError> {
1520 let Some(rate_limiter) = &self.rate_limiter else {
1521 return Ok(AuthRateLimitVerdict::Allowed);
1522 };
1523 rate_limiter.check_auth_attempt(key).await
1524 }
1525
1526 async fn record_rate_result_or_audit_unavailable(
1527 &self,
1528 key: &AuthRateLimitKey,
1529 outcome: &AuthAuditOutcome,
1530 attempt: AuthAttemptScheme,
1531 authorization: Option<&str>,
1532 source: SipAuthSource,
1533 method: &str,
1534 context: &SipAuthContext,
1535 ) -> Result<()> {
1536 let Some(rate_limiter) = &self.rate_limiter else {
1537 return Ok(());
1538 };
1539 if let Err(err) = rate_limiter.record_auth_result(key, outcome).await {
1540 let provider_outcome =
1541 AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1542 self.audit_attempt(
1543 attempt,
1544 provider_outcome,
1545 authorization,
1546 source,
1547 method,
1548 context,
1549 )
1550 .await?;
1551 return Err(SessionError::AuthError(format!(
1552 "auth rate limiter unavailable: {err}"
1553 )));
1554 }
1555 Ok(())
1556 }
1557
1558 async fn audit_attempt(
1559 &self,
1560 attempt: AuthAttemptScheme,
1561 outcome: AuthAuditOutcome,
1562 authorization: Option<&str>,
1563 source: SipAuthSource,
1564 method: &str,
1565 context: &SipAuthContext,
1566 ) -> Result<()> {
1567 let Some(sink) = &self.audit_sink else {
1568 return Ok(());
1569 };
1570 let (subject, realm) = subject_realm_from_authorization(authorization);
1571 let mut event = AuthAuditEvent::new(attempt.audit_scheme(), outcome);
1572 if let Some(subject) = subject {
1573 event = event.with_subject(subject);
1574 }
1575 if let Some(realm) = realm {
1576 event = event.with_realm(realm);
1577 }
1578 if let Some(peer) = context.peer.as_ref() {
1579 event = event.with_peer(peer.clone());
1580 }
1581 event = event
1582 .with_metadata("method", method.to_ascii_uppercase())
1583 .with_metadata(
1584 "source",
1585 match source {
1586 SipAuthSource::Origin => "origin",
1587 SipAuthSource::Proxy => "proxy",
1588 },
1589 );
1590 for (key, value) in &context.metadata {
1591 event = event.with_metadata(key.clone(), value.clone());
1592 }
1593
1594 match sink.record_auth_event(event).await {
1595 Ok(()) => Ok(()),
1596 Err(err) if self.audit_failure_policy == AuditFailurePolicy::FailOpen => {
1597 let _ = err;
1598 Ok(())
1599 }
1600 Err(err) => Err(SessionError::AuthError(format!(
1601 "auth audit sink unavailable: {err}"
1602 ))),
1603 }
1604 }
1605
1606 fn challenges_with_digest_value(
1607 &self,
1608 source: SipAuthSource,
1609 digest_value: String,
1610 ) -> Vec<SipAuthChallenge> {
1611 let mut challenges = Vec::new();
1612 if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1613 if let Some(aka) = &self.aka {
1614 challenges.push(aka.challenge(source));
1615 }
1616 }
1617 if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1618 if let Some(bearer) = &self.bearer_realm {
1619 challenges.push(SipAuthChallenge {
1620 scheme: SipAuthScheme::Bearer,
1621 value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1622 source,
1623 });
1624 }
1625 }
1626 if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1627 challenges.push(SipAuthChallenge {
1628 scheme: SipAuthScheme::Digest,
1629 value: digest_value,
1630 source,
1631 });
1632 }
1633 if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1634 if let Some(basic) = &self.basic {
1635 challenges.push(SipAuthChallenge {
1636 scheme: SipAuthScheme::Basic,
1637 value: format!("Basic realm=\"{}\"", basic.realm),
1638 source,
1639 });
1640 }
1641 }
1642 challenges
1643 }
1644
1645 pub fn challenges(&self, source: SipAuthSource) -> Vec<SipAuthChallenge> {
1650 let mut challenges = Vec::new();
1651 if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1652 if let Some(aka) = &self.aka {
1653 challenges.push(aka.challenge(source));
1654 }
1655 }
1656 if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1657 if let Some(bearer) = &self.bearer_realm {
1658 challenges.push(SipAuthChallenge {
1659 scheme: SipAuthScheme::Bearer,
1660 value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1661 source,
1662 });
1663 }
1664 }
1665 if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1666 if let Some(digest) = &self.digest_provider {
1667 let challenge = digest.challenge();
1668 if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1669 challenges.push(SipAuthChallenge {
1670 scheme: SipAuthScheme::Digest,
1671 value: digest.www_authenticate(&challenge),
1672 source,
1673 });
1674 }
1675 } else if let Some(digest) = &self.digest {
1676 let challenge = digest.challenge();
1677 if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1678 challenges.push(SipAuthChallenge {
1679 scheme: SipAuthScheme::Digest,
1680 value: digest.www_authenticate(&challenge),
1681 source,
1682 });
1683 }
1684 }
1685 }
1686 if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1687 if let Some(basic) = &self.basic {
1688 challenges.push(SipAuthChallenge {
1689 scheme: SipAuthScheme::Basic,
1690 value: format!("Basic realm=\"{}\"", basic.realm),
1691 source,
1692 });
1693 }
1694 }
1695 challenges
1696 }
1697
1698 pub async fn challenges_async(&self, source: SipAuthSource) -> Result<Vec<SipAuthChallenge>> {
1701 let mut challenges = Vec::new();
1702 if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1703 if let Some(aka) = &self.aka {
1704 challenges.push(aka.challenge(source));
1705 }
1706 }
1707 if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1708 if let Some(bearer) = &self.bearer_realm {
1709 challenges.push(SipAuthChallenge {
1710 scheme: SipAuthScheme::Bearer,
1711 value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1712 source,
1713 });
1714 }
1715 }
1716 if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1717 if let Some(digest) = &self.digest_provider {
1718 let challenge = digest.challenge_async().await?;
1719 if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1720 challenges.push(SipAuthChallenge {
1721 scheme: SipAuthScheme::Digest,
1722 value: digest.www_authenticate(&challenge),
1723 source,
1724 });
1725 }
1726 } else if let Some(digest) = &self.digest {
1727 let challenge = if let Some(replay_store) = &self.digest_replay_store {
1728 digest
1729 .challenge_with_replay_store(replay_store.clone())
1730 .await?
1731 } else {
1732 digest.challenge()
1733 };
1734 if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1735 challenges.push(SipAuthChallenge {
1736 scheme: SipAuthScheme::Digest,
1737 value: digest.www_authenticate(&challenge),
1738 source,
1739 });
1740 }
1741 }
1742 }
1743 if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1744 if let Some(basic) = &self.basic {
1745 challenges.push(SipAuthChallenge {
1746 scheme: SipAuthScheme::Basic,
1747 value: format!("Basic realm=\"{}\"", basic.realm),
1748 source,
1749 });
1750 }
1751 }
1752 Ok(challenges)
1753 }
1754
1755 async fn rejected_async(&self, source: SipAuthSource) -> Result<SipAuthDecision> {
1756 Ok(SipAuthDecision::Rejected {
1757 challenges: self.challenges_async(source).await?,
1758 })
1759 }
1760
1761 async fn authenticate_digest_with_reason(
1762 &self,
1763 authorization: &str,
1764 method: &str,
1765 request_uri: &str,
1766 body: Option<&[u8]>,
1767 source: SipAuthSource,
1768 ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1769 if let Some(digest) = &self.digest_provider {
1770 return match digest
1771 .validate_authorization_detailed(authorization, method, request_uri, body)
1772 .await?
1773 {
1774 (AuthDecision::Authorized { username, realm }, failure_reason) => Ok((
1775 SipAuthDecision::Authorized(AuthIdentity {
1776 scheme: SipAuthScheme::Digest,
1777 username: Some(username),
1778 subject: None,
1779 realm: Some(realm),
1780 scopes: Vec::new(),
1781 source,
1782 }),
1783 failure_reason,
1784 )),
1785 (
1786 AuthDecision::Rejected {
1787 www_authenticate, ..
1788 },
1789 failure_reason,
1790 ) => Ok((
1791 SipAuthDecision::Rejected {
1792 challenges: self.challenges_with_digest_value(source, www_authenticate),
1793 },
1794 failure_reason,
1795 )),
1796 };
1797 }
1798 let Some(digest) = &self.digest else {
1799 return Ok((
1800 self.rejected_async(source).await?,
1801 Some(AuthFailureReason::UnsupportedScheme),
1802 ));
1803 };
1804 let digest_decision = if let Some(replay_store) = &self.digest_replay_store {
1805 digest
1806 .validate_authorization_with_replay_store(
1807 authorization,
1808 method,
1809 request_uri,
1810 body,
1811 replay_store.clone(),
1812 )
1813 .await?
1814 } else {
1815 digest.validate_authorization(authorization, method, request_uri, body)?
1816 };
1817 match digest_decision {
1818 AuthDecision::Authorized { username, realm } => Ok((
1819 SipAuthDecision::Authorized(AuthIdentity {
1820 scheme: SipAuthScheme::Digest,
1821 username: Some(username),
1822 subject: None,
1823 realm: Some(realm),
1824 scopes: Vec::new(),
1825 source,
1826 }),
1827 None,
1828 )),
1829 AuthDecision::Rejected {
1830 www_authenticate, ..
1831 } => {
1832 let reason = if www_authenticate.contains("stale=true") {
1833 AuthFailureReason::StaleNonce
1834 } else {
1835 AuthFailureReason::InvalidCredential
1836 };
1837 Ok((
1838 SipAuthDecision::Rejected {
1839 challenges: self.challenges_with_digest_value(source, www_authenticate),
1840 },
1841 Some(reason),
1842 ))
1843 }
1844 }
1845 }
1846
1847 async fn authenticate_bearer_with_reason(
1848 &self,
1849 authorization: &str,
1850 source: SipAuthSource,
1851 ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1852 let Some(validator) = &self.bearer else {
1853 return Ok((
1854 self.rejected_async(source).await?,
1855 Some(AuthFailureReason::UnsupportedScheme),
1856 ));
1857 };
1858 let token = authorization
1859 .split_once(char::is_whitespace)
1860 .map(|(_, value)| value.trim())
1861 .unwrap_or_default();
1862 match validator.validate(token).await {
1863 Ok(assurance) => Ok((
1864 SipAuthDecision::Authorized(identity_from_bearer_assurance(
1865 assurance,
1866 self.bearer_realm.clone(),
1867 source,
1868 )),
1869 None,
1870 )),
1871 Err(BearerAuthError::Empty) | Err(BearerAuthError::Invalid(_)) => Ok((
1872 self.rejected_async(source).await?,
1873 Some(AuthFailureReason::InvalidCredential),
1874 )),
1875 Err(BearerAuthError::Unavailable(err)) => Err(SessionError::AuthError(format!(
1876 "Bearer validator unavailable: {err}"
1877 ))),
1878 }
1879 }
1880
1881 async fn authenticate_basic_with_reason(
1882 &self,
1883 authorization: &str,
1884 source: SipAuthSource,
1885 transport: &SipTransportSecurityContext,
1886 ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1887 let Some(basic) = &self.basic else {
1888 return Ok((
1889 self.rejected_async(source).await?,
1890 Some(AuthFailureReason::UnsupportedScheme),
1891 ));
1892 };
1893 if !transport.is_secure() && !self.allow_basic_over_cleartext {
1894 return Ok((
1895 self.rejected_async(source).await?,
1896 Some(AuthFailureReason::PolicyRejected),
1897 ));
1898 }
1899 let token = authorization
1900 .split_once(char::is_whitespace)
1901 .map(|(_, value)| value.trim())
1902 .unwrap_or_default();
1903 let decoded = match BASE64_STANDARD.decode(token) {
1904 Ok(decoded) => decoded,
1905 Err(_) => {
1906 return Ok((
1907 self.rejected_async(source).await?,
1908 Some(AuthFailureReason::MalformedCredential),
1909 ))
1910 }
1911 };
1912 let decoded = match String::from_utf8(decoded) {
1913 Ok(decoded) => decoded,
1914 Err(_) => {
1915 return Ok((
1916 self.rejected_async(source).await?,
1917 Some(AuthFailureReason::MalformedCredential),
1918 ))
1919 }
1920 };
1921 let Some((username, password)) = decoded.split_once(':') else {
1922 return Ok((
1923 self.rejected_async(source).await?,
1924 Some(AuthFailureReason::MalformedCredential),
1925 ));
1926 };
1927 if let Some(verifier) = &basic.verifier {
1928 return match verifier.verify_password(username, password).await {
1929 Ok(assurance) => {
1930 let mut identity = identity_from_bearer_assurance(
1931 assurance,
1932 Some(basic.realm.clone()),
1933 source,
1934 );
1935 identity.scheme = SipAuthScheme::Basic;
1936 identity.username = Some(username.to_string());
1937 Ok((SipAuthDecision::Authorized(identity), None))
1938 }
1939 Err(CredentialAuthError::Invalid) => Ok((
1940 self.rejected_async(source).await?,
1941 Some(AuthFailureReason::InvalidCredential),
1942 )),
1943 Err(CredentialAuthError::PolicyRejected(_)) => Ok((
1944 self.rejected_async(source).await?,
1945 Some(AuthFailureReason::PolicyRejected),
1946 )),
1947 Err(err) => Err(SessionError::AuthError(err.to_string())),
1948 };
1949 }
1950 let valid = {
1951 let users = basic
1952 .users
1953 .read()
1954 .unwrap_or_else(|poisoned| poisoned.into_inner());
1955 users.get(username).is_some_and(|stored| stored == password)
1956 };
1957 if !valid {
1958 return Ok((
1959 self.rejected_async(source).await?,
1960 Some(AuthFailureReason::InvalidCredential),
1961 ));
1962 }
1963 Ok((
1964 SipAuthDecision::Authorized(AuthIdentity {
1965 scheme: SipAuthScheme::Basic,
1966 username: Some(username.to_string()),
1967 subject: None,
1968 realm: Some(basic.realm.clone()),
1969 scopes: Vec::new(),
1970 source,
1971 }),
1972 None,
1973 ))
1974 }
1975
1976 async fn authenticate_aka_with_reason(
1977 &self,
1978 authorization: &str,
1979 method: &str,
1980 request_uri: &str,
1981 body: Option<&[u8]>,
1982 source: SipAuthSource,
1983 ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1984 let Some(aka) = &self.aka else {
1985 return Ok((
1986 self.rejected_async(source).await?,
1987 Some(AuthFailureReason::UnsupportedScheme),
1988 ));
1989 };
1990 match aka
1991 .validate(authorization, method, request_uri, body)
1992 .await?
1993 {
1994 Some(mut identity) => {
1995 identity.scheme = SipAuthScheme::Aka;
1996 identity.source = source;
1997 Ok((SipAuthDecision::Authorized(identity), None))
1998 }
1999 None => Ok((
2000 self.rejected_async(source).await?,
2001 Some(AuthFailureReason::InvalidCredential),
2002 )),
2003 }
2004 }
2005}
2006
2007impl Default for SipAuthService {
2008 fn default() -> Self {
2009 Self::new()
2010 }
2011}
2012
2013#[async_trait]
2019pub trait SipIncomingAuthenticator {
2020 type Decision: Send;
2022
2023 async fn authenticate_incoming(
2025 &self,
2026 authorization: Option<&str>,
2027 method: &str,
2028 request_uri: &str,
2029 body: Option<&[u8]>,
2030 source: SipAuthSource,
2031 is_tls: bool,
2032 ) -> Result<Self::Decision>;
2033
2034 async fn authenticate_incoming_with_transport_context(
2041 &self,
2042 authorization: Option<&str>,
2043 method: &str,
2044 request_uri: &str,
2045 body: Option<&[u8]>,
2046 source: SipAuthSource,
2047 transport: &SipTransportSecurityContext,
2048 ) -> Result<Self::Decision> {
2049 self.authenticate_incoming(
2050 authorization,
2051 method,
2052 request_uri,
2053 body,
2054 source,
2055 transport.is_secure(),
2056 )
2057 .await
2058 }
2059}
2060
2061#[async_trait]
2062impl SipIncomingAuthenticator for SipDigestAuthService {
2063 type Decision = AuthDecision;
2064
2065 async fn authenticate_incoming(
2066 &self,
2067 authorization: Option<&str>,
2068 method: &str,
2069 request_uri: &str,
2070 body: Option<&[u8]>,
2071 _source: SipAuthSource,
2072 _is_tls: bool,
2073 ) -> Result<Self::Decision> {
2074 self.authenticate_authorization(authorization, method, request_uri, body)
2075 }
2076}
2077
2078#[async_trait]
2079impl SipIncomingAuthenticator for SipAuthService {
2080 type Decision = SipAuthDecision;
2081
2082 async fn authenticate_incoming(
2083 &self,
2084 authorization: Option<&str>,
2085 method: &str,
2086 request_uri: &str,
2087 body: Option<&[u8]>,
2088 source: SipAuthSource,
2089 is_tls: bool,
2090 ) -> Result<Self::Decision> {
2091 self.authenticate_authorization(authorization, method, request_uri, body, source, is_tls)
2092 .await
2093 }
2094
2095 async fn authenticate_incoming_with_transport_context(
2096 &self,
2097 authorization: Option<&str>,
2098 method: &str,
2099 request_uri: &str,
2100 body: Option<&[u8]>,
2101 source: SipAuthSource,
2102 transport: &SipTransportSecurityContext,
2103 ) -> Result<Self::Decision> {
2104 self.authenticate_authorization_with_transport_context(
2105 authorization,
2106 method,
2107 request_uri,
2108 body,
2109 source,
2110 transport,
2111 )
2112 .await
2113 }
2114}
2115
2116impl std::fmt::Debug for SipAuthService {
2117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2118 f.debug_struct("SipAuthService")
2119 .field("policy", &self.policy)
2120 .field("digest", &self.digest.is_some())
2121 .field("digest_provider", &self.digest_provider.is_some())
2122 .field("bearer", &self.bearer.is_some())
2123 .field("bearer_realm", &self.bearer_realm)
2124 .field("bearer_scope", &self.bearer_scope)
2125 .field("basic", &self.basic.as_ref().map(|b| &b.realm))
2126 .field("aka", &self.aka.is_some())
2127 .field(
2128 "allow_bearer_over_cleartext",
2129 &self.allow_bearer_over_cleartext,
2130 )
2131 .field(
2132 "allow_basic_over_cleartext",
2133 &self.allow_basic_over_cleartext,
2134 )
2135 .field("audit_sink", &self.audit_sink.is_some())
2136 .field("audit_failure_policy", &self.audit_failure_policy)
2137 .field("rate_limiter", &self.rate_limiter.is_some())
2138 .field("digest_replay_store", &self.digest_replay_store.is_some())
2139 .finish()
2140 }
2141}
2142
2143#[derive(Clone)]
2144struct DigestProviderAuthStore {
2145 authenticator: DigestAuthenticator,
2146 realm: String,
2147 provider: Arc<dyn DigestSecretProvider>,
2148 nonces: Arc<RwLock<HashMap<String, Instant>>>,
2149 nonce_counts: Arc<RwLock<HashMap<(String, String), u32>>>,
2150 nonce_ttl: Duration,
2151 replay_store: Option<Arc<dyn DigestReplayStore>>,
2152}
2153
2154impl DigestProviderAuthStore {
2155 fn new(realm: impl Into<String>, provider: Arc<dyn DigestSecretProvider>) -> Self {
2156 let realm = realm.into();
2157 Self {
2158 authenticator: DigestAuthenticator::new(realm.clone()),
2159 realm,
2160 provider,
2161 nonces: Arc::new(RwLock::new(HashMap::new())),
2162 nonce_counts: Arc::new(RwLock::new(HashMap::new())),
2163 nonce_ttl: Duration::from_secs(300),
2164 replay_store: None,
2165 }
2166 }
2167
2168 fn with_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
2169 self.authenticator = self.authenticator.with_algorithm(algorithm);
2170 self
2171 }
2172
2173 fn with_replay_store(mut self, replay_store: Arc<dyn DigestReplayStore>) -> Self {
2174 self.replay_store = Some(replay_store);
2175 self
2176 }
2177
2178 fn challenge(&self) -> DigestChallenge {
2179 let challenge = self.authenticator.generate_challenge();
2180 self.record_nonce_local(&challenge.nonce);
2181 challenge
2182 }
2183
2184 async fn challenge_async(&self) -> Result<DigestChallenge> {
2185 let challenge = self.authenticator.generate_challenge();
2186 if let Some(replay_store) = &self.replay_store {
2187 replay_store
2188 .record_nonce(&challenge.nonce, system_time_after(self.nonce_ttl))
2189 .await
2190 .map_err(|err| SessionError::AuthError(err.to_string()))?;
2191 } else {
2192 self.record_nonce_local(&challenge.nonce);
2193 }
2194 Ok(challenge)
2195 }
2196
2197 fn record_nonce_local(&self, nonce: &str) {
2198 let expires_at = Instant::now()
2199 .checked_add(self.nonce_ttl)
2200 .unwrap_or_else(Instant::now);
2201 let mut nonces = self
2202 .nonces
2203 .write()
2204 .unwrap_or_else(|poisoned| poisoned.into_inner());
2205 nonces.insert(nonce.to_string(), expires_at);
2206 }
2207
2208 fn www_authenticate(&self, challenge: &DigestChallenge) -> String {
2209 self.authenticator.format_www_authenticate(challenge)
2210 }
2211
2212 fn www_authenticate_with_stale(&self, challenge: &DigestChallenge, stale: bool) -> String {
2213 self.authenticator
2214 .format_www_authenticate_with_stale(challenge, stale)
2215 }
2216
2217 async fn validate_authorization_detailed(
2218 &self,
2219 authorization: &str,
2220 method: &str,
2221 request_uri: &str,
2222 body: Option<&[u8]>,
2223 ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2224 let response = match DigestAuthenticator::parse_authorization(authorization) {
2225 Ok(response) => response,
2226 Err(_) => {
2227 return self
2228 .rejected_with_reason(AuthFailureReason::MalformedCredential)
2229 .await
2230 }
2231 };
2232
2233 if response.uri != request_uri || response.realm != self.realm {
2234 return self
2235 .rejected_with_reason(AuthFailureReason::InvalidCredential)
2236 .await;
2237 }
2238
2239 let secret = match self
2240 .provider
2241 .lookup_digest_secret(&response.username, &response.realm, response.algorithm)
2242 .await
2243 {
2244 Ok(Some(secret)) => secret,
2245 Ok(None) | Err(CredentialAuthError::Invalid) => {
2246 return self
2247 .rejected_with_reason(AuthFailureReason::InvalidCredential)
2248 .await
2249 }
2250 Err(CredentialAuthError::PolicyRejected(_)) => {
2251 return self
2252 .rejected_with_reason(AuthFailureReason::PolicyRejected)
2253 .await
2254 }
2255 Err(err) => return Err(SessionError::AuthError(err.to_string())),
2256 };
2257
2258 let valid = match self
2259 .authenticator
2260 .validate_response_with_secret_and_body(&response, method, &secret, body)
2261 {
2262 Ok(valid) => valid,
2263 Err(_) => {
2264 return self
2265 .rejected_with_reason(AuthFailureReason::UnsupportedScheme)
2266 .await
2267 }
2268 };
2269 if !valid {
2270 return self
2271 .rejected_with_reason(AuthFailureReason::InvalidCredential)
2272 .await;
2273 }
2274
2275 match self.nonce_status_async(&response.nonce).await? {
2276 NonceStatus::Active => {}
2277 NonceStatus::Expired => {
2278 return self
2279 .rejected_stale_with_reason(AuthFailureReason::StaleNonce)
2280 .await
2281 }
2282 NonceStatus::Unknown => {
2283 return self
2284 .rejected_with_reason(AuthFailureReason::InvalidCredential)
2285 .await
2286 }
2287 }
2288
2289 if let Some(reason) = self.accept_nonce_count_async(&response).await? {
2290 return self.rejected_with_reason(reason).await;
2291 }
2292
2293 Ok((
2294 AuthDecision::Authorized {
2295 username: response.username,
2296 realm: response.realm,
2297 },
2298 None,
2299 ))
2300 }
2301
2302 fn nonce_status(&self, nonce: &str) -> NonceStatus {
2303 let now = Instant::now();
2304 let mut nonces = self
2305 .nonces
2306 .write()
2307 .unwrap_or_else(|poisoned| poisoned.into_inner());
2308 match nonces.get(nonce).copied() {
2309 Some(expires_at) if expires_at > now => NonceStatus::Active,
2310 Some(_) => {
2311 nonces.remove(nonce);
2312 NonceStatus::Expired
2313 }
2314 None => NonceStatus::Unknown,
2315 }
2316 }
2317
2318 fn accept_nonce_count(&self, response: &DigestResponse) -> bool {
2319 let Some(qop) = response.qop.as_deref() else {
2320 return true;
2321 };
2322 if qop != "auth" && qop != "auth-int" {
2323 return false;
2324 }
2325 let Some(nc) = response
2326 .nc
2327 .as_deref()
2328 .and_then(|value| u32::from_str_radix(value, 16).ok())
2329 else {
2330 return false;
2331 };
2332 let Some(cnonce) = response.cnonce.clone() else {
2333 return false;
2334 };
2335 if cnonce.is_empty() {
2336 return false;
2337 }
2338 let key = (response.username.clone(), response.nonce.clone());
2339 let mut nonce_counts = self
2340 .nonce_counts
2341 .write()
2342 .unwrap_or_else(|poisoned| poisoned.into_inner());
2343 if nonce_counts.get(&key).is_some_and(|last| nc <= *last) {
2344 return false;
2345 }
2346 nonce_counts.insert(key, nc);
2347 true
2348 }
2349
2350 async fn nonce_status_async(&self, nonce: &str) -> Result<NonceStatus> {
2351 let Some(replay_store) = &self.replay_store else {
2352 return Ok(self.nonce_status(nonce));
2353 };
2354 match replay_store
2355 .nonce_status(nonce, SystemTime::now())
2356 .await
2357 .map_err(|err| SessionError::AuthError(err.to_string()))?
2358 {
2359 DigestNonceStatus::Active => Ok(NonceStatus::Active),
2360 DigestNonceStatus::Expired => Ok(NonceStatus::Expired),
2361 DigestNonceStatus::Unknown => Ok(NonceStatus::Unknown),
2362 }
2363 }
2364
2365 async fn accept_nonce_count_async(
2366 &self,
2367 response: &DigestResponse,
2368 ) -> Result<Option<AuthFailureReason>> {
2369 let Some(qop) = response.qop.as_deref() else {
2370 return Ok(None);
2371 };
2372 if qop != "auth" && qop != "auth-int" {
2373 return Ok(Some(AuthFailureReason::UnsupportedScheme));
2374 }
2375 let Some(nc) = response
2376 .nc
2377 .as_deref()
2378 .and_then(|value| u32::from_str_radix(value, 16).ok())
2379 else {
2380 return Ok(Some(AuthFailureReason::MalformedCredential));
2381 };
2382 let Some(cnonce) = response.cnonce.as_ref() else {
2383 return Ok(Some(AuthFailureReason::MalformedCredential));
2384 };
2385 if cnonce.is_empty() {
2386 return Ok(Some(AuthFailureReason::MalformedCredential));
2387 }
2388 let accepted = if let Some(replay_store) = &self.replay_store {
2389 replay_store
2390 .accept_nonce_count(&response.username, &response.nonce, nc)
2391 .await
2392 .map_err(|err| SessionError::AuthError(err.to_string()))?
2393 } else {
2394 self.accept_nonce_count(response)
2395 };
2396 if accepted {
2397 Ok(None)
2398 } else {
2399 Ok(Some(AuthFailureReason::ReplayRejected))
2400 }
2401 }
2402
2403 async fn rejected_async(&self) -> Result<AuthDecision> {
2404 let challenge = self.challenge_async().await?;
2405 let www_authenticate = self.www_authenticate(&challenge);
2406 Ok(AuthDecision::Rejected {
2407 challenge,
2408 www_authenticate,
2409 })
2410 }
2411
2412 async fn rejected_stale_async(&self) -> Result<AuthDecision> {
2413 let challenge = self.challenge_async().await?;
2414 let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
2415 Ok(AuthDecision::Rejected {
2416 challenge,
2417 www_authenticate,
2418 })
2419 }
2420
2421 async fn rejected_with_reason(
2422 &self,
2423 reason: AuthFailureReason,
2424 ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2425 Ok((self.rejected_async().await?, Some(reason)))
2426 }
2427
2428 async fn rejected_stale_with_reason(
2429 &self,
2430 reason: AuthFailureReason,
2431 ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2432 Ok((self.rejected_stale_async().await?, Some(reason)))
2433 }
2434}
2435
2436#[derive(Clone)]
2437struct BasicAuthStore {
2438 realm: String,
2439 users: Arc<RwLock<HashMap<String, String>>>,
2440 verifier: Option<Arc<dyn PasswordVerifier>>,
2441}
2442
2443fn identity_from_bearer_assurance(
2444 assurance: IdentityAssurance,
2445 realm: Option<String>,
2446 source: SipAuthSource,
2447) -> AuthIdentity {
2448 match assurance {
2449 IdentityAssurance::UserAuthorized {
2450 user_id, scopes, ..
2451 } => AuthIdentity {
2452 scheme: SipAuthScheme::Bearer,
2453 username: None,
2454 subject: Some(user_id.to_string()),
2455 realm,
2456 scopes,
2457 source,
2458 },
2459 IdentityAssurance::TaskScoped {
2460 identity,
2461 task_id,
2462 scopes,
2463 ..
2464 } => AuthIdentity {
2465 scheme: SipAuthScheme::Bearer,
2466 username: None,
2467 subject: Some(format!("{}:{}", identity, task_id)),
2468 realm,
2469 scopes,
2470 source,
2471 },
2472 other => AuthIdentity {
2473 scheme: SipAuthScheme::Bearer,
2474 username: None,
2475 subject: Some(format!("{other:?}")),
2476 realm,
2477 scopes: Vec::new(),
2478 source,
2479 },
2480 }
2481}
2482
2483fn auth_attempt_scheme(authorization: Option<&str>) -> AuthAttemptScheme {
2484 let Some(authorization) = authorization
2485 .map(str::trim)
2486 .filter(|value| !value.is_empty())
2487 else {
2488 return AuthAttemptScheme::Missing;
2489 };
2490 let lower = authorization.to_ascii_lowercase();
2491 if lower.starts_with("bearer ") {
2492 AuthAttemptScheme::Bearer
2493 } else if lower.starts_with("basic ") {
2494 AuthAttemptScheme::Basic
2495 } else if lower.starts_with("digest ") {
2496 if contains_aka_challenge(authorization) {
2497 AuthAttemptScheme::Aka
2498 } else {
2499 AuthAttemptScheme::Digest
2500 }
2501 } else {
2502 AuthAttemptScheme::Unknown
2503 }
2504}
2505
2506fn auth_outcome_for_decision(
2507 decision: &SipAuthDecision,
2508 failure_reason: Option<AuthFailureReason>,
2509) -> AuthAuditOutcome {
2510 match decision {
2511 SipAuthDecision::Authorized(_) => AuthAuditOutcome::Success,
2512 SipAuthDecision::Rejected { .. } => AuthAuditOutcome::Failure(
2513 failure_reason.unwrap_or(AuthFailureReason::InvalidCredential),
2514 ),
2515 }
2516}
2517
2518fn subject_realm_from_authorization(
2519 authorization: Option<&str>,
2520) -> (Option<String>, Option<String>) {
2521 let Some(authorization) = authorization
2522 .map(str::trim)
2523 .filter(|value| !value.is_empty())
2524 else {
2525 return (None, None);
2526 };
2527 let lower = authorization.to_ascii_lowercase();
2528 if lower.starts_with("digest ") {
2529 return DigestAuthenticator::parse_authorization(authorization)
2530 .map(|response| (Some(response.username), Some(response.realm)))
2531 .unwrap_or((None, None));
2532 }
2533 if lower.starts_with("basic ") {
2534 let token = authorization
2535 .split_once(char::is_whitespace)
2536 .map(|(_, value)| value.trim())
2537 .unwrap_or_default();
2538 return BASE64_STANDARD
2539 .decode(token)
2540 .ok()
2541 .and_then(|decoded| String::from_utf8(decoded).ok())
2542 .and_then(|decoded| {
2543 decoded
2544 .split_once(':')
2545 .map(|(username, _)| username.to_string())
2546 })
2547 .map(|username| (Some(username), None))
2548 .unwrap_or((None, None));
2549 }
2550 (None, None)
2551}
2552
2553fn system_time_after(duration: Duration) -> SystemTime {
2554 SystemTime::now()
2555 .checked_add(duration)
2556 .unwrap_or_else(SystemTime::now)
2557}
2558
2559async fn accept_nonce_count_with_replay_store(
2560 response: &DigestResponse,
2561 replay_store: &dyn DigestReplayStore,
2562) -> Result<bool> {
2563 let Some(qop) = response.qop.as_deref() else {
2564 return Ok(true);
2565 };
2566 if qop != "auth" && qop != "auth-int" {
2567 return Ok(false);
2568 }
2569 let Some(nc) = response
2570 .nc
2571 .as_deref()
2572 .and_then(|value| u32::from_str_radix(value, 16).ok())
2573 else {
2574 return Ok(false);
2575 };
2576 let Some(cnonce) = response.cnonce.as_ref() else {
2577 return Ok(false);
2578 };
2579 if cnonce.is_empty() {
2580 return Ok(false);
2581 }
2582 replay_store
2583 .accept_nonce_count(&response.username, &response.nonce, nc)
2584 .await
2585 .map_err(|err| SessionError::AuthError(err.to_string()))
2586}
2587
2588fn bearer_challenge_value(
2589 realm: &str,
2590 scope: Option<&str>,
2591 error: Option<&str>,
2592 error_description: Option<&str>,
2593) -> String {
2594 let mut value = format!("Bearer realm=\"{realm}\"");
2595 if let Some(scope) = scope {
2596 value.push_str(&format!(", scope=\"{scope}\""));
2597 }
2598 if let Some(error) = error {
2599 value.push_str(&format!(", error=\"{error}\""));
2600 }
2601 if let Some(error_description) = error_description {
2602 value.push_str(&format!(", error_description=\"{error_description}\""));
2603 }
2604 value
2605}
2606
2607fn contains_auth_scheme(value: &str, scheme: &str) -> bool {
2608 split_auth_challenges(value).into_iter().any(|challenge| {
2609 let trimmed = challenge.trim_start();
2610 let token = trimmed
2611 .split_once(char::is_whitespace)
2612 .map(|(token, _)| token)
2613 .unwrap_or(trimmed);
2614 token.eq_ignore_ascii_case(scheme)
2615 })
2616}
2617
2618fn contains_aka_challenge(value: &str) -> bool {
2619 let upper = value.to_ascii_uppercase();
2620 upper.contains("AKAV1-MD5") || upper.contains("AKAV2-MD5")
2621}
2622
2623fn extract_digest_challenge(value: &str) -> Option<String> {
2624 let mut best = None;
2625 for challenge in split_auth_challenges(value)
2626 .into_iter()
2627 .filter(|challenge| {
2628 challenge
2629 .trim_start()
2630 .to_ascii_lowercase()
2631 .starts_with("digest ")
2632 })
2633 {
2634 let Ok(parsed) = rvoip_auth_core::DigestAuthenticator::parse_challenge(&challenge) else {
2635 if best.is_none() {
2636 best = Some((0, challenge));
2637 }
2638 continue;
2639 };
2640 let strength = digest_algorithm_strength(parsed.algorithm);
2641 if best
2642 .as_ref()
2643 .map_or(true, |(best_strength, _)| strength > *best_strength)
2644 {
2645 best = Some((strength, challenge));
2646 }
2647 }
2648 best.map(|(_, challenge)| challenge)
2649}
2650
2651fn parse_digest_stale(value: &str) -> bool {
2652 let Some(challenge) = extract_digest_challenge(value) else {
2653 return false;
2654 };
2655 rvoip_auth_core::DigestAuthenticator::parse_challenge_details(&challenge)
2656 .map(|details| details.stale)
2657 .unwrap_or(false)
2658}
2659
2660fn digest_algorithm_strength(algorithm: DigestAlgorithm) -> u8 {
2661 match algorithm {
2662 DigestAlgorithm::SHA512256Sess => 60,
2663 DigestAlgorithm::SHA512256 => 50,
2664 DigestAlgorithm::SHA256Sess => 40,
2665 DigestAlgorithm::SHA256 => 30,
2666 DigestAlgorithm::MD5Sess => 20,
2667 DigestAlgorithm::MD5 => 10,
2668 }
2669}
2670
2671fn split_auth_challenges(value: &str) -> Vec<String> {
2672 let mut challenges = Vec::new();
2673 let mut start = 0;
2674 let mut in_quotes = false;
2675 let chars: Vec<(usize, char)> = value.char_indices().collect();
2676
2677 for (position, (idx, ch)) in chars.iter().copied().enumerate() {
2678 if ch == '"' {
2679 in_quotes = !in_quotes;
2680 continue;
2681 }
2682 if ch != ',' || in_quotes {
2683 continue;
2684 }
2685 let next_idx = idx + ch.len_utf8();
2686 let rest = &value[next_idx..];
2687 let trimmed = rest.trim_start();
2688 if looks_like_auth_challenge_start(trimmed) {
2689 let current = value[start..idx].trim();
2690 if !current.is_empty() {
2691 challenges.push(current.to_string());
2692 }
2693 let whitespace = rest.len() - trimmed.len();
2694 start = next_idx + whitespace;
2695 }
2696
2697 if position + 1 == chars.len() {
2698 break;
2699 }
2700 }
2701
2702 let current = value[start..].trim();
2703 if !current.is_empty() {
2704 challenges.push(current.to_string());
2705 }
2706 challenges
2707}
2708
2709fn looks_like_auth_challenge_start(value: &str) -> bool {
2710 let mut token_len = 0;
2711 for ch in value.chars() {
2712 if ch.is_ascii_alphanumeric() || ch == '-' {
2713 token_len += ch.len_utf8();
2714 } else {
2715 break;
2716 }
2717 }
2718 if token_len == 0 {
2719 return false;
2720 }
2721 let rest = &value[token_len..];
2722 rest.starts_with(char::is_whitespace)
2723}
2724
2725fn select_composite_client_auth(
2726 auths: &[SipClientAuth],
2727 challenge_header: &str,
2728 method: &str,
2729 request_uri: &str,
2730 nonce_count: u32,
2731 body: Option<&[u8]>,
2732 transport: &SipTransportSecurityContext,
2733) -> Result<ClientAuthHeader> {
2734 let priorities: &[fn(&SipClientAuth) -> bool] = &[
2735 |auth| matches!(auth, SipClientAuth::Aka(_)),
2736 |auth| {
2737 matches!(
2738 auth,
2739 SipClientAuth::BearerToken(_) | SipClientAuth::BearerTokenCleartextAllowed(_)
2740 )
2741 },
2742 |auth| matches!(auth, SipClientAuth::Digest(_)),
2743 |auth| matches!(auth, SipClientAuth::Basic { .. }),
2744 ];
2745
2746 for matches_priority in priorities {
2747 for auth in auths.iter().filter(|auth| matches_priority(auth)) {
2748 if let Ok(header) = auth.authorization_for_challenge_with_transport_context(
2749 challenge_header,
2750 method,
2751 request_uri,
2752 nonce_count,
2753 body,
2754 transport,
2755 ) {
2756 return Ok(header);
2757 }
2758 }
2759 }
2760
2761 Err(SessionError::AuthError(
2762 "no configured auth option can answer the challenge".to_string(),
2763 ))
2764}
2765
2766#[derive(Debug, Clone, PartialEq, Eq)]
2769pub enum AuthDecision {
2770 Authorized {
2772 username: String,
2774 realm: String,
2776 },
2777 Rejected {
2779 challenge: DigestChallenge,
2781 www_authenticate: String,
2783 },
2784}
2785
2786#[derive(Clone)]
2796pub struct SipDigestAuthService {
2797 authenticator: DigestAuthenticator,
2798 realm: String,
2799 users: Arc<RwLock<HashMap<String, String>>>,
2800 nonces: Arc<RwLock<HashMap<String, Instant>>>,
2801 nonce_counts: Arc<RwLock<HashMap<(String, String), u32>>>,
2802 nonce_ttl: Duration,
2803}
2804
2805impl SipDigestAuthService {
2806 pub fn new(realm: impl Into<String>) -> Self {
2808 let realm = realm.into();
2809 Self {
2810 authenticator: DigestAuthenticator::new(realm.clone()),
2811 realm,
2812 users: Arc::new(RwLock::new(HashMap::new())),
2813 nonces: Arc::new(RwLock::new(HashMap::new())),
2814 nonce_counts: Arc::new(RwLock::new(HashMap::new())),
2815 nonce_ttl: Duration::from_secs(300),
2816 }
2817 }
2818
2819 pub fn with_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
2821 self.authenticator = self.authenticator.with_algorithm(algorithm);
2822 self
2823 }
2824
2825 pub fn with_nonce_ttl(mut self, ttl: Duration) -> Self {
2827 self.nonce_ttl = ttl;
2828 self
2829 }
2830
2831 pub fn add_user(&self, username: impl Into<String>, password: impl Into<String>) {
2833 let mut users = self
2834 .users
2835 .write()
2836 .unwrap_or_else(|poisoned| poisoned.into_inner());
2837 users.insert(username.into(), password.into());
2838 }
2839
2840 pub fn challenge(&self) -> DigestChallenge {
2842 let challenge = self.authenticator.generate_challenge();
2843 let expires_at = Instant::now() + self.nonce_ttl;
2844 let mut nonces = self
2845 .nonces
2846 .write()
2847 .unwrap_or_else(|poisoned| poisoned.into_inner());
2848 nonces.insert(challenge.nonce.clone(), expires_at);
2849 challenge
2850 }
2851
2852 pub async fn challenge_with_replay_store(
2858 &self,
2859 replay_store: Arc<dyn DigestReplayStore>,
2860 ) -> Result<DigestChallenge> {
2861 let challenge = self.authenticator.generate_challenge();
2862 replay_store
2863 .record_nonce(&challenge.nonce, system_time_after(self.nonce_ttl))
2864 .await
2865 .map_err(|err| SessionError::AuthError(err.to_string()))?;
2866 Ok(challenge)
2867 }
2868
2869 pub fn www_authenticate(&self, challenge: &DigestChallenge) -> String {
2871 self.authenticator.format_www_authenticate(challenge)
2872 }
2873
2874 pub fn www_authenticate_with_stale(&self, challenge: &DigestChallenge, stale: bool) -> String {
2877 self.authenticator
2878 .format_www_authenticate_with_stale(challenge, stale)
2879 }
2880
2881 pub fn authenticate_authorization(
2886 &self,
2887 authorization: Option<&str>,
2888 method: &str,
2889 request_uri: &str,
2890 body: Option<&[u8]>,
2891 ) -> Result<AuthDecision> {
2892 let Some(authorization) = authorization else {
2893 return Ok(self.rejected());
2894 };
2895
2896 self.validate_authorization(authorization, method, request_uri, body)
2897 }
2898
2899 pub async fn authenticate_authorization_with_replay_store(
2906 &self,
2907 authorization: Option<&str>,
2908 method: &str,
2909 request_uri: &str,
2910 body: Option<&[u8]>,
2911 replay_store: Arc<dyn DigestReplayStore>,
2912 ) -> Result<AuthDecision> {
2913 let Some(authorization) = authorization else {
2914 return self.rejected_with_replay_store(replay_store).await;
2915 };
2916
2917 self.validate_authorization_with_replay_store(
2918 authorization,
2919 method,
2920 request_uri,
2921 body,
2922 replay_store,
2923 )
2924 .await
2925 }
2926
2927 pub fn validate_authorization(
2933 &self,
2934 authorization: &str,
2935 method: &str,
2936 request_uri: &str,
2937 body: Option<&[u8]>,
2938 ) -> Result<AuthDecision> {
2939 let response = match DigestAuthenticator::parse_authorization(authorization) {
2940 Ok(response) => response,
2941 Err(_) => return Ok(self.rejected()),
2942 };
2943
2944 if response.uri != request_uri {
2945 return Ok(self.rejected());
2946 }
2947 if response.realm != self.realm {
2948 return Ok(self.rejected());
2949 }
2950
2951 let password = {
2952 let users = self
2953 .users
2954 .read()
2955 .unwrap_or_else(|poisoned| poisoned.into_inner());
2956 match users.get(&response.username) {
2957 Some(password) => password.clone(),
2958 None => return Ok(self.rejected()),
2959 }
2960 };
2961
2962 let valid = match self
2963 .authenticator
2964 .validate_response_with_body(&response, method, &password, body)
2965 {
2966 Ok(valid) => valid,
2967 Err(_) => return Ok(self.rejected()),
2968 };
2969 if !valid {
2970 return Ok(self.rejected());
2971 }
2972
2973 match self.nonce_status(&response.nonce) {
2974 NonceStatus::Active => {}
2975 NonceStatus::Expired => return Ok(self.rejected_stale()),
2976 NonceStatus::Unknown => return Ok(self.rejected()),
2977 }
2978
2979 if !self.accept_nonce_count(&response) {
2980 return Ok(self.rejected());
2981 }
2982
2983 Ok(AuthDecision::Authorized {
2984 username: response.username,
2985 realm: response.realm,
2986 })
2987 }
2988
2989 pub async fn validate_authorization_with_replay_store(
2992 &self,
2993 authorization: &str,
2994 method: &str,
2995 request_uri: &str,
2996 body: Option<&[u8]>,
2997 replay_store: Arc<dyn DigestReplayStore>,
2998 ) -> Result<AuthDecision> {
2999 let response = match DigestAuthenticator::parse_authorization(authorization) {
3000 Ok(response) => response,
3001 Err(_) => return self.rejected_with_replay_store(replay_store).await,
3002 };
3003
3004 if response.uri != request_uri || response.realm != self.realm {
3005 return self.rejected_with_replay_store(replay_store).await;
3006 }
3007
3008 let password = {
3009 let users = self
3010 .users
3011 .read()
3012 .unwrap_or_else(|poisoned| poisoned.into_inner());
3013 users.get(&response.username).cloned()
3014 };
3015 let Some(password) = password else {
3016 return self.rejected_with_replay_store(replay_store).await;
3017 };
3018
3019 let valid = match self
3020 .authenticator
3021 .validate_response_with_body(&response, method, &password, body)
3022 {
3023 Ok(valid) => valid,
3024 Err(_) => return self.rejected_with_replay_store(replay_store).await,
3025 };
3026 if !valid {
3027 return self.rejected_with_replay_store(replay_store).await;
3028 }
3029
3030 match replay_store
3031 .nonce_status(&response.nonce, SystemTime::now())
3032 .await
3033 .map_err(|err| SessionError::AuthError(err.to_string()))?
3034 {
3035 DigestNonceStatus::Active => {}
3036 DigestNonceStatus::Expired => {
3037 return self.rejected_stale_with_replay_store(replay_store).await
3038 }
3039 DigestNonceStatus::Unknown => {
3040 return self.rejected_with_replay_store(replay_store).await
3041 }
3042 }
3043
3044 if !accept_nonce_count_with_replay_store(&response, replay_store.as_ref()).await? {
3045 return self.rejected_with_replay_store(replay_store).await;
3046 }
3047
3048 Ok(AuthDecision::Authorized {
3049 username: response.username,
3050 realm: response.realm,
3051 })
3052 }
3053
3054 fn nonce_status(&self, nonce: &str) -> NonceStatus {
3055 let now = Instant::now();
3056 let mut nonces = self
3057 .nonces
3058 .write()
3059 .unwrap_or_else(|poisoned| poisoned.into_inner());
3060 match nonces.get(nonce).copied() {
3061 Some(expires_at) if expires_at > now => NonceStatus::Active,
3062 Some(_) => {
3063 nonces.remove(nonce);
3064 NonceStatus::Expired
3065 }
3066 None => NonceStatus::Unknown,
3067 }
3068 }
3069
3070 fn accept_nonce_count(&self, response: &DigestResponse) -> bool {
3071 let Some(qop) = response.qop.as_deref() else {
3072 return true;
3073 };
3074 if qop != "auth" && qop != "auth-int" {
3075 return false;
3076 }
3077 let Some(nc) = response
3078 .nc
3079 .as_deref()
3080 .and_then(|value| u32::from_str_radix(value, 16).ok())
3081 else {
3082 return false;
3083 };
3084 let Some(cnonce) = response.cnonce.clone() else {
3085 return false;
3086 };
3087 if cnonce.is_empty() {
3088 return false;
3089 }
3090 let key = (response.username.clone(), response.nonce.clone());
3091 let mut nonce_counts = self
3092 .nonce_counts
3093 .write()
3094 .unwrap_or_else(|poisoned| poisoned.into_inner());
3095 if nonce_counts.get(&key).is_some_and(|last| nc <= *last) {
3096 return false;
3097 }
3098 nonce_counts.insert(key, nc);
3099 true
3100 }
3101
3102 fn rejected(&self) -> AuthDecision {
3103 let challenge = self.challenge();
3104 let www_authenticate = self.www_authenticate(&challenge);
3105 AuthDecision::Rejected {
3106 challenge,
3107 www_authenticate,
3108 }
3109 }
3110
3111 fn rejected_stale(&self) -> AuthDecision {
3112 let challenge = self.challenge();
3113 let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
3114 AuthDecision::Rejected {
3115 challenge,
3116 www_authenticate,
3117 }
3118 }
3119
3120 async fn rejected_with_replay_store(
3121 &self,
3122 replay_store: Arc<dyn DigestReplayStore>,
3123 ) -> Result<AuthDecision> {
3124 let challenge = self.challenge_with_replay_store(replay_store).await?;
3125 let www_authenticate = self.www_authenticate(&challenge);
3126 Ok(AuthDecision::Rejected {
3127 challenge,
3128 www_authenticate,
3129 })
3130 }
3131
3132 async fn rejected_stale_with_replay_store(
3133 &self,
3134 replay_store: Arc<dyn DigestReplayStore>,
3135 ) -> Result<AuthDecision> {
3136 let challenge = self.challenge_with_replay_store(replay_store).await?;
3137 let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
3138 Ok(AuthDecision::Rejected {
3139 challenge,
3140 www_authenticate,
3141 })
3142 }
3143}
3144
3145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3146enum NonceStatus {
3147 Active,
3148 Expired,
3149 Unknown,
3150}
3151
3152impl std::fmt::Debug for SipDigestAuthService {
3153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3154 let user_count = self
3155 .users
3156 .read()
3157 .map(|users| users.len())
3158 .unwrap_or_default();
3159 let nonce_count = self
3160 .nonces
3161 .read()
3162 .map(|nonces| nonces.len())
3163 .unwrap_or_default();
3164 f.debug_struct("SipDigestAuthService")
3165 .field("authenticator", &self.authenticator)
3166 .field("user_count", &user_count)
3167 .field("nonce_count", &nonce_count)
3168 .field("nonce_ttl", &self.nonce_ttl)
3169 .finish()
3170 }
3171}
3172
3173#[cfg(test)]
3174mod tests {
3175 use super::*;
3176 use proptest::prelude::*;
3177 use std::sync::Mutex;
3178
3179 struct StaticPasswordVerifier;
3180
3181 #[async_trait::async_trait]
3182 impl PasswordVerifier for StaticPasswordVerifier {
3183 async fn verify_password(
3184 &self,
3185 username: &str,
3186 password: &str,
3187 ) -> std::result::Result<IdentityAssurance, CredentialAuthError> {
3188 if username == "alice" && password == "secret" {
3189 Ok(test_assurance())
3190 } else {
3191 Err(CredentialAuthError::Invalid)
3192 }
3193 }
3194 }
3195
3196 struct StaticDigestProvider;
3197
3198 #[async_trait::async_trait]
3199 impl DigestSecretProvider for StaticDigestProvider {
3200 async fn lookup_digest_secret(
3201 &self,
3202 username: &str,
3203 _realm: &str,
3204 _algorithm: DigestAlgorithm,
3205 ) -> std::result::Result<Option<DigestSecret>, CredentialAuthError> {
3206 if username == "alice" {
3207 Ok(Some(DigestSecret::PlaintextPassword("secret".to_string())))
3208 } else {
3209 Ok(None)
3210 }
3211 }
3212 }
3213
3214 #[derive(Clone, Default)]
3215 struct RecordingAuditSink {
3216 events: Arc<Mutex<Vec<AuthAuditEvent>>>,
3217 fail: bool,
3218 }
3219
3220 impl RecordingAuditSink {
3221 fn into_arc(self) -> Arc<dyn AuthAuditSink> {
3222 Arc::new(self)
3223 }
3224
3225 fn events(&self) -> Vec<AuthAuditEvent> {
3226 self.events.lock().unwrap().clone()
3227 }
3228 }
3229
3230 #[async_trait::async_trait]
3231 impl AuthAuditSink for RecordingAuditSink {
3232 async fn record_auth_event(
3233 &self,
3234 event: AuthAuditEvent,
3235 ) -> std::result::Result<(), CredentialAuthError> {
3236 if self.fail {
3237 return Err(CredentialAuthError::Unavailable("audit down".to_string()));
3238 }
3239 self.events.lock().unwrap().push(event);
3240 Ok(())
3241 }
3242 }
3243
3244 #[derive(Clone)]
3245 struct TestRateLimiter {
3246 verdict: AuthRateLimitVerdict,
3247 checked: Arc<Mutex<Vec<AuthRateLimitKey>>>,
3248 results: Arc<Mutex<Vec<AuthAuditOutcome>>>,
3249 fail_check: bool,
3250 fail_record: bool,
3251 }
3252
3253 impl TestRateLimiter {
3254 fn allow() -> Self {
3255 Self {
3256 verdict: AuthRateLimitVerdict::Allowed,
3257 checked: Arc::new(Mutex::new(Vec::new())),
3258 results: Arc::new(Mutex::new(Vec::new())),
3259 fail_check: false,
3260 fail_record: false,
3261 }
3262 }
3263
3264 fn deny() -> Self {
3265 Self {
3266 verdict: AuthRateLimitVerdict::Denied {
3267 retry_after: Some(Duration::from_secs(1)),
3268 },
3269 checked: Arc::new(Mutex::new(Vec::new())),
3270 results: Arc::new(Mutex::new(Vec::new())),
3271 fail_check: false,
3272 fail_record: false,
3273 }
3274 }
3275
3276 fn fail_check() -> Self {
3277 Self {
3278 fail_check: true,
3279 ..Self::allow()
3280 }
3281 }
3282
3283 fn into_arc(self) -> Arc<dyn AuthRateLimiter> {
3284 Arc::new(self)
3285 }
3286
3287 fn results(&self) -> Vec<AuthAuditOutcome> {
3288 self.results.lock().unwrap().clone()
3289 }
3290 }
3291
3292 #[async_trait::async_trait]
3293 impl AuthRateLimiter for TestRateLimiter {
3294 async fn check_auth_attempt(
3295 &self,
3296 key: &AuthRateLimitKey,
3297 ) -> std::result::Result<AuthRateLimitVerdict, CredentialAuthError> {
3298 if self.fail_check {
3299 return Err(CredentialAuthError::Unavailable(
3300 "rate limiter down".to_string(),
3301 ));
3302 }
3303 self.checked.lock().unwrap().push(key.clone());
3304 Ok(self.verdict.clone())
3305 }
3306
3307 async fn record_auth_result(
3308 &self,
3309 _key: &AuthRateLimitKey,
3310 outcome: &AuthAuditOutcome,
3311 ) -> std::result::Result<(), CredentialAuthError> {
3312 if self.fail_record {
3313 return Err(CredentialAuthError::Unavailable(
3314 "rate limiter record down".to_string(),
3315 ));
3316 }
3317 self.results.lock().unwrap().push(outcome.clone());
3318 Ok(())
3319 }
3320 }
3321
3322 struct UnavailableBearer;
3323
3324 #[async_trait::async_trait]
3325 impl BearerValidator for UnavailableBearer {
3326 async fn validate(
3327 &self,
3328 _token: &str,
3329 ) -> std::result::Result<IdentityAssurance, BearerAuthError> {
3330 Err(BearerAuthError::Unavailable("idp down".to_string()))
3331 }
3332 }
3333
3334 #[derive(Default)]
3335 struct MemoryDigestReplayStore {
3336 nonces: Mutex<HashMap<String, SystemTime>>,
3337 nonce_counts: Mutex<HashMap<(String, String), u32>>,
3338 force_expired: Mutex<bool>,
3339 }
3340
3341 impl MemoryDigestReplayStore {
3342 fn set_force_expired(&self, expired: bool) {
3343 *self.force_expired.lock().unwrap() = expired;
3344 }
3345 }
3346
3347 #[async_trait::async_trait]
3348 impl DigestReplayStore for MemoryDigestReplayStore {
3349 async fn record_nonce(
3350 &self,
3351 nonce: &str,
3352 expires_at: SystemTime,
3353 ) -> std::result::Result<(), CredentialAuthError> {
3354 self.nonces
3355 .lock()
3356 .unwrap()
3357 .insert(nonce.to_string(), expires_at);
3358 Ok(())
3359 }
3360
3361 async fn nonce_status(
3362 &self,
3363 nonce: &str,
3364 now: SystemTime,
3365 ) -> std::result::Result<DigestNonceStatus, CredentialAuthError> {
3366 let nonces = self.nonces.lock().unwrap();
3367 let Some(expires_at) = nonces.get(nonce).copied() else {
3368 return Ok(DigestNonceStatus::Unknown);
3369 };
3370 if *self.force_expired.lock().unwrap() || expires_at <= now {
3371 Ok(DigestNonceStatus::Expired)
3372 } else {
3373 Ok(DigestNonceStatus::Active)
3374 }
3375 }
3376
3377 async fn accept_nonce_count(
3378 &self,
3379 username: &str,
3380 nonce: &str,
3381 nonce_count: u32,
3382 ) -> std::result::Result<bool, CredentialAuthError> {
3383 let key = (username.to_string(), nonce.to_string());
3384 let mut counts = self.nonce_counts.lock().unwrap();
3385 if counts.get(&key).is_some_and(|last| nonce_count <= *last) {
3386 return Ok(false);
3387 }
3388 counts.insert(key, nonce_count);
3389 Ok(true)
3390 }
3391 }
3392
3393 fn test_assurance() -> IdentityAssurance {
3394 let identity = rvoip_core_traits::ids::IdentityId::from_string("user_alice");
3395 IdentityAssurance::UserAuthorized {
3396 identity: identity.clone(),
3397 user_id: identity,
3398 scopes: vec!["sip.register".to_string()],
3399 }
3400 }
3401
3402 fn auth_token_strategy() -> impl Strategy<Value = String> {
3403 proptest::string::string_regex("[A-Za-z0-9._~-]{1,16}").unwrap()
3404 }
3405
3406 fn quoted_auth_value_strategy() -> impl Strategy<Value = String> {
3407 prop::collection::vec(
3408 proptest::string::string_regex("[A-Za-z0-9._~-]{1,12}").unwrap(),
3409 1..4,
3410 )
3411 .prop_map(|parts| parts.join(","))
3412 }
3413
3414 fn authorization_for(
3415 username: &str,
3416 password: &str,
3417 challenge: &DigestChallenge,
3418 method: &str,
3419 uri: &str,
3420 body: Option<&[u8]>,
3421 ) -> String {
3422 authorization_for_nc(username, password, challenge, method, uri, body, 1)
3423 }
3424
3425 fn authorization_for_nc(
3426 username: &str,
3427 password: &str,
3428 challenge: &DigestChallenge,
3429 method: &str,
3430 uri: &str,
3431 body: Option<&[u8]>,
3432 nc: u32,
3433 ) -> String {
3434 let computed = DigestAuth::compute_response_with_state(
3435 username, password, challenge, method, uri, nc, body,
3436 )
3437 .expect("digest computation");
3438 DigestAuth::format_authorization_with_state(username, challenge, uri, &computed)
3439 }
3440
3441 #[test]
3442 fn sip_digest_auth_service_accepts_valid_authorization() {
3443 let service =
3444 SipDigestAuthService::new("example.test").with_algorithm(DigestAlgorithm::SHA512256);
3445 service.add_user("alice", "secret");
3446 let challenge = service.challenge();
3447 let authorization = authorization_for(
3448 "alice",
3449 "secret",
3450 &challenge,
3451 "OPTIONS",
3452 "sip:bob@example.test",
3453 None,
3454 );
3455
3456 let decision = service
3457 .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3458 .expect("validation succeeds");
3459
3460 assert_eq!(
3461 decision,
3462 AuthDecision::Authorized {
3463 username: "alice".to_string(),
3464 realm: "example.test".to_string(),
3465 }
3466 );
3467 }
3468
3469 #[test]
3470 fn sip_digest_auth_service_rejects_missing_or_invalid_authorization() {
3471 let service = SipDigestAuthService::new("example.test");
3472 service.add_user("alice", "secret");
3473 let challenge = service.challenge();
3474 let wrong_password = authorization_for(
3475 "alice",
3476 "wrong",
3477 &challenge,
3478 "MESSAGE",
3479 "sip:bob@example.test",
3480 None,
3481 );
3482
3483 assert!(matches!(
3484 service
3485 .authenticate_authorization(None, "MESSAGE", "sip:bob@example.test", None)
3486 .expect("missing auth decision"),
3487 AuthDecision::Rejected { .. }
3488 ));
3489 assert!(matches!(
3490 service
3491 .validate_authorization(&wrong_password, "MESSAGE", "sip:bob@example.test", None)
3492 .expect("invalid auth decision"),
3493 AuthDecision::Rejected { .. }
3494 ));
3495 }
3496
3497 #[test]
3498 fn sip_digest_auth_service_rejects_realm_mismatch() {
3499 let service = SipDigestAuthService::new("example.test");
3500 service.add_user("alice", "secret");
3501 let mut challenge = service.challenge();
3502 challenge.realm = "wrong.realm".to_string();
3503 let authorization = authorization_for(
3504 "alice",
3505 "secret",
3506 &challenge,
3507 "OPTIONS",
3508 "sip:bob@example.test",
3509 None,
3510 );
3511
3512 assert!(matches!(
3513 service
3514 .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3515 .expect("realm mismatch decision"),
3516 AuthDecision::Rejected { .. }
3517 ));
3518 }
3519
3520 #[test]
3521 fn sip_digest_auth_service_marks_expired_issued_nonce_stale() {
3522 let service =
3523 SipDigestAuthService::new("example.test").with_nonce_ttl(Duration::from_millis(1));
3524 service.add_user("alice", "secret");
3525 let challenge = service.challenge();
3526 let authorization = authorization_for(
3527 "alice",
3528 "secret",
3529 &challenge,
3530 "OPTIONS",
3531 "sip:bob@example.test",
3532 None,
3533 );
3534
3535 std::thread::sleep(Duration::from_millis(5));
3536
3537 let decision = service
3538 .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3539 .expect("expired nonce decision");
3540
3541 match decision {
3542 AuthDecision::Rejected {
3543 www_authenticate, ..
3544 } => assert!(
3545 www_authenticate.contains("stale=true"),
3546 "expired nonce should produce stale challenge: {www_authenticate}"
3547 ),
3548 other => panic!("expected stale rejection, got {other:?}"),
3549 }
3550 }
3551
3552 #[test]
3553 fn sip_digest_auth_service_rejects_unknown_nonce_and_replay() {
3554 let service = SipDigestAuthService::new("example.test");
3555 service.add_user("alice", "secret");
3556
3557 let unknown_challenge = DigestChallenge {
3558 realm: "example.test".to_string(),
3559 nonce: "not-issued".to_string(),
3560 algorithm: DigestAlgorithm::MD5,
3561 qop: Some(vec!["auth".to_string()]),
3562 opaque: None,
3563 };
3564 let unknown_nonce_auth = authorization_for(
3565 "alice",
3566 "secret",
3567 &unknown_challenge,
3568 "OPTIONS",
3569 "sip:bob@example.test",
3570 None,
3571 );
3572 assert!(matches!(
3573 service
3574 .validate_authorization(
3575 &unknown_nonce_auth,
3576 "OPTIONS",
3577 "sip:bob@example.test",
3578 None
3579 )
3580 .expect("unknown nonce decision"),
3581 AuthDecision::Rejected { .. }
3582 ));
3583
3584 let challenge = service.challenge();
3585 let authorization = authorization_for(
3586 "alice",
3587 "secret",
3588 &challenge,
3589 "OPTIONS",
3590 "sip:bob@example.test",
3591 None,
3592 );
3593 assert!(matches!(
3594 service
3595 .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3596 .expect("first nonce-count decision"),
3597 AuthDecision::Authorized { .. }
3598 ));
3599 assert!(matches!(
3600 service
3601 .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3602 .expect("replayed nonce-count decision"),
3603 AuthDecision::Rejected { .. }
3604 ));
3605
3606 let next_nonce_same_count = authorization_for_nc(
3607 "alice",
3608 "secret",
3609 &challenge,
3610 "OPTIONS",
3611 "sip:bob@example.test",
3612 None,
3613 1,
3614 );
3615 assert!(matches!(
3616 service
3617 .validate_authorization(
3618 &next_nonce_same_count,
3619 "OPTIONS",
3620 "sip:bob@example.test",
3621 None
3622 )
3623 .expect("same nonce-count with new cnonce decision"),
3624 AuthDecision::Rejected { .. }
3625 ));
3626
3627 let next_nonce_count = authorization_for_nc(
3628 "alice",
3629 "secret",
3630 &challenge,
3631 "OPTIONS",
3632 "sip:bob@example.test",
3633 None,
3634 2,
3635 );
3636 assert!(matches!(
3637 service
3638 .validate_authorization(&next_nonce_count, "OPTIONS", "sip:bob@example.test", None)
3639 .expect("higher nonce-count decision"),
3640 AuthDecision::Authorized { .. }
3641 ));
3642 }
3643
3644 #[test]
3645 fn sip_digest_auth_service_validates_auth_int_body() {
3646 let service = SipDigestAuthService::new("example.test");
3647 service.add_user("alice", "secret");
3648 let mut challenge = service.challenge();
3649 challenge.qop = Some(vec!["auth-int".to_string()]);
3650 let body = b"hello";
3651 let authorization = authorization_for(
3652 "alice",
3653 "secret",
3654 &challenge,
3655 "MESSAGE",
3656 "sip:bob@example.test",
3657 Some(body),
3658 );
3659
3660 assert!(matches!(
3661 service
3662 .validate_authorization(
3663 &authorization,
3664 "MESSAGE",
3665 "sip:bob@example.test",
3666 Some(body)
3667 )
3668 .expect("auth-int decision"),
3669 AuthDecision::Authorized { .. }
3670 ));
3671 }
3672
3673 #[tokio::test]
3674 async fn sip_auth_service_accepts_basic_when_cleartext_explicitly_allowed() {
3675 let mut service = SipAuthService::new()
3676 .with_basic_realm("legacy")
3677 .allow_basic_over_cleartext(true);
3678 service.add_basic_user("alice", "secret");
3679 let token = BASE64_STANDARD.encode("alice:secret");
3680
3681 let decision = service
3682 .authenticate_authorization(
3683 Some(&format!("Basic {token}")),
3684 "OPTIONS",
3685 "sip:bob@example.test",
3686 None,
3687 SipAuthSource::Origin,
3688 false,
3689 )
3690 .await
3691 .expect("basic validation");
3692
3693 assert_eq!(
3694 decision,
3695 SipAuthDecision::Authorized(AuthIdentity {
3696 scheme: SipAuthScheme::Basic,
3697 username: Some("alice".to_string()),
3698 subject: None,
3699 realm: Some("legacy".to_string()),
3700 scopes: Vec::new(),
3701 source: SipAuthSource::Origin,
3702 })
3703 );
3704 }
3705
3706 #[tokio::test]
3707 async fn sip_auth_service_rejects_basic_over_cleartext_by_default() {
3708 let mut service = SipAuthService::new().with_basic_realm("legacy");
3709 service.add_basic_user("alice", "secret");
3710 let token = BASE64_STANDARD.encode("alice:secret");
3711
3712 let decision = service
3713 .authenticate_authorization(
3714 Some(&format!("Basic {token}")),
3715 "OPTIONS",
3716 "sip:bob@example.test",
3717 None,
3718 SipAuthSource::Origin,
3719 false,
3720 )
3721 .await
3722 .expect("basic validation");
3723
3724 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3725 }
3726
3727 #[tokio::test]
3728 async fn sip_auth_service_accepts_basic_with_secure_transport_context() {
3729 let mut service = SipAuthService::new().with_basic_realm("legacy");
3730 service.add_basic_user("alice", "secret");
3731 let token = BASE64_STANDARD.encode("alice:secret");
3732
3733 let decision = service
3734 .authenticate_authorization_with_transport_context(
3735 Some(&format!("Basic {token}")),
3736 "OPTIONS",
3737 "sip:bob@example.test",
3738 None,
3739 SipAuthSource::Origin,
3740 &SipTransportSecurityContext::from_transport_name("WSS"),
3741 )
3742 .await
3743 .expect("basic validation with transport context");
3744
3745 assert!(matches!(
3746 decision,
3747 SipAuthDecision::Authorized(AuthIdentity {
3748 scheme: SipAuthScheme::Basic,
3749 ..
3750 })
3751 ));
3752 }
3753
3754 #[tokio::test]
3755 async fn sip_auth_service_accepts_basic_password_verifier() {
3756 let service = SipAuthService::new()
3757 .with_basic_verifier("legacy", Arc::new(StaticPasswordVerifier))
3758 .allow_basic_over_cleartext(true);
3759 let token = BASE64_STANDARD.encode("alice:secret");
3760
3761 let decision = service
3762 .authenticate_authorization(
3763 Some(&format!("Basic {token}")),
3764 "OPTIONS",
3765 "sip:bob@example.test",
3766 None,
3767 SipAuthSource::Origin,
3768 false,
3769 )
3770 .await
3771 .expect("provider-backed basic validation");
3772
3773 match decision {
3774 SipAuthDecision::Authorized(identity) => {
3775 assert_eq!(identity.scheme, SipAuthScheme::Basic);
3776 assert_eq!(identity.username.as_deref(), Some("alice"));
3777 assert_eq!(identity.realm.as_deref(), Some("legacy"));
3778 assert_eq!(identity.scopes, vec!["sip.register".to_string()]);
3779 }
3780 other => panic!("expected provider-backed Basic authorization, got {other:?}"),
3781 }
3782 }
3783
3784 #[tokio::test]
3785 async fn sip_auth_service_accepts_bearer_validator_identity() {
3786 let service =
3787 SipAuthService::new().with_bearer_validator("api", rvoip_auth_core::bearer_stub());
3788
3789 let decision = service
3790 .authenticate_authorization_with_transport_context(
3791 Some("Bearer token-123"),
3792 "MESSAGE",
3793 "sip:bob@example.test",
3794 None,
3795 SipAuthSource::Proxy,
3796 &SipTransportSecurityContext::from_transport_name("TLS"),
3797 )
3798 .await
3799 .expect("bearer validation");
3800
3801 match decision {
3802 SipAuthDecision::Authorized(identity) => {
3803 assert_eq!(identity.scheme, SipAuthScheme::Bearer);
3804 assert_eq!(identity.realm.as_deref(), Some("api"));
3805 assert_eq!(identity.source, SipAuthSource::Proxy);
3806 }
3807 other => panic!("expected bearer authorization, got {other:?}"),
3808 }
3809 }
3810
3811 #[tokio::test]
3812 async fn sip_auth_service_rejects_bearer_over_cleartext_by_default() {
3813 let service =
3814 SipAuthService::new().with_bearer_validator("api", rvoip_auth_core::bearer_stub());
3815
3816 let decision = service
3817 .authenticate_authorization(
3818 Some("Bearer token-123"),
3819 "MESSAGE",
3820 "sip:bob@example.test",
3821 None,
3822 SipAuthSource::Origin,
3823 false,
3824 )
3825 .await
3826 .expect("bearer cleartext policy");
3827
3828 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3829 }
3830
3831 #[tokio::test]
3832 async fn sip_auth_service_accepts_bearer_cleartext_when_explicitly_allowed() {
3833 let service = SipAuthService::new()
3834 .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
3835 .allow_bearer_over_cleartext(true);
3836
3837 let decision = service
3838 .authenticate_authorization(
3839 Some("Bearer token-123"),
3840 "MESSAGE",
3841 "sip:bob@example.test",
3842 None,
3843 SipAuthSource::Origin,
3844 false,
3845 )
3846 .await
3847 .expect("bearer cleartext opt-in");
3848
3849 assert!(matches!(
3850 decision,
3851 SipAuthDecision::Authorized(AuthIdentity {
3852 scheme: SipAuthScheme::Bearer,
3853 ..
3854 })
3855 ));
3856 }
3857
3858 #[tokio::test]
3859 async fn sip_auth_policy_filters_challenges_and_rejects_disabled_scheme() {
3860 let mut service = SipAuthService::digest("example.test")
3861 .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
3862 .with_basic_realm("legacy")
3863 .with_policy(SipAuthPolicy::new().allow_only([SipAuthScheme::Bearer]));
3864 service.add_digest_user("alice", "secret");
3865 service.add_basic_user("alice", "secret");
3866
3867 let challenges = service.challenges(SipAuthSource::Origin);
3868 assert_eq!(challenges.len(), 1);
3869 assert_eq!(challenges[0].scheme, SipAuthScheme::Bearer);
3870
3871 let token = BASE64_STANDARD.encode("alice:secret");
3872 let decision = service
3873 .authenticate_authorization_with_transport_context(
3874 Some(&format!("Basic {token}")),
3875 "OPTIONS",
3876 "sip:bob@example.test",
3877 None,
3878 SipAuthSource::Origin,
3879 &SipTransportSecurityContext::from_transport_name("TLS"),
3880 )
3881 .await
3882 .expect("policy rejection");
3883 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3884 }
3885
3886 #[tokio::test]
3887 async fn sip_auth_policy_rejects_digest_below_minimum_algorithm() {
3888 let mut service = SipAuthService::digest("example.test").with_policy(
3889 SipAuthPolicy::new().with_minimum_digest_algorithm(DigestAlgorithm::SHA256),
3890 );
3891 service.add_digest_user("alice", "secret");
3892 let challenge = SipDigestAuthService::new("example.test").challenge();
3893 let authorization = authorization_for(
3894 "alice",
3895 "secret",
3896 &challenge,
3897 "OPTIONS",
3898 "sip:bob@example.test",
3899 None,
3900 );
3901
3902 let decision = service
3903 .authenticate_authorization(
3904 Some(&authorization),
3905 "OPTIONS",
3906 "sip:bob@example.test",
3907 None,
3908 SipAuthSource::Origin,
3909 false,
3910 )
3911 .await
3912 .expect("minimum digest policy");
3913
3914 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3915 }
3916
3917 #[tokio::test]
3918 async fn sip_auth_policy_can_require_digest_replay_store() {
3919 let mut service = SipAuthService::digest("example.test")
3920 .with_policy(SipAuthPolicy::new().require_digest_replay_store(true));
3921 service.add_digest_user("alice", "secret");
3922 let challenge = SipDigestAuthService::new("example.test").challenge();
3923 let authorization = authorization_for(
3924 "alice",
3925 "secret",
3926 &challenge,
3927 "OPTIONS",
3928 "sip:bob@example.test",
3929 None,
3930 );
3931
3932 let decision = service
3933 .authenticate_authorization(
3934 Some(&authorization),
3935 "OPTIONS",
3936 "sip:bob@example.test",
3937 None,
3938 SipAuthSource::Origin,
3939 false,
3940 )
3941 .await
3942 .expect("required replay-store policy");
3943
3944 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3945 }
3946
3947 #[tokio::test]
3948 async fn sip_auth_service_accepts_digest_secret_provider() {
3949 let service = SipAuthService::new()
3950 .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
3951 .with_digest_provider_algorithm(DigestAlgorithm::SHA256);
3952 let challenge = service
3953 .challenges(SipAuthSource::Origin)
3954 .into_iter()
3955 .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
3956 .expect("digest challenge");
3957 let digest_challenge =
3958 DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
3959 assert_eq!(digest_challenge.algorithm, DigestAlgorithm::SHA256);
3960 let authorization = authorization_for(
3961 "alice",
3962 "secret",
3963 &digest_challenge,
3964 "OPTIONS",
3965 "sip:bob@example.test",
3966 None,
3967 );
3968
3969 let decision = service
3970 .authenticate_authorization(
3971 Some(&authorization),
3972 "OPTIONS",
3973 "sip:bob@example.test",
3974 None,
3975 SipAuthSource::Origin,
3976 false,
3977 )
3978 .await
3979 .expect("provider-backed digest validation");
3980
3981 assert_eq!(
3982 decision,
3983 SipAuthDecision::Authorized(AuthIdentity {
3984 scheme: SipAuthScheme::Digest,
3985 username: Some("alice".to_string()),
3986 subject: None,
3987 realm: Some("example.test".to_string()),
3988 scopes: Vec::new(),
3989 source: SipAuthSource::Origin,
3990 })
3991 );
3992 }
3993
3994 #[tokio::test]
3995 async fn sip_auth_service_emits_redacted_audit_and_rate_results() {
3996 let sink = RecordingAuditSink::default();
3997 let limiter = TestRateLimiter::allow();
3998 let mut service = SipAuthService::new()
3999 .with_basic_realm("legacy")
4000 .allow_basic_over_cleartext(true)
4001 .with_audit_sink(sink.clone().into_arc())
4002 .with_rate_limiter(limiter.clone().into_arc());
4003 service.add_basic_user("alice", "secret");
4004 let context = SipAuthContext::new()
4005 .with_peer("192.0.2.10")
4006 .with_metadata("tenant", "acme");
4007
4008 let valid = BASE64_STANDARD.encode("alice:secret");
4009 let wrong = BASE64_STANDARD.encode("alice:wrong");
4010
4011 service
4012 .authenticate_authorization_with_context(
4013 Some(&format!("Basic {valid}")),
4014 "OPTIONS",
4015 "sip:bob@example.test",
4016 None,
4017 SipAuthSource::Origin,
4018 false,
4019 &context,
4020 )
4021 .await
4022 .expect("valid Basic auth");
4023 service
4024 .authenticate_authorization_with_context(
4025 Some(&format!("Basic {wrong}")),
4026 "OPTIONS",
4027 "sip:bob@example.test",
4028 None,
4029 SipAuthSource::Origin,
4030 false,
4031 &context,
4032 )
4033 .await
4034 .expect("invalid Basic auth");
4035 service
4036 .authenticate_authorization_with_context(
4037 None,
4038 "OPTIONS",
4039 "sip:bob@example.test",
4040 None,
4041 SipAuthSource::Origin,
4042 false,
4043 &context,
4044 )
4045 .await
4046 .expect("missing auth");
4047
4048 let events = sink.events();
4049 assert_eq!(events.len(), 3);
4050 assert_eq!(events[0].scheme, AuthAuditScheme::Basic);
4051 assert_eq!(events[0].outcome, AuthAuditOutcome::Success);
4052 assert_eq!(events[0].subject.as_deref(), Some("alice"));
4053 assert_eq!(events[0].peer.as_deref(), Some("192.0.2.10"));
4054 assert_eq!(
4055 events[0].metadata.get("tenant").map(String::as_str),
4056 Some("acme")
4057 );
4058 assert_eq!(
4059 events[1].outcome,
4060 AuthAuditOutcome::Failure(AuthFailureReason::InvalidCredential)
4061 );
4062 assert_eq!(
4063 events[2].outcome,
4064 AuthAuditOutcome::Failure(AuthFailureReason::MissingCredential)
4065 );
4066 assert_eq!(
4067 limiter.results(),
4068 vec![
4069 AuthAuditOutcome::Success,
4070 AuthAuditOutcome::Failure(AuthFailureReason::InvalidCredential),
4071 AuthAuditOutcome::Failure(AuthFailureReason::MissingCredential)
4072 ]
4073 );
4074 for event in events {
4075 assert!(
4076 !event
4077 .metadata
4078 .values()
4079 .any(|value| value.contains("secret")),
4080 "audit metadata must not contain credentials: {event:?}"
4081 );
4082 }
4083 }
4084
4085 #[tokio::test]
4086 async fn sip_auth_service_audits_basic_cleartext_rejection() {
4087 let sink = RecordingAuditSink::default();
4088 let mut service = SipAuthService::new()
4089 .with_basic_realm("legacy")
4090 .with_audit_sink(sink.clone().into_arc());
4091 service.add_basic_user("alice", "secret");
4092 let token = BASE64_STANDARD.encode("alice:secret");
4093
4094 let decision = service
4095 .authenticate_authorization(
4096 Some(&format!("Basic {token}")),
4097 "OPTIONS",
4098 "sip:bob@example.test",
4099 None,
4100 SipAuthSource::Origin,
4101 false,
4102 )
4103 .await
4104 .expect("Basic cleartext rejection");
4105
4106 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
4107 assert_eq!(
4108 sink.events()[0].outcome,
4109 AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)
4110 );
4111 }
4112
4113 #[tokio::test]
4114 async fn sip_auth_service_rate_limiter_denies_before_validation() {
4115 let sink = RecordingAuditSink::default();
4116 let limiter = TestRateLimiter::deny();
4117 let mut service = SipAuthService::new()
4118 .with_basic_realm("legacy")
4119 .allow_basic_over_cleartext(true)
4120 .with_audit_sink(sink.clone().into_arc())
4121 .with_rate_limiter(limiter.clone().into_arc());
4122 service.add_basic_user("alice", "secret");
4123 let token = BASE64_STANDARD.encode("alice:secret");
4124
4125 let decision = service
4126 .authenticate_authorization(
4127 Some(&format!("Basic {token}")),
4128 "OPTIONS",
4129 "sip:bob@example.test",
4130 None,
4131 SipAuthSource::Origin,
4132 false,
4133 )
4134 .await
4135 .expect("rate-limit denial");
4136
4137 assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
4138 assert_eq!(
4139 sink.events()[0].outcome,
4140 AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)
4141 );
4142 assert_eq!(
4143 limiter.results(),
4144 vec![AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)]
4145 );
4146 }
4147
4148 #[tokio::test]
4149 async fn sip_auth_service_rate_limiter_failure_fails_closed() {
4150 let sink = RecordingAuditSink::default();
4151 let limiter = TestRateLimiter::fail_check();
4152 let service = SipAuthService::new()
4153 .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
4154 .with_audit_sink(sink.clone().into_arc())
4155 .with_rate_limiter(limiter.into_arc());
4156
4157 let err = service
4158 .authenticate_authorization(
4159 Some("Bearer token"),
4160 "MESSAGE",
4161 "sip:bob@example.test",
4162 None,
4163 SipAuthSource::Origin,
4164 false,
4165 )
4166 .await
4167 .expect_err("rate limiter failure should fail closed");
4168
4169 assert!(matches!(err, SessionError::AuthError(_)));
4170 assert_eq!(
4171 sink.events()[0].outcome,
4172 AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable)
4173 );
4174 }
4175
4176 #[tokio::test]
4177 async fn sip_auth_service_audits_bearer_provider_unavailable() {
4178 let sink = RecordingAuditSink::default();
4179 let service = SipAuthService::new()
4180 .with_bearer_validator("api", Arc::new(UnavailableBearer))
4181 .with_audit_sink(sink.clone().into_arc());
4182
4183 let err = service
4184 .authenticate_authorization_with_transport_context(
4185 Some("Bearer token"),
4186 "MESSAGE",
4187 "sip:bob@example.test",
4188 None,
4189 SipAuthSource::Origin,
4190 &SipTransportSecurityContext::from_transport_name("TLS"),
4191 )
4192 .await
4193 .expect_err("provider failure should return error");
4194
4195 assert!(matches!(err, SessionError::AuthError(_)));
4196 assert_eq!(
4197 sink.events()[0].outcome,
4198 AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable)
4199 );
4200 }
4201
4202 #[tokio::test]
4203 async fn sip_auth_service_uses_digest_replay_store() {
4204 let replay_store = Arc::new(MemoryDigestReplayStore::default());
4205 let sink = RecordingAuditSink::default();
4206 let service = SipAuthService::new()
4207 .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
4208 .with_digest_replay_store(replay_store.clone())
4209 .with_audit_sink(sink.clone().into_arc());
4210 let challenge = service
4211 .challenges_async(SipAuthSource::Origin)
4212 .await
4213 .expect("async challenges")
4214 .into_iter()
4215 .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4216 .expect("Digest challenge");
4217 let digest_challenge =
4218 DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
4219 assert_eq!(
4220 replay_store
4221 .nonce_status(&digest_challenge.nonce, SystemTime::now())
4222 .await
4223 .unwrap(),
4224 DigestNonceStatus::Active
4225 );
4226 let authorization = authorization_for(
4227 "alice",
4228 "secret",
4229 &digest_challenge,
4230 "OPTIONS",
4231 "sip:bob@example.test",
4232 None,
4233 );
4234
4235 let first = service
4236 .authenticate_authorization(
4237 Some(&authorization),
4238 "OPTIONS",
4239 "sip:bob@example.test",
4240 None,
4241 SipAuthSource::Origin,
4242 false,
4243 )
4244 .await
4245 .expect("first Digest auth");
4246 assert!(matches!(first, SipAuthDecision::Authorized(_)));
4247
4248 let replay = service
4249 .authenticate_authorization(
4250 Some(&authorization),
4251 "OPTIONS",
4252 "sip:bob@example.test",
4253 None,
4254 SipAuthSource::Origin,
4255 false,
4256 )
4257 .await
4258 .expect("replayed Digest auth");
4259 assert!(matches!(replay, SipAuthDecision::Rejected { .. }));
4260 assert_eq!(
4261 sink.events().last().unwrap().outcome,
4262 AuthAuditOutcome::Failure(AuthFailureReason::ReplayRejected)
4263 );
4264 }
4265
4266 #[tokio::test]
4267 async fn sip_auth_service_preserves_digest_stale_challenge() {
4268 let replay_store = Arc::new(MemoryDigestReplayStore::default());
4269 let sink = RecordingAuditSink::default();
4270 let service = SipAuthService::new()
4271 .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
4272 .with_digest_replay_store(replay_store.clone())
4273 .with_audit_sink(sink.clone().into_arc());
4274 let challenge = service
4275 .challenges_async(SipAuthSource::Origin)
4276 .await
4277 .expect("async challenges")
4278 .into_iter()
4279 .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4280 .expect("Digest challenge");
4281 let digest_challenge =
4282 DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
4283 replay_store.set_force_expired(true);
4284 let authorization = authorization_for(
4285 "alice",
4286 "secret",
4287 &digest_challenge,
4288 "OPTIONS",
4289 "sip:bob@example.test",
4290 None,
4291 );
4292
4293 let decision = service
4294 .authenticate_authorization(
4295 Some(&authorization),
4296 "OPTIONS",
4297 "sip:bob@example.test",
4298 None,
4299 SipAuthSource::Origin,
4300 false,
4301 )
4302 .await
4303 .expect("stale Digest auth");
4304
4305 match decision {
4306 SipAuthDecision::Rejected { challenges } => {
4307 let digest = challenges
4308 .into_iter()
4309 .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4310 .expect("Digest challenge");
4311 assert!(
4312 digest.value.contains("stale=true"),
4313 "stale challenge must be preserved: {}",
4314 digest.value
4315 );
4316 }
4317 other => panic!("expected stale rejection, got {other:?}"),
4318 }
4319 assert_eq!(
4320 sink.events().last().unwrap().outcome,
4321 AuthAuditOutcome::Failure(AuthFailureReason::StaleNonce)
4322 );
4323 }
4324
4325 #[tokio::test]
4326 async fn sip_digest_auth_service_supports_replay_store_helper() {
4327 let replay_store = Arc::new(MemoryDigestReplayStore::default());
4328 let service = SipDigestAuthService::new("example.test");
4329 service.add_user("alice", "secret");
4330 let challenge = service
4331 .challenge_with_replay_store(replay_store.clone())
4332 .await
4333 .expect("challenge with replay store");
4334 let authorization = authorization_for(
4335 "alice",
4336 "secret",
4337 &challenge,
4338 "OPTIONS",
4339 "sip:bob@example.test",
4340 None,
4341 );
4342
4343 let first = service
4344 .authenticate_authorization_with_replay_store(
4345 Some(&authorization),
4346 "OPTIONS",
4347 "sip:bob@example.test",
4348 None,
4349 replay_store.clone(),
4350 )
4351 .await
4352 .expect("first Digest auth");
4353 assert!(matches!(first, AuthDecision::Authorized { .. }));
4354
4355 let replay = service
4356 .authenticate_authorization_with_replay_store(
4357 Some(&authorization),
4358 "OPTIONS",
4359 "sip:bob@example.test",
4360 None,
4361 replay_store,
4362 )
4363 .await
4364 .expect("replayed Digest auth");
4365 assert!(matches!(replay, AuthDecision::Rejected { .. }));
4366 }
4367
4368 #[test]
4369 fn sip_transport_security_context_classifies_secure_transports() {
4370 assert!(SipTransportSecurityContext::from_transport_name("TLS").is_secure());
4371 assert!(SipTransportSecurityContext::from_transport_name("wss").is_secure());
4372 assert!(
4373 SipTransportSecurityContext::from_request_uri_hint("sips:bob@example.test").is_secure()
4374 );
4375 assert!(
4376 SipTransportSecurityContext::from_request_uri_transport_hint(
4377 "sip:bob@example.test;transport=tls"
4378 )
4379 .is_secure()
4380 );
4381 assert!(
4382 SipTransportSecurityContext::from_request_uri_transport_hint(
4383 "sip:bob@example.test;transport=wss"
4384 )
4385 .is_secure()
4386 );
4387 assert!(!SipTransportSecurityContext::from_transport_name("UDP").is_secure());
4388 assert!(
4389 !SipTransportSecurityContext::from_request_uri_hint("sip:bob@example.test").is_secure()
4390 );
4391 }
4392
4393 #[test]
4394 fn sip_client_auth_basic_uses_transport_context_policy() {
4395 let auth = SipClientAuth::basic("alice", "secret");
4396 let cleartext = auth.authorization_for_challenge_with_transport_context(
4397 r#"Basic realm="legacy""#,
4398 "OPTIONS",
4399 "sip:bob@example.test",
4400 1,
4401 None,
4402 &SipTransportSecurityContext::unknown(),
4403 );
4404 assert!(
4405 format!("{:?}", cleartext.expect_err("cleartext Basic must fail"))
4406 .contains("cleartext")
4407 );
4408
4409 let secure = auth
4410 .authorization_for_challenge_with_transport_context(
4411 r#"Basic realm="legacy""#,
4412 "OPTIONS",
4413 "sip:bob@example.test",
4414 1,
4415 None,
4416 &SipTransportSecurityContext::from_transport_name("TLS"),
4417 )
4418 .expect("secure transport permits Basic");
4419 assert_eq!(secure.scheme, SipAuthScheme::Basic);
4420 assert!(secure.value.starts_with("Basic "));
4421 }
4422
4423 #[test]
4424 fn sip_client_auth_bearer_uses_transport_context_policy() {
4425 let auth = SipClientAuth::bearer_token("token-123");
4426 let cleartext = auth.authorization_for_challenge_with_transport_context(
4427 r#"Bearer realm="api""#,
4428 "OPTIONS",
4429 "sip:bob@example.test",
4430 1,
4431 None,
4432 &SipTransportSecurityContext::unknown(),
4433 );
4434 assert!(
4435 format!("{:?}", cleartext.expect_err("cleartext Bearer must fail"))
4436 .contains("cleartext")
4437 );
4438
4439 let secure = auth
4440 .authorization_for_challenge_with_transport_context(
4441 r#"Bearer realm="api""#,
4442 "OPTIONS",
4443 "sip:bob@example.test",
4444 1,
4445 None,
4446 &SipTransportSecurityContext::from_transport_name("TLS"),
4447 )
4448 .expect("secure transport permits Bearer");
4449 assert_eq!(secure.scheme, SipAuthScheme::Bearer);
4450 assert_eq!(secure.value, "Bearer token-123");
4451
4452 let cleartext_allowed = SipClientAuth::bearer_token("token-123")
4453 .allow_bearer_over_cleartext(true)
4454 .authorization_for_challenge_with_transport_context(
4455 r#"Bearer realm="api""#,
4456 "OPTIONS",
4457 "sip:bob@example.test",
4458 1,
4459 None,
4460 &SipTransportSecurityContext::unknown(),
4461 )
4462 .expect("explicit cleartext opt-in permits Bearer");
4463 assert_eq!(cleartext_allowed.value, "Bearer token-123");
4464 }
4465
4466 #[test]
4467 fn sip_client_auth_matches_schemes_case_insensitively() {
4468 let bearer = SipClientAuth::bearer_token("token-123")
4469 .authorization_for_challenge_with_transport_context(
4470 r#"bearer realm="api""#,
4471 "OPTIONS",
4472 "sip:bob@example.test",
4473 1,
4474 None,
4475 &SipTransportSecurityContext::from_transport_name("TLS"),
4476 )
4477 .expect("lowercase bearer challenge must match");
4478 assert_eq!(bearer.scheme, SipAuthScheme::Bearer);
4479
4480 let basic = SipClientAuth::basic("alice", "secret")
4481 .allow_basic_over_cleartext(true)
4482 .authorization_for_challenge(
4483 r#"bAsIc realm="legacy""#,
4484 "OPTIONS",
4485 "sip:bob@example.test",
4486 1,
4487 None,
4488 false,
4489 )
4490 .expect("mixed-case basic challenge must match");
4491 assert_eq!(basic.scheme, SipAuthScheme::Basic);
4492 }
4493
4494 #[test]
4495 fn sip_client_auth_composite_selects_strongest_compatible_scheme() {
4496 let auth = SipClientAuth::any([
4497 SipClientAuth::digest("alice", "secret"),
4498 SipClientAuth::bearer_token("token-123"),
4499 SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4500 ]);
4501 let header = auth
4502 .authorization_for_challenge_with_transport_context(
4503 r#"Digest realm="pbx", nonce="n1", algorithm=MD5, Bearer realm="api", Basic realm="legacy""#,
4504 "OPTIONS",
4505 "sip:bob@example.test",
4506 1,
4507 None,
4508 &SipTransportSecurityContext::from_transport_name("TLS"),
4509 )
4510 .expect("composite auth");
4511
4512 assert_eq!(header.scheme, SipAuthScheme::Bearer);
4513 assert_eq!(header.value, "Bearer token-123");
4514 }
4515
4516 #[test]
4517 fn sip_client_auth_composite_handles_quoted_commas_in_challenge_lists() {
4518 let auth = SipClientAuth::any([
4519 SipClientAuth::digest("alice", "secret"),
4520 SipClientAuth::bearer_token("token-123"),
4521 SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4522 ]);
4523 let header = auth
4524 .authorization_for_challenge_with_transport_context(
4525 r#"Basic realm="legacy,with,commas", Digest realm="pbx", nonce="n1", algorithm=MD5, qop="auth,auth-int", Bearer realm="api", scope="sip.invite,sip.message""#,
4526 "OPTIONS",
4527 "sip:bob@example.test",
4528 1,
4529 None,
4530 &SipTransportSecurityContext::from_transport_name("TLS"),
4531 )
4532 .expect("composite auth with quoted commas");
4533
4534 assert_eq!(header.scheme, SipAuthScheme::Bearer);
4535 assert_eq!(header.value, "Bearer token-123");
4536 }
4537
4538 proptest! {
4539 #[test]
4540 fn auth_challenge_splitter_preserves_quoted_commas(
4541 basic_realm in quoted_auth_value_strategy(),
4542 digest_realm in quoted_auth_value_strategy(),
4543 nonce in auth_token_strategy(),
4544 bearer_scope in quoted_auth_value_strategy(),
4545 ) {
4546 let header = format!(
4547 r#"Basic realm="{basic_realm}", Digest realm="{digest_realm}", nonce="{nonce}", algorithm=SHA-256, qop="auth,auth-int", Bearer realm="api", scope="{bearer_scope}""#
4548 );
4549
4550 let challenges = split_auth_challenges(&header);
4551 prop_assert_eq!(
4552 challenges.len(),
4553 3,
4554 "challenge splitter must not split quoted commas: {:?}",
4555 challenges
4556 );
4557 prop_assert!(challenges[0].starts_with("Basic "));
4558 prop_assert!(challenges[1].starts_with("Digest "));
4559 prop_assert!(challenges[2].starts_with("Bearer "));
4560
4561 let digest = extract_digest_challenge(&header).expect("Digest challenge");
4562 let parsed = DigestAuthenticator::parse_challenge(&digest).expect("parse Digest challenge");
4563 prop_assert_eq!(parsed.realm, digest_realm);
4564 prop_assert_eq!(parsed.nonce, nonce);
4565 prop_assert_eq!(parsed.algorithm, DigestAlgorithm::SHA256);
4566 }
4567 }
4568
4569 #[test]
4570 fn sip_client_auth_composite_rejects_basic_downgrade_when_digest_is_offered() {
4571 let auth = SipClientAuth::any([
4572 SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4573 SipClientAuth::digest("alice", "secret"),
4574 ]);
4575 let header = auth
4576 .authorization_for_challenge(
4577 r#"Basic realm="legacy", Digest realm="pbx", nonce="n1", algorithm=SHA-256"#,
4578 "OPTIONS",
4579 "sip:bob@example.test",
4580 1,
4581 None,
4582 false,
4583 )
4584 .expect("composite auth");
4585
4586 assert_eq!(header.scheme, SipAuthScheme::Digest);
4587 assert!(header.value.starts_with("Digest "));
4588 }
4589
4590 #[test]
4591 fn sip_client_auth_digest_selects_strongest_supported_challenge() {
4592 let auth = SipClientAuth::digest("alice", "secret");
4593 let header = auth
4594 .authorization_for_challenge(
4595 r#"Digest realm="pbx", nonce="weak", algorithm=MD5, Digest realm="pbx", nonce="strong", algorithm=SHA-512-256, qop="auth""#,
4596 "OPTIONS",
4597 "sip:bob@example.test",
4598 1,
4599 None,
4600 false,
4601 )
4602 .expect("digest auth");
4603 let response =
4604 DigestAuthenticator::parse_authorization(&header.value).expect("parse authorization");
4605
4606 assert_eq!(header.scheme, SipAuthScheme::Digest);
4607 assert_eq!(response.algorithm, DigestAlgorithm::SHA512256);
4608 assert_eq!(response.nonce, "strong");
4609 }
4610
4611 #[test]
4612 fn sip_client_auth_digest_skips_malformed_challenge_when_valid_digest_exists() {
4613 let auth = SipClientAuth::digest("alice", "secret");
4614 let header = auth
4615 .authorization_for_challenge(
4616 r#"Digest realm="pbx", algorithm=SHA-512-256, Digest realm="pbx", nonce="valid", algorithm=SHA-256, qop="auth""#,
4617 "OPTIONS",
4618 "sip:bob@example.test",
4619 1,
4620 None,
4621 false,
4622 )
4623 .expect("valid Digest alternative should be selected");
4624 let response =
4625 DigestAuthenticator::parse_authorization(&header.value).expect("parse authorization");
4626
4627 assert_eq!(header.scheme, SipAuthScheme::Digest);
4628 assert_eq!(response.algorithm, DigestAlgorithm::SHA256);
4629 assert_eq!(response.nonce, "valid");
4630 }
4631
4632 #[test]
4633 fn sip_client_auth_digest_rejects_malformed_only_challenge() {
4634 let err = SipClientAuth::digest("alice", "secret")
4635 .authorization_for_challenge(
4636 r#"Digest realm="pbx", algorithm=SHA-512-256"#,
4637 "OPTIONS",
4638 "sip:bob@example.test",
4639 1,
4640 None,
4641 false,
4642 )
4643 .expect_err("malformed-only Digest challenge must fail");
4644
4645 assert!(
4646 format!("{err:?}").contains("Invalid digest challenge")
4647 || format!("{err:?}").contains("nonce"),
4648 "unexpected error for malformed Digest challenge: {err:?}"
4649 );
4650 }
4651}