Skip to main content

dvb_ci_runtime/
session.rs

1//! SPDU session layer — a sans-IO mechanism over the transport layer
2//! (ETSI EN 50221 §7.2).
3//!
4//! Multiplexes logical sessions (one per resource in use) over the transport
5//! connection: allocates/tracks `session_nb`s, answers `open_session_request`
6//! for resources the host advertises, opens `create_session` on demand, and
7//! routes `session_number`+APDU to/from the resource bound to a session. It is
8//! mechanism only — *which* resources the host provides is the caller's policy,
9//! supplied as the `provides` predicate to [`SessionLayer::on_spdu`].
10
11use std::collections::BTreeMap;
12
13use dvb_ci::resource::ResourceId;
14use dvb_ci::spdu::{
15    tags, CloseSessionRequest, CloseSessionResponse, CreateSessionResponse,
16    OpenSessionRequest, OpenSessionResponse, SessionNumber, SessionStatus,
17};
18use dvb_common::{Parse, Serialize};
19
20fn ser<S: Serialize>(s: &S) -> Vec<u8> {
21    let mut b = vec![0u8; s.serialized_len()];
22    // The buffer is sized to `serialized_len()`, so serialization cannot fail;
23    // matched (not `expect`ed) to avoid a `Debug` bound on `S::Error`.
24    match s.serialize_into(&mut b) {
25        Ok(n) => b.truncate(n),
26        Err(_) => b.clear(),
27    }
28    b
29}
30
31/// What the session layer wants done after handling one SPDU.
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
33pub struct SessionOut {
34    /// SPDUs to hand down to the transport layer (each becomes a `T_Data_Last`).
35    pub spdus: Vec<Vec<u8>>,
36    /// `(session_nb, apdu_bytes)` to pass up to the resource layer.
37    pub apdus: Vec<(u16, Vec<u8>)>,
38    /// Sessions newly opened (`session_nb`, bound resource).
39    pub opened: Vec<(u16, ResourceId)>,
40    /// `session_nb`s that closed.
41    pub closed: Vec<u16>,
42}
43
44/// The session table + `session_nb` allocator.
45#[derive(Debug, Default)]
46pub struct SessionLayer {
47    sessions: BTreeMap<u16, ResourceId>,
48    next: u16,
49}
50
51impl SessionLayer {
52    /// New, empty session layer.
53    #[must_use]
54    pub fn new() -> Self {
55        Self {
56            sessions: BTreeMap::new(),
57            next: 1, // session_nb 0 is reserved
58        }
59    }
60
61    /// Resource bound to `session_nb`, if open.
62    #[must_use]
63    pub fn resource_of(&self, session_nb: u16) -> Option<ResourceId> {
64        self.sessions.get(&session_nb).copied()
65    }
66
67    /// All open `(session_nb, resource)` pairs, ascending by `session_nb`.
68    #[must_use]
69    pub fn sessions(&self) -> Vec<(u16, ResourceId)> {
70        self.sessions.iter().map(|(&n, &r)| (n, r)).collect()
71    }
72
73    /// Number of open sessions.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.sessions.len()
77    }
78
79    /// Whether there are no open sessions.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.sessions.is_empty()
83    }
84
85    fn alloc(&mut self) -> u16 {
86        let nb = self.next;
87        self.next = self.next.checked_add(1).filter(|&n| n != 0).unwrap_or(1);
88        nb
89    }
90
91    /// Open a session to a **module-provided** resource (host-initiated):
92    /// returns the `open_session_request` SPDU to send. Sessions are opened the
93    /// same way in both directions (§8.4.1) — the host sends
94    /// `open_session_request`, and the module (the resource provider) assigns the
95    /// `session_nb` in its `open_session_response`. (`create_session`/0x93 is a
96    /// resource-manager-internal primitive; a real CAM rejects it with
97    /// `status=0xF0` — verified live against an AlphaCrypt.) The session is
98    /// recorded once the module's `open_session_response(ok)` arrives.
99    pub fn create_session(&mut self, resource: ResourceId) -> Vec<u8> {
100        ser(&OpenSessionRequest { resource })
101    }
102
103    /// Wrap an APDU for sending on `session_nb` (`session_number` + body).
104    #[must_use]
105    pub fn send_apdu(&self, session_nb: u16, apdu: &[u8]) -> Vec<u8> {
106        let mut v = ser(&SessionNumber { session_nb });
107        v.extend_from_slice(apdu);
108        v
109    }
110
111    /// Begin closing `session_nb`: returns the `close_session_request` SPDU.
112    pub fn close(&mut self, session_nb: u16) -> Vec<u8> {
113        self.sessions.remove(&session_nb);
114        ser(&CloseSessionRequest { session_nb })
115    }
116
117    /// Handle one inbound SPDU. `provides` answers "does the host provide this
118    /// resource?" for an incoming `open_session_request`.
119    pub fn on_spdu(&mut self, spdu: &[u8], provides: impl Fn(ResourceId) -> bool) -> SessionOut {
120        let mut out = SessionOut::default();
121        match spdu.first().copied() {
122            // Module wants a host-provided resource.
123            Some(tags::OPEN_SESSION_REQUEST) => {
124                if let Ok(req) = OpenSessionRequest::parse(spdu) {
125                    if provides(req.resource) {
126                        let session_nb = self.alloc();
127                        self.sessions.insert(session_nb, req.resource);
128                        out.spdus.push(ser(&OpenSessionResponse {
129                            status: SessionStatus::Ok,
130                            resource: req.resource,
131                            session_nb,
132                        }));
133                        out.opened.push((session_nb, req.resource));
134                    } else {
135                        out.spdus.push(ser(&OpenSessionResponse {
136                            status: SessionStatus::ResourceNonExistent,
137                            resource: req.resource,
138                            session_nb: 0,
139                        }));
140                    }
141                }
142            }
143            // Module's reply to our open_session_request (host opened a
144            // module-provided resource); the module assigns the session_nb.
145            Some(tags::OPEN_SESSION_RESPONSE) => {
146                if let Ok(resp) = OpenSessionResponse::parse(spdu) {
147                    if resp.status == SessionStatus::Ok {
148                        self.sessions.insert(resp.session_nb, resp.resource);
149                        out.opened.push((resp.session_nb, resp.resource));
150                    }
151                }
152            }
153            // (Legacy) module's reply to a create_session, if any module uses it.
154            Some(tags::CREATE_SESSION_RESPONSE) => {
155                if let Ok(resp) = CreateSessionResponse::parse(spdu) {
156                    if resp.status == SessionStatus::Ok {
157                        self.sessions.insert(resp.session_nb, resp.resource);
158                        out.opened.push((resp.session_nb, resp.resource));
159                    }
160                }
161            }
162            // Peer closes a session.
163            Some(tags::CLOSE_SESSION_REQUEST) => {
164                if let Ok(req) = CloseSessionRequest::parse(spdu) {
165                    self.sessions.remove(&req.session_nb);
166                    out.spdus.push(ser(&CloseSessionResponse {
167                        status: SessionStatus::Ok,
168                        session_nb: req.session_nb,
169                    }));
170                    out.closed.push(req.session_nb);
171                }
172            }
173            // Ack of a close we initiated.
174            Some(tags::CLOSE_SESSION_RESPONSE) => {
175                if let Ok(resp) = CloseSessionResponse::parse(spdu) {
176                    self.sessions.remove(&resp.session_nb);
177                    out.closed.push(resp.session_nb);
178                }
179            }
180            // Data: session_number(nb) + APDU body.
181            Some(tags::SESSION_NUMBER) => {
182                if let Ok(sn) = SessionNumber::parse(spdu) {
183                    if spdu.len() > SessionNumber::HEADER_LEN {
184                        out.apdus
185                            .push((sn.session_nb, spdu[SessionNumber::HEADER_LEN..].to_vec()));
186                    }
187                }
188            }
189            _ => {}
190        }
191        out
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use dvb_ci::resource::{APPLICATION_INFORMATION, RESOURCE_MANAGER};
199
200    fn provides_rm(r: ResourceId) -> bool {
201        r == RESOURCE_MANAGER
202    }
203
204    #[test]
205    fn open_request_for_provided_resource_grants_and_tracks() {
206        let mut s = SessionLayer::new();
207        let req = ser(&OpenSessionRequest {
208            resource: RESOURCE_MANAGER,
209        });
210        let out = s.on_spdu(&req, provides_rm);
211        assert_eq!(out.opened.len(), 1);
212        let (nb, res) = out.opened[0];
213        assert_eq!(res, RESOURCE_MANAGER);
214        assert_eq!(s.resource_of(nb), Some(RESOURCE_MANAGER));
215        // reply is an open_session_response with status ok
216        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
217        assert_eq!(resp.status, SessionStatus::Ok);
218        assert_eq!(resp.session_nb, nb);
219    }
220
221    #[test]
222    fn open_request_for_absent_resource_denied() {
223        let mut s = SessionLayer::new();
224        let req = ser(&OpenSessionRequest {
225            resource: APPLICATION_INFORMATION,
226        });
227        let out = s.on_spdu(&req, provides_rm);
228        assert!(out.opened.is_empty());
229        let resp = OpenSessionResponse::parse(&out.spdus[0]).unwrap();
230        assert_eq!(resp.status, SessionStatus::ResourceNonExistent);
231        assert!(s.is_empty());
232    }
233
234    #[test]
235    fn create_session_tracked_on_ok_response() {
236        let mut s = SessionLayer::new();
237        let _spdu = s.create_session(APPLICATION_INFORMATION);
238        // module replies ok for session 1
239        let resp = ser(&CreateSessionResponse {
240            status: SessionStatus::Ok,
241            resource: APPLICATION_INFORMATION,
242            session_nb: 1,
243        });
244        let out = s.on_spdu(&resp, |_| false);
245        assert_eq!(out.opened, vec![(1, APPLICATION_INFORMATION)]);
246        assert_eq!(s.resource_of(1), Some(APPLICATION_INFORMATION));
247    }
248
249    #[test]
250    fn session_number_routes_apdu_up() {
251        let mut s = SessionLayer::new();
252        let apdu = [0x9F, 0x80, 0x21, 0x00];
253        let mut spdu = ser(&SessionNumber { session_nb: 7 });
254        spdu.extend_from_slice(&apdu);
255        let out = s.on_spdu(&spdu, |_| false);
256        assert_eq!(out.apdus, vec![(7, apdu.to_vec())]);
257    }
258
259    #[test]
260    fn close_request_acks_and_removes() {
261        let mut s = SessionLayer::new();
262        // open one first
263        let req = ser(&OpenSessionRequest {
264            resource: RESOURCE_MANAGER,
265        });
266        let nb = s.on_spdu(&req, provides_rm).opened[0].0;
267        // peer closes it
268        let close = ser(&CloseSessionRequest { session_nb: nb });
269        let out = s.on_spdu(&close, |_| false);
270        assert_eq!(out.closed, vec![nb]);
271        assert!(s.is_empty());
272        // reply is a close_session_response
273        assert_eq!(out.spdus[0][0], tags::CLOSE_SESSION_RESPONSE);
274    }
275
276    #[test]
277    fn send_apdu_prefixes_session_number() {
278        let s = SessionLayer::new();
279        let wire = s.send_apdu(3, &[0xAA, 0xBB]);
280        let sn = SessionNumber::parse(&wire).unwrap();
281        assert_eq!(sn.session_nb, 3);
282        assert_eq!(&wire[SessionNumber::HEADER_LEN..], &[0xAA, 0xBB]);
283    }
284}