Skip to main content

rvoip_core/
identity.rs

1//! Identity surface — the trait + the rich structs that reference
2//! rvoip-core's `Result` type live here; the *pure data types*
3//! (`Jwk`, `IdentityKind`, `DeviceKind`, `IdentityAssurance`,
4//! `CredentialKind`, `Credential`) moved to `rvoip-core-traits` in
5//! V2.A.1 and are re-exported below so `use rvoip_core::identity::*`
6//! call sites work unchanged.
7
8use crate::error::Result;
9use crate::ids::{DeviceId, IdentityId};
10use chrono::{DateTime, Utc};
11use std::collections::HashMap;
12use std::fmt;
13use tokio::sync::mpsc;
14
15// V2.A — pure-data types now live in rvoip-core-traits. Re-export so
16// downstream `use rvoip_core::identity::IdentityAssurance` etc. keep
17// working unchanged.
18pub use rvoip_core_traits::identity::{
19    AuthenticatedPrincipal, AuthenticationMethod, BearerAuthError, Credential, CredentialKind,
20    DeviceKind, IdentityAssurance, IdentityKind, Jwk, PrincipalOwnershipKey,
21};
22
23#[derive(Clone)]
24pub struct Identity {
25    pub id: IdentityId,
26    pub display_name: Option<String>,
27    pub kind: IdentityKind,
28    pub external_refs: HashMap<String, String>,
29    pub signing_keys: Vec<Jwk>,
30    pub assurance: IdentityAssurance,
31}
32
33impl fmt::Debug for Identity {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter
36            .debug_struct("Identity")
37            .field("id_present", &!self.id.as_str().is_empty())
38            .field("display_name_present", &self.display_name.is_some())
39            .field("kind", &self.kind)
40            .field("external_reference_count", &self.external_refs.len())
41            .field("signing_key_count", &self.signing_keys.len())
42            .field("assurance_kind", &self.assurance.kind())
43            .finish()
44    }
45}
46
47#[derive(Clone)]
48pub struct Device {
49    pub id: DeviceId,
50    pub identity_id: IdentityId,
51    pub kind: DeviceKind,
52    pub platform: String,
53    pub registered_at: DateTime<Utc>,
54    pub device_signing_key: Option<Jwk>,
55}
56
57impl fmt::Debug for Device {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter
60            .debug_struct("Device")
61            .field("id_present", &!self.id.as_str().is_empty())
62            .field("identity_present", &!self.identity_id.as_str().is_empty())
63            .field("kind", &self.kind)
64            .field("platform_present", &!self.platform.is_empty())
65            .field("signing_key_present", &self.device_signing_key.is_some())
66            .finish()
67    }
68}
69
70#[derive(Clone)]
71pub struct ReachabilityHint {
72    pub transport: crate::connection::Transport,
73    pub address: String,
74    pub device_id: DeviceId,
75    pub priority: u16,
76    pub expires_at: Option<DateTime<Utc>>,
77}
78
79impl fmt::Debug for ReachabilityHint {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter
82            .debug_struct("ReachabilityHint")
83            .field("transport", &self.transport)
84            .field("address_present", &!self.address.is_empty())
85            .field("device_present", &!self.device_id.as_str().is_empty())
86            .field("priority", &self.priority)
87            .field("expires_at_present", &self.expires_at.is_some())
88            .finish()
89    }
90}
91
92#[derive(Clone)]
93pub struct ReachabilityChange {
94    pub identity_id: IdentityId,
95    pub kind: ReachabilityChangeKind,
96    pub hint: ReachabilityHint,
97}
98
99impl fmt::Debug for ReachabilityChange {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        formatter
102            .debug_struct("ReachabilityChange")
103            .field("identity_present", &!self.identity_id.as_str().is_empty())
104            .field("kind", &self.kind)
105            .field("hint", &self.hint)
106            .finish()
107    }
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub enum ReachabilityChangeKind {
112    Added,
113    Removed,
114    Updated,
115    Expired,
116}
117
118/// DTLS-SRTP fingerprint binding payload (RFC 8122 §5).
119#[derive(Clone)]
120pub struct DtlsFingerprint {
121    pub algorithm: String,
122    pub value: String,
123}
124
125impl fmt::Debug for DtlsFingerprint {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter
128            .debug_struct("DtlsFingerprint")
129            .field("algorithm_present", &!self.algorithm.is_empty())
130            .field("fingerprint_bytes", &self.value.len())
131            .finish()
132    }
133}
134
135/// SignatureHeaders re-exported here for the trait surface — actual
136/// shape lives in [`crate::adapter::SignatureHeaders`].
137pub use crate::adapter::SignatureHeaders;
138
139/// Plug-in identity backend. P7 completes the trait surface per
140/// INTERFACE_DESIGN.md §8 (9 methods). Production impls live in
141/// `rvoip-identity`; the 3 default-`NotImplemented` methods let
142/// existing in-tree no-op impls compile unchanged.
143#[async_trait::async_trait]
144pub trait IdentityProvider: Send + Sync {
145    async fn resolve(&self, identity_ref: &str) -> Result<Identity>;
146    async fn devices(&self, identity_id: IdentityId) -> Result<Vec<Device>>;
147    async fn reachable_via(&self, identity_id: IdentityId) -> Result<Vec<ReachabilityHint>>;
148    async fn authenticate(&self, credential: Credential)
149        -> Result<(IdentityId, IdentityAssurance)>;
150
151    /// Authenticate a credential into the complete ownership-bound principal
152    /// needed for security-sensitive upgrades.
153    ///
154    /// The legacy [`Self::authenticate`] result omits issuer, tenant, subject,
155    /// and expiry and therefore cannot safely authorize a step-up on an
156    /// existing connection. Existing providers remain source compatible, but
157    /// step-up fails closed until they implement this additive method.
158    async fn authenticate_principal(
159        &self,
160        _credential: Credential,
161    ) -> Result<AuthenticatedPrincipal> {
162        Err(crate::error::RvoipError::NotImplemented(
163            "IdentityProvider::authenticate_principal",
164        ))
165    }
166    async fn assurance_level(&self, id: IdentityId) -> Result<IdentityAssurance>;
167    fn subscribe_reachability(&self) -> mpsc::Receiver<ReachabilityChange>;
168
169    /// P7 — register an agent's public signing key. Default
170    /// `NotImplemented`.
171    async fn register_agent_key(&self, _id: IdentityId, _key: Jwk) -> Result<()> {
172        Err(crate::error::RvoipError::NotImplemented(
173            "IdentityProvider::register_agent_key",
174        ))
175    }
176
177    /// P7 — verify an RFC 9421 signature against the identity's
178    /// registered keys.
179    async fn verify_signature(
180        &self,
181        _id: IdentityId,
182        _sig: SignatureHeaders,
183        _body: &[u8],
184    ) -> Result<IdentityAssurance> {
185        Err(crate::error::RvoipError::NotImplemented(
186            "IdentityProvider::verify_signature",
187        ))
188    }
189
190    /// P7 — derive (or look up) the DTLS-SRTP fingerprint bound to an
191    /// Identity. None when the identity has no fingerprint binding.
192    async fn derive_dtls_fingerprint(&self, _id: IdentityId) -> Result<Option<DtlsFingerprint>> {
193        Err(crate::error::RvoipError::NotImplemented(
194            "IdentityProvider::derive_dtls_fingerprint",
195        ))
196    }
197}
198
199#[cfg(test)]
200mod diagnostic_tests {
201    use super::*;
202
203    const CANARY: &str = "identity-diagnostic-canary\r\nAuthorization: exposed";
204
205    #[test]
206    fn identity_device_reachability_and_fingerprint_debug_are_metadata_only() {
207        let identity = Identity {
208            id: IdentityId::from_string(CANARY),
209            display_name: Some(CANARY.into()),
210            kind: IdentityKind::Service,
211            external_refs: HashMap::from([(CANARY.into(), CANARY.into())]),
212            signing_keys: vec![Jwk(serde_json::json!({"private": CANARY}))],
213            assurance: IdentityAssurance::DtlsFingerprint {
214                algorithm: CANARY.into(),
215                value: CANARY.into(),
216            },
217        };
218        let device = Device {
219            id: DeviceId::from_string(CANARY),
220            identity_id: IdentityId::from_string(CANARY),
221            kind: DeviceKind::Server,
222            platform: CANARY.into(),
223            registered_at: Utc::now(),
224            device_signing_key: Some(Jwk(serde_json::json!({"private": CANARY}))),
225        };
226        let hint = ReachabilityHint {
227            transport: crate::connection::Transport::Quic,
228            address: CANARY.into(),
229            device_id: DeviceId::from_string(CANARY),
230            priority: 1,
231            expires_at: None,
232        };
233        let fingerprint = DtlsFingerprint {
234            algorithm: CANARY.into(),
235            value: CANARY.into(),
236        };
237
238        for rendered in [
239            format!("{identity:?}"),
240            format!("{device:?}"),
241            format!("{hint:?}"),
242            format!("{fingerprint:?}"),
243        ] {
244            assert!(!rendered.contains(CANARY), "identity leaked: {rendered}");
245        }
246        assert_eq!(identity.id.as_str(), CANARY);
247        assert_eq!(device.platform, CANARY);
248        assert_eq!(hint.address, CANARY);
249        assert_eq!(fingerprint.value, CANARY);
250    }
251}