Skip to main content

scll_core/scp/
mod.rs

1//! SCP version selection + session handle — PDD §4.3 / §5.9.
2//!
3//! Selection rule (§4.3): prefer SCP03 with an in-scope `i` (random or
4//! pseudo-random; see [`scp03::i_supported`]) — highest advertised — else
5//! SCP02; else `ScllError::ScpProtocolUnsupported`. Missing CRD → assume
6//! `Scp02 { i = 0x55 }` with a warning. A caller may override via
7//! `force_scp_version` (not recommended).
8//!
9//! (Unchanged by the v0.9 `no_std` conversion: `ScpSession` wraps the state
10//! types regardless of their internals; in v0.9 `scp03::Scp03State` /
11//! `scp02::Scp02State` carry the backend session handle — see `scp/scp03.rs`,
12//! `scp/scp02.rs`. `select` is alloc-free.)
13
14pub mod scp02;
15pub mod scp03;
16
17use crate::model::ScpVariant;
18
19/// Open secure-channel handle, protocol-tagged (PDD §5.9 return type).
20pub enum ScpSession {
21    Scp03(scp03::Scp03State),
22    Scp02(scp02::Scp02State),
23}
24
25impl ScpSession {
26    /// The effective (capped) security level fixed at channel open (§4.1).
27    #[must_use]
28    pub fn security_level(&self) -> u8 {
29        match self {
30            ScpSession::Scp03(s) => s.security_level(),
31            ScpSession::Scp02(s) => s.security_level(),
32        }
33    }
34
35    /// The negotiated SCP `i` parameter.
36    #[must_use]
37    pub fn i_param(&self) -> u8 {
38        match self {
39            ScpSession::Scp03(s) => s.i_param(),
40            ScpSession::Scp02(s) => s.i_param(),
41        }
42    }
43
44    /// The key version number the card authenticated with at channel open.
45    #[must_use]
46    pub fn kvn(&self) -> u8 {
47        match self {
48            ScpSession::Scp03(s) => s.kvn(),
49            ScpSession::Scp02(s) => s.kvn(),
50        }
51    }
52
53    /// Which SCP protocol this session runs (for report population, §7).
54    #[must_use]
55    pub fn protocol(&self) -> crate::report::ScpProtocol {
56        match self {
57            ScpSession::Scp03(_) => crate::report::ScpProtocol::Scp03,
58            ScpSession::Scp02(_) => crate::report::ScpProtocol::Scp02,
59        }
60    }
61
62    /// A stable, deterministic session identifier derived from the backend
63    /// session-slot index (§7 reports carry no timestamp/version, §10.3).
64    #[must_use]
65    pub fn session_id(&self) -> u64 {
66        match self {
67            ScpSession::Scp03(s) => u64::from(s.session().index()),
68            ScpSession::Scp02(s) => u64::from(s.session().index()),
69        }
70    }
71}
72
73/// Apply the §4.3 selection rule to the card's advertised variants: a caller
74/// `force` override wins; otherwise prefer SCP03 with an in-scope `i` (random
75/// or pseudo-random; see [`scp03::i_supported`]) — highest advertised — else
76/// the first SCP02 variant, else `None`. (The "missing CRD → assume
77/// `Scp02 { i = 0x55 }` with a warning" fallback is the workflow's job — it
78/// applies before calling this with a non-empty list.)
79#[must_use]
80pub fn select(advertised: &[ScpVariant], force: Option<ScpVariant>) -> Option<ScpVariant> {
81    if let Some(forced) = force {
82        return Some(forced);
83    }
84    // Prefer SCP03; among in-scope `i` values pick the highest advertised.
85    let best_scp03 = advertised
86        .iter()
87        .filter_map(|v| match v {
88            ScpVariant::Scp03 { i_param } if scp03::i_supported(*i_param) => Some(*i_param),
89            _ => None,
90        })
91        .max();
92    if let Some(i_param) = best_scp03 {
93        return Some(ScpVariant::Scp03 { i_param });
94    }
95    // Otherwise the first advertised SCP02 variant.
96    advertised.iter().find_map(|v| match v {
97        ScpVariant::Scp02 { i_param } => Some(ScpVariant::Scp02 { i_param: *i_param }),
98        ScpVariant::Scp03 { .. } => None,
99    })
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn prefers_highest_scp03() {
108        let adv = [
109            ScpVariant::Scp02 { i_param: 0x55 },
110            ScpVariant::Scp03 { i_param: 0x30 },
111            ScpVariant::Scp03 { i_param: 0x70 },
112        ];
113        assert_eq!(
114            select(&adv, None),
115            Some(ScpVariant::Scp03 { i_param: 0x70 })
116        );
117    }
118
119    #[test]
120    fn falls_back_to_scp02_when_no_scp03() {
121        let adv = [ScpVariant::Scp02 { i_param: 0x55 }];
122        assert_eq!(
123            select(&adv, None),
124            Some(ScpVariant::Scp02 { i_param: 0x55 })
125        );
126    }
127
128    #[test]
129    fn ignores_out_of_scope_scp03_i() {
130        // An SCP03 i with an out-of-scope bit (0x04 = RFU) is not selectable;
131        // SCP02 wins instead. (S8/S16 × random/pseudo configs ARE in scope.)
132        let adv = [
133            ScpVariant::Scp03 { i_param: 0x04 },
134            ScpVariant::Scp02 { i_param: 0x15 },
135        ];
136        assert_eq!(
137            select(&adv, None),
138            Some(ScpVariant::Scp02 { i_param: 0x15 })
139        );
140    }
141
142    #[test]
143    fn selects_s16_scp03() {
144        // S16 SCP03 (0x78) is in scope and preferred over SCP02.
145        let adv = [
146            ScpVariant::Scp02 { i_param: 0x55 },
147            ScpVariant::Scp03 { i_param: 0x78 },
148        ];
149        assert_eq!(
150            select(&adv, None),
151            Some(ScpVariant::Scp03 { i_param: 0x78 })
152        );
153    }
154
155    #[test]
156    fn selects_random_challenge_scp03() {
157        // Random-challenge SCP03 (0x60) is in scope and preferred over SCP02.
158        let adv = [
159            ScpVariant::Scp02 { i_param: 0x55 },
160            ScpVariant::Scp03 { i_param: 0x60 },
161        ];
162        assert_eq!(
163            select(&adv, None),
164            Some(ScpVariant::Scp03 { i_param: 0x60 })
165        );
166    }
167
168    #[test]
169    fn force_overrides_selection() {
170        let adv = [ScpVariant::Scp03 { i_param: 0x70 }];
171        let forced = ScpVariant::Scp02 { i_param: 0x55 };
172        assert_eq!(select(&adv, Some(forced)), Some(forced));
173    }
174
175    #[test]
176    fn empty_yields_none() {
177        assert_eq!(select(&[], None), None);
178    }
179}