snap_dataplane/session/
state.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! SNAP data plane session state.
15
16use std::{
17    collections::BTreeMap,
18    time::{Duration, SystemTime, UNIX_EPOCH},
19};
20
21use ed25519_dalek::{
22    SigningKey,
23    pkcs8::{EncodePrivateKey, EncodePublicKey},
24};
25use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header};
26use pem::Pem;
27use rand::RngCore;
28use scion_sdk_common_types::ed25519::Ed25519SigningKeyPem;
29use serde::{Deserialize, Serialize};
30use snap_tokens::{Pssid, session_token::SessionTokenClaims};
31
32use super::manager::{SessionTokenError, TokenIssuer};
33use crate::state::{DataPlaneId, Id};
34
35pub mod dto;
36
37const DEFAULT_SESSION_DURATION: Duration = Duration::from_secs(3600); // 1 hour
38
39/// SNAP data plane session ID.
40#[derive(Debug, Ord, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Clone)]
41pub struct SessionId {
42    pssid: Pssid,
43    data_plane_id: DataPlaneId,
44}
45
46impl SessionId {
47    /// Creates a new SNAP data plane session ID.
48    pub fn new(pssid: Pssid, data_plane_id: DataPlaneId) -> Self {
49        Self {
50            pssid,
51            data_plane_id,
52        }
53    }
54}
55
56/// Manages data plane sessions.
57///
58/// A session is identified by the pseudo SCION subscriber identity (PSSID) from the SNAP token. At
59/// any point in time, there is at most one session open with a data plane per PSSID.
60///
61/// Note: We might need to weaken this constraint in the future to allow multiple session per data
62/// plane to allow session failover in case only a single data plane is running.
63#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
64pub struct SessionManagerState {
65    /// The maximum duration of a session.
66    session_duration: Duration,
67    /// The currently open sessions.
68    sessions: BTreeMap<SessionId, Session>,
69}
70
71impl SessionManagerState {
72    /// Creates a new session manager state with the given session duration.
73    pub fn new(session_duration: Duration) -> Self {
74        Self {
75            session_duration,
76            sessions: BTreeMap::new(),
77        }
78    }
79
80    /// Opens a new SNAP data plane session for the given PSSID and data plane ID.
81    // XXX(bunert): We allow multiple sessions per PSSID for now. Later we might want to
82    // disallow this when we properly remove sessions from terminated connections.
83    pub fn open(
84        &mut self,
85        pssid: Pssid,
86        data_plane_id: DataPlaneId,
87    ) -> Result<SessionGrant, SessionOpenError> {
88        let session_id = SessionId::new(pssid.clone(), data_plane_id);
89        let session_expiry = SystemTime::now() + self.session_duration;
90
91        // XXX(bunert): For now it does not matter if we create a new session or update an existing
92        // one.
93        let _res = self
94            .sessions
95            .insert(session_id, Session::new(session_expiry));
96
97        Ok(SessionGrant {
98            expiry: session_expiry,
99        })
100    }
101}
102
103/// Errors that can occur during session management.
104#[derive(Debug, thiserror::Error)]
105pub enum SessionOpenError {}
106
107impl Default for SessionManagerState {
108    fn default() -> Self {
109        Self::new(DEFAULT_SESSION_DURATION)
110    }
111}
112
113/// Grant for a session required to issue session tokens.
114pub struct SessionGrant {
115    // The expiration time of the session.
116    expiry: SystemTime,
117}
118
119/// Open data plane session.
120#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
121pub struct Session {
122    expiry: SystemTime,
123}
124
125impl Session {
126    fn new(expiry: SystemTime) -> Self {
127        Self { expiry }
128    }
129}
130
131/// Session token issuer state. Allows issuing session tokens for the opened data plane sessions
132/// depending on the SNAP token validity of the session.
133#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
134pub struct SessionTokenIssuerState {
135    /// The encoding key (PEM format) used to issue session tokens.
136    key: Ed25519SigningKeyPem,
137}
138
139impl SessionTokenIssuerState {
140    /// Creates a new session token issuer state with the given signing key.
141    pub fn new(key: Ed25519SigningKeyPem) -> Self {
142        Self { key }
143    }
144}
145
146impl TokenIssuer for SessionTokenIssuerState {
147    fn issue(
148        &self,
149        pssid: Pssid,
150        data_plane_id: DataPlaneId,
151        session_grant: SessionGrant,
152    ) -> Result<String, SessionTokenError> {
153        let claims = SessionTokenClaims {
154            pssid,
155            data_plane_id: data_plane_id.as_usize(),
156            exp: session_grant
157                .expiry
158                .duration_since(UNIX_EPOCH)
159                .unwrap()
160                .as_secs(),
161        };
162
163        let encoding_key = (&self.key).into();
164        let token = jsonwebtoken::encode(&Header::new(Algorithm::EdDSA), &claims, &encoding_key)
165            .map_err(SessionTokenError::EncodingError)?;
166        Ok(token)
167    }
168}
169
170/// Returns a session key pair for the given SNAP ID.
171///
172/// Note: This is only for testing purposes.
173pub fn insecure_const_session_key_pair(input: usize) -> (EncodingKey, DecodingKey) {
174    let (private_pem, public_pem) = insecure_const_session_key_pair_pem(input);
175
176    let encoding_key = EncodingKey::from_ed_pem(pem::encode(&private_pem).as_bytes()).unwrap();
177    let decoding_key = DecodingKey::from_ed_pem(pem::encode(&public_pem).as_bytes()).unwrap();
178
179    (encoding_key, decoding_key)
180}
181
182/// Returns a session key pair for the given SNAP ID in PEM format.
183///
184/// Note: This is only for testing purposes.
185pub fn insecure_const_session_key_pair_pem(input: usize) -> (Pem, Pem) {
186    let dalek_keypair = insecure_const_ed25519_signing_key(input);
187    let public_key =
188        ed25519_dalek::pkcs8::PublicKeyBytes(*dalek_keypair.verifying_key().as_bytes());
189
190    let kp = ed25519_dalek::pkcs8::KeypairBytes {
191        secret_key: *dalek_keypair.as_bytes(),
192        public_key: Some(public_key),
193    };
194
195    let private_pem = pem::Pem::new("PRIVATE KEY", kp.to_pkcs8_der().unwrap().as_bytes());
196
197    let public_pem = pem::Pem::new(
198        "PUBLIC KEY",
199        public_key.to_public_key_der().unwrap().as_bytes(),
200    );
201
202    (private_pem, public_pem)
203}
204
205/// Returns a seeded Ed25519 signing key.
206pub fn insecure_const_ed25519_signing_key(input: usize) -> SigningKey {
207    let mut seed = [43u8; 32];
208    let id_bytes = input.to_le_bytes();
209    seed[..id_bytes.len()].copy_from_slice(&id_bytes);
210
211    ed25519_dalek::SigningKey::from_bytes(&seed)
212}
213
214/// Returns a random Ed25519 signing key.
215pub fn random_ed25519_signing_key() -> SigningKey {
216    let mut trng = rand::rng();
217    let mut seed = [0u8; 32];
218    trng.fill_bytes(&mut seed[..]);
219
220    ed25519_dalek::SigningKey::from_bytes(&seed)
221}
222
223#[cfg(test)]
224mod tests {
225    use std::time::{Duration, UNIX_EPOCH};
226
227    use scion_sdk_token_validator::validator::{TokenValidator, Validator};
228    use snap_tokens::snap_token::SnapTokenClaims;
229    use test_log::test;
230    use uuid::Uuid;
231
232    use super::*;
233
234    #[test]
235    fn session_mgmt() {
236        let claims = SnapTokenClaims {
237            pssid: Pssid(Uuid::new_v4()),
238            exp: (SystemTime::now() + Duration::from_secs(360))
239                .duration_since(UNIX_EPOCH)
240                .unwrap()
241                .as_secs(),
242        };
243        let dp_id = DataPlaneId::from_usize(0);
244
245        let signing_key = insecure_const_ed25519_signing_key(0);
246        let signing_key = Ed25519SigningKeyPem::from(signing_key);
247        let decoding_key = signing_key.to_decoding_key();
248        let issuer = SessionTokenIssuerState::new(signing_key);
249
250        let mut session_manager = SessionManagerState::default();
251
252        let session_grant = session_manager.open(claims.pssid.clone(), dp_id).unwrap();
253
254        let session_token = issuer
255            .issue(claims.pssid.clone(), dp_id, session_grant)
256            .unwrap();
257
258        Validator::<SessionTokenClaims>::new(decoding_key, None)
259            .validate(SystemTime::now(), &session_token)
260            .expect("validation failed");
261
262        // Open another session with the same PSSID should succeed for now.
263        let _ = session_manager
264            .open(claims.pssid.clone(), dp_id)
265            .expect("open second session");
266    }
267}