Skip to main content

scll_core/command/
rmac_session.rs

1//! BEGIN / END R-MAC SESSION (CLA 80, INS 7A / 78) — SCP02 response integrity.
2//!
3//! These frame an SCP02 R-MAC session: `BEGIN R-MAC SESSION` tells the card to
4//! start appending an R-MAC to each response, and `END R-MAC SESSION` retrieves
5//! (and optionally ends) the accumulated R-MAC. They build the **plaintext**
6//! C-APDU only; the session/backend applies `CLA | 0x04` + C-MAC afterwards (the
7//! commands must themselves be sent inside the secure channel).
8//!
9//! Coding (GPCS v2.3.1 Appendix E; the parameter tables mirror Amendment D
10//! §7.x for SCP03, and are cross-checked byte-for-byte against the `skythen/scp02`
11//! Go reference, `beginRMACSession`/`EndRMACSession`):
12//!   * BEGIN: INS `0x7A`, P1 `0x10` (each response carries an R-MAC — for SCP02
13//!     which has no response encryption, `0x30`/R-ENC does not apply), P2 `0x00`.
14//!     The data field is an LV-coded 'data' element (`len ‖ data`). The card does
15//!     not interpret 'data' but folds it into the R-MAC, letting the host inject
16//!     a challenge; the LV total is ≤ 25 bytes, so `data` is `1..=24` bytes.
17//!   * END: INS `0x78`, P1 `0x00`, P2 `0x03` (end the session **and** return the
18//!     R-MAC) or `0x01` (return the current R-MAC without ending). No data field;
19//!     `Le = 00` requests the 8-byte R-MAC in the response.
20
21use crate::command::{build, push_lv, BuildError, Capdu};
22
23/// P1: begin an R-MAC session in which each response carries an R-MAC.
24const BEGIN_P1_RMAC: u8 = 0x10;
25/// P2: end the R-MAC session and return the accumulated R-MAC.
26const END_P2_END_AND_RETURN: u8 = 0x03;
27/// P2: return the current R-MAC without ending the session.
28const END_P2_RETURN_ONLY: u8 = 0x01;
29
30/// Build a `BEGIN R-MAC SESSION` (CLA 80, INS 7A) plaintext C-APDU.
31///
32/// `data` is the caller's optional R-MAC challenge, sent LV-coded (`len ‖ data`)
33/// and folded into the card's R-MAC. It must be `1..=24` bytes.
34///
35/// # Errors
36/// [`BuildError::Overflow`] if `data` is empty or longer than 24 bytes.
37#[allow(clippy::module_name_repetitions)] // GP command name; intentional public API
38pub fn begin_rmac_session(data: &[u8]) -> Result<Capdu, BuildError> {
39    if data.is_empty() || data.len() > 24 {
40        return Err(BuildError::Overflow);
41    }
42    let mut field = Capdu::new();
43    push_lv(&mut field, data)?; // 'data' element: len ‖ data
44    build(0x80, 0x7A, BEGIN_P1_RMAC, 0x00, &field, true)
45}
46
47/// Build an `END R-MAC SESSION` (CLA 80, INS 78) plaintext C-APDU. `end_session`
48/// selects P2 `0x03` (end and return the R-MAC) vs `0x01` (return only). The
49/// response returns the 8-byte R-MAC, so `Le = 00` is present.
50#[must_use]
51#[allow(clippy::module_name_repetitions)] // GP command name; intentional public API
52pub fn end_rmac_session(end_session: bool) -> Capdu {
53    let p2 = if end_session {
54        END_P2_END_AND_RETURN
55    } else {
56        END_P2_RETURN_ONLY
57    };
58    // 80 78 00 P2 00 — four-byte header + Le, no data field. Five bytes is far
59    // under CAPDU_MAX, so the push cannot fail (no-panic invariant, §10.5).
60    let mut apdu = Capdu::new();
61    let _ = apdu.extend_from_slice(&[0x80, 0x78, 0x00, p2, 0x00]);
62    apdu
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use scll_test_util::HexSlice;
69
70    #[test]
71    fn begin_wraps_data_lv_with_p1_10() {
72        // 80 7A 10 00 | Lc=04 | LV(03 A1 B2 C3) | Le=00
73        let apdu = begin_rmac_session(&[0xA1, 0xB2, 0xC3]).unwrap();
74        assert_eq!(
75            HexSlice(&apdu),
76            HexSlice([0x80, 0x7A, 0x10, 0x00, 0x04, 0x03, 0xA1, 0xB2, 0xC3, 0x00])
77        );
78    }
79
80    #[test]
81    fn begin_rejects_empty_and_oversized_data() {
82        assert_eq!(begin_rmac_session(&[]), Err(BuildError::Overflow));
83        assert_eq!(begin_rmac_session(&[0u8; 25]), Err(BuildError::Overflow));
84        // Boundary: 24 data bytes → 25-byte LV, still valid.
85        assert!(begin_rmac_session(&[0u8; 24]).is_ok());
86    }
87
88    #[test]
89    fn end_p2_selects_end_vs_return_only() {
90        // End + return: 80 78 00 03 00
91        assert_eq!(
92            HexSlice(&end_rmac_session(true)),
93            HexSlice([0x80, 0x78, 0x00, 0x03, 0x00])
94        );
95        // Return only: 80 78 00 01 00
96        assert_eq!(
97            HexSlice(&end_rmac_session(false)),
98            HexSlice([0x80, 0x78, 0x00, 0x01, 0x00])
99        );
100    }
101}