Skip to main content

scll_core/backend/
scp03.rs

1//! `Scp03Backend` — SCP03, Amendment D v1.1.2. PDD §3.3 / §4.1.
2//!
3//! `no_std`: `wrap`/`unwrap`/`encrypt_put_key_payload` return bounded
4//! `heapless::Vec` (a wrapped C-APDU is still ≤ `CAPDU_MAX`; a stripped R-APDU
5//! ≤ `RAPDU_MAX`; an encrypted key block ≤ `ENC_KEY_BLOCK_MAX`).
6
7use heapless::Vec;
8
9use crate::backend::key::{KeyBackend, KeyHandle};
10use crate::error::BackendError;
11use crate::limits::{CAPDU_MAX, ENC_KEY_BLOCK_MAX, RAPDU_MAX, SCP03_S16_MAX};
12
13/// SCP03 secure-channel size mode (Amendment D §5.1 Table 5-1, bit b4 `0x08`):
14/// `S8` uses 8-byte challenges/cryptograms and 8-byte (truncated) MACs (legacy);
15/// `S16` (Amendment D v1.2) uses 16-byte challenges/cryptograms and full 16-byte
16/// MACs. The MAC *chaining* value is 16 bytes in both modes.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ScpMode {
19    /// 8-byte fields, 8-byte truncated MACs.
20    S8,
21    /// 16-byte fields, full 16-byte MACs (Amendment D v1.2).
22    S16,
23}
24
25impl ScpMode {
26    /// Derive the mode from the SCP03 `i` parameter (bit b4 `0x08` ⇒ S16).
27    #[must_use]
28    pub const fn from_i(i_param: u8) -> Self {
29        if i_param & 0x08 != 0 {
30            Self::S16
31        } else {
32            Self::S8
33        }
34    }
35    /// Challenge / cryptogram length in bytes (8 or 16).
36    #[must_use]
37    pub const fn field_len(self) -> usize {
38        match self {
39            Self::S8 => 8,
40            Self::S16 => 16,
41        }
42    }
43    /// Length of the MAC appended to commands/responses: 8 bytes (truncated) in
44    /// S8, the full 16 bytes in S16 (Amendment D §6.2.4/§6.2.5).
45    #[must_use]
46    pub const fn mac_len(self) -> usize {
47        self.field_len()
48    }
49    /// KDF "L" (derived-data length in bits) for challenges and cryptograms:
50    /// `0x0040` (64) in S8, `0x0080` (128) in S16 (Amendment D §6.2.2).
51    #[must_use]
52    pub const fn l_bits(self) -> u16 {
53        match self {
54            Self::S8 => 0x0040,
55            Self::S16 => 0x0080,
56        }
57    }
58}
59
60/// Backend-defined opaque SCP03 session: an **index into the backend's session
61/// table**, which holds the derived S-ENC/S-MAC/S-RMAC, MAC chaining value and
62/// fixed security level. Opaque to callers; never exposes key material. The
63/// `new`/`index` accessors are backend-facing.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Scp03Session(u16);
66
67impl Scp03Session {
68    /// Construct from a backend session-slot index.
69    #[must_use]
70    pub const fn new(index: u16) -> Self {
71        Self(index)
72    }
73    /// The backend session-slot index.
74    #[must_use]
75    pub const fn index(self) -> u16 {
76        self.0
77    }
78}
79
80/// SCP03 crypto flow. Required by the default card manager (§3.6).
81pub trait Scp03Backend: KeyBackend {
82    /// Derive the SCP03 session keys (S-ENC/S-MAC/S-RMAC) from the static keys
83    /// and the host/card challenges, returning an opaque [`Scp03Session`]. The
84    /// `mode` (S8/S16) is recorded in the session and fixes the MAC width and
85    /// cryptogram length for its lifetime. `host_challenge`/`card_challenge` are
86    /// `mode.field_len()` bytes (8 or 16).
87    ///
88    /// # Errors
89    /// Returns [`BackendError::Crypto`] if key derivation fails or a challenge
90    /// length does not match `mode`, or [`BackendError::KeyGen`] if no session
91    /// slot is free.
92    fn scp03_derive_session(
93        &self,
94        static_enc: &KeyHandle,
95        static_mac: &KeyHandle,
96        mode: ScpMode,
97        host_challenge: &[u8],
98        card_challenge: &[u8],
99    ) -> Result<Scp03Session, BackendError>;
100
101    /// Compute the expected card cryptogram for verification (8 or 16 bytes per
102    /// the session's mode).
103    ///
104    /// # Errors
105    /// Returns [`BackendError::Crypto`] if the session is invalid or the MAC
106    /// computation fails.
107    fn scp03_card_cryptogram(
108        &self,
109        s: &Scp03Session,
110        host_ch: &[u8],
111        card_ch: &[u8],
112    ) -> Result<Vec<u8, SCP03_S16_MAX>, BackendError>;
113
114    /// Compute the host cryptogram for EXTERNAL AUTHENTICATE (8 or 16 bytes per
115    /// the session's mode).
116    ///
117    /// # Errors
118    /// Returns [`BackendError::Crypto`] if the session is invalid or the MAC
119    /// computation fails.
120    fn scp03_host_cryptogram(
121        &self,
122        s: &Scp03Session,
123        host_ch: &[u8],
124        card_ch: &[u8],
125    ) -> Result<Vec<u8, SCP03_S16_MAX>, BackendError>;
126
127    /// Recompute the expected **pseudo-random** card challenge for verification
128    /// (Amendment D §6.2.2.1): KDF keyed by the static `Key-ENC`, derivation
129    /// constant `0x02`, `L = mode.l_bits()`, context = `seq_counter ‖
130    /// invoker_aid`. Returns 8 or 16 bytes per `mode`. The caller compares it
131    /// (constant-time) to the card challenge from the IU response when a
132    /// sequence counter is present.
133    ///
134    /// # Errors
135    /// Returns [`BackendError::Crypto`] if the handle is invalid or derivation
136    /// fails.
137    fn scp03_pseudo_card_challenge(
138        &self,
139        static_enc: &KeyHandle,
140        mode: ScpMode,
141        seq_counter: &[u8; 3],
142        invoker_aid: &[u8],
143    ) -> Result<Vec<u8, SCP03_S16_MAX>, BackendError>;
144
145    // No `level` arg: the session fixes its security level at open (§4.1).
146    /// Apply the session security level (C-MAC, optional C-ENC) to `capdu`.
147    ///
148    /// # Errors
149    /// Returns [`BackendError::Crypto`] if the session is invalid or wrapping
150    /// fails.
151    fn scp03_wrap_command(
152        &self,
153        s: &mut Scp03Session,
154        capdu: &[u8],
155    ) -> Result<Vec<u8, CAPDU_MAX>, BackendError>;
156
157    /// Verify and strip the R-MAC/R-ENC from `rapdu`.
158    ///
159    /// # Errors
160    /// Returns [`BackendError::Crypto`] if R-MAC verification fails or the
161    /// session is invalid.
162    fn scp03_unwrap_response(
163        &self,
164        s: &mut Scp03Session,
165        rapdu: &[u8],
166    ) -> Result<Vec<u8, RAPDU_MAX>, BackendError>;
167
168    // New key is a HANDLE, not bytes (HSM-wrappable, no host plaintext).
169    // SCP03 uses the static DEK (Amendment D §6.2.6).
170    /// Encrypt `new_key` under the static DEK for a PUT KEY payload
171    /// (Amendment D §6.2.6).
172    ///
173    /// # Errors
174    /// Returns [`BackendError::Crypto`] if encryption fails, or
175    /// [`BackendError::Unsupported`] if a handle is not encryptable.
176    fn scp03_encrypt_put_key_payload(
177        &self,
178        dek: &KeyHandle,
179        new_key: &KeyHandle,
180    ) -> Result<Vec<u8, ENC_KEY_BLOCK_MAX>, BackendError>;
181
182    /// Release the backend session slot held by `s`, zeroizing its session
183    /// keys and MAC chaining value. Called by the card manager on channel
184    /// close so a long-lived backend does not leak slots across many
185    /// open/close cycles (PDD §3.6 / §4.1). The default is a no-op, so
186    /// stateless or stub backends that keep no slot table need not override it.
187    fn scp03_close_session(&self, _s: Scp03Session) {}
188}