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::{SessionManager, SessionOpenError, 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
81impl Default for SessionManagerState {
82    fn default() -> Self {
83        Self::new(DEFAULT_SESSION_DURATION)
84    }
85}
86
87/// Grant for a session required to issue session tokens.
88pub struct SessionGrant {
89    // The expiration time of the session.
90    expiry: SystemTime,
91}
92
93impl SessionManager for SessionManagerState {
94    // Opens a new SNAP data plane session for the given PSSID and data plane ID.
95    //
96    // XXX(bunert): We allow multiple sessions per PSSID for now. Later we might want to
97    // disallow this when we properly remove sessions from terminated connections.
98    fn open(
99        &mut self,
100        pssid: Pssid,
101        data_plane_id: DataPlaneId,
102    ) -> Result<SessionGrant, SessionOpenError> {
103        let session_id = SessionId::new(pssid.clone(), data_plane_id);
104        let session_expiry = SystemTime::now() + self.session_duration;
105
106        // XXX(bunert): For now it does not matter if we create a new session or update an existing
107        // one.
108        let _res = self
109            .sessions
110            .insert(session_id, Session::new(session_expiry));
111
112        Ok(SessionGrant {
113            expiry: session_expiry,
114        })
115    }
116}
117
118/// Open data plane session.
119#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
120pub struct Session {
121    expiry: SystemTime,
122}
123
124impl Session {
125    fn new(expiry: SystemTime) -> Self {
126        Self { expiry }
127    }
128}
129
130/// Session token issuer state. Allows issuing session tokens for the opened data plane sessions
131/// depending on the SNAP token validity of the session.
132#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
133pub struct SessionTokenIssuerState {
134    /// The encoding key (PEM format) used to issue session tokens.
135    key: Ed25519SigningKeyPem,
136}
137
138impl SessionTokenIssuerState {
139    /// Creates a new session token issuer state with the given signing key.
140    pub fn new(key: Ed25519SigningKeyPem) -> Self {
141        Self { key }
142    }
143}
144
145impl TokenIssuer for SessionTokenIssuerState {
146    fn issue(
147        &self,
148        pssid: Pssid,
149        data_plane_id: DataPlaneId,
150        session_grant: SessionGrant,
151    ) -> Result<String, SessionTokenError> {
152        let claims = SessionTokenClaims {
153            pssid,
154            data_plane_id: data_plane_id.as_usize(),
155            exp: session_grant
156                .expiry
157                .duration_since(UNIX_EPOCH)
158                .unwrap()
159                .as_secs(),
160        };
161
162        let encoding_key = (&self.key).into();
163        let token = jsonwebtoken::encode(&Header::new(Algorithm::EdDSA), &claims, &encoding_key)
164            .map_err(SessionTokenError::EncodingError)?;
165        Ok(token)
166    }
167}
168
169/// Returns a session key pair for the given SNAP ID.
170///
171/// Note: This is only for testing purposes.
172pub fn insecure_const_session_key_pair(input: usize) -> (EncodingKey, DecodingKey) {
173    let (private_pem, public_pem) = insecure_const_session_key_pair_pem(input);
174
175    let encoding_key = EncodingKey::from_ed_pem(pem::encode(&private_pem).as_bytes()).unwrap();
176    let decoding_key = DecodingKey::from_ed_pem(pem::encode(&public_pem).as_bytes()).unwrap();
177
178    (encoding_key, decoding_key)
179}
180
181/// Returns a session key pair for the given SNAP ID in PEM format.
182///
183/// Note: This is only for testing purposes.
184pub fn insecure_const_session_key_pair_pem(input: usize) -> (Pem, Pem) {
185    let dalek_keypair = insecure_const_ed25519_signing_key(input);
186    let public_key =
187        ed25519_dalek::pkcs8::PublicKeyBytes(*dalek_keypair.verifying_key().as_bytes());
188
189    let kp = ed25519_dalek::pkcs8::KeypairBytes {
190        secret_key: *dalek_keypair.as_bytes(),
191        public_key: Some(public_key),
192    };
193
194    let private_pem = pem::Pem::new("PRIVATE KEY", kp.to_pkcs8_der().unwrap().as_bytes());
195
196    let public_pem = pem::Pem::new(
197        "PUBLIC KEY",
198        public_key.to_public_key_der().unwrap().as_bytes(),
199    );
200
201    (private_pem, public_pem)
202}
203
204/// Returns a seeded Ed25519 signing key.
205pub fn insecure_const_ed25519_signing_key(input: usize) -> SigningKey {
206    let mut seed = [43u8; 32];
207    let id_bytes = input.to_le_bytes();
208    seed[..id_bytes.len()].copy_from_slice(&id_bytes);
209
210    ed25519_dalek::SigningKey::from_bytes(&seed)
211}
212
213/// Returns a random Ed25519 signing key.
214pub fn random_ed25519_signing_key() -> SigningKey {
215    let mut trng = rand::rng();
216    let mut seed = [0u8; 32];
217    trng.fill_bytes(&mut seed[..]);
218
219    ed25519_dalek::SigningKey::from_bytes(&seed)
220}
221
222#[cfg(test)]
223mod tests {
224    use std::time::{Duration, UNIX_EPOCH};
225
226    use scion_sdk_token_validator::validator::{TokenValidator, Validator};
227    use snap_tokens::snap_token::SnapTokenClaims;
228    use test_log::test;
229    use uuid::Uuid;
230
231    use super::*;
232
233    #[test]
234    fn session_mgmt() {
235        let claims = SnapTokenClaims {
236            pssid: Pssid(Uuid::new_v4()),
237            exp: (SystemTime::now() + Duration::from_secs(360))
238                .duration_since(UNIX_EPOCH)
239                .unwrap()
240                .as_secs(),
241        };
242        let dp_id = DataPlaneId::from_usize(0);
243
244        let signing_key = insecure_const_ed25519_signing_key(0);
245        let signing_key = Ed25519SigningKeyPem::from(signing_key);
246        let decoding_key = signing_key.to_decoding_key();
247        let issuer = SessionTokenIssuerState::new(signing_key);
248
249        let mut session_manager = SessionManagerState::default();
250
251        let session_grant = session_manager.open(claims.pssid.clone(), dp_id).unwrap();
252
253        let session_token = issuer
254            .issue(claims.pssid.clone(), dp_id, session_grant)
255            .unwrap();
256
257        Validator::<SessionTokenClaims>::new(decoding_key, None)
258            .validate(SystemTime::now(), &session_token)
259            .expect("validation failed");
260
261        // Open another session with the same PSSID should succeed for now.
262        let _ = session_manager
263            .open(claims.pssid.clone(), dp_id)
264            .expect("open second session");
265    }
266}