Skip to main content

scll_core/workflow/
open_scp.rs

1//! Step 9 — `open_scp` (PDD §5.9).
2//!
3//! Negotiates SCP version per §4.3 (SCP03 preferred) over the card's advertised
4//! variants, SELECTs the target, runs INITIALIZE UPDATE → `scp::begin` →
5//! EXTERNAL AUTHENTICATE, and yields the open [`ScpSession`] inside the report.
6//! Hard-fails on EXTERNAL AUTHENTICATE rejection (no retry). For direct SD
7//! targeting SELECT targets the SD; cooperative-applet routing (the applet
8//! forwards IU/EA to its SD) is requested via [`ScpTargetKind::ApplicationAid`].
9
10use heapless::Vec;
11
12use crate::aid::Aid;
13use crate::backend::{KeyBackend, KeyHandle, Scp02Backend, Scp03Backend, ScpMode};
14use crate::error::ScllError;
15use crate::limits::SCP03_S16_MAX;
16use crate::model::ScpVariant;
17use crate::report::{OpenScpParams, OpenScpReport, ScpTargetKind};
18use crate::scp::{self, scp02, scp03, ScpSession};
19use crate::transport::Transport;
20use crate::workflow::session::{self, SW_OK};
21
22/// SCP02 INITIALIZE UPDATE key identifier (P2). `0x00` selects the keyset's
23/// default key set version (PDD §5.9 SCP02 step 4).
24const SCP02_KEY_ID: u8 = 0x00;
25
26/// The three static SD keys (ENC/MAC/DEK) as opaque backend handles. Bytes
27/// never cross this boundary (§3.6).
28#[derive(Debug, Clone, Copy)]
29pub struct SdKeys {
30    pub enc: KeyHandle,
31    pub mac: KeyHandle,
32    pub dek: KeyHandle,
33}
34
35/// Inputs to [`open_scp`]. `advertised` is the card's SCP list (from
36/// `discover_card`); `force_scp` overrides the §4.3 selection (not
37/// recommended). `kvn = 0x00` lets the card pick; `requested_level` is the
38/// security level to request (capped to the card's `i`, §5.9 step 8).
39pub struct OpenScpArgs<'a> {
40    pub target_aid: &'a [u8],
41    pub target_kind: ScpTargetKind,
42    pub sd_keys: SdKeys,
43    pub advertised: &'a [ScpVariant],
44    pub force_scp: Option<ScpVariant>,
45    pub kvn: u8,
46    pub requested_level: u8,
47}
48
49/// Open an SCP03/SCP02 secure channel; the [`OpenScpReport`] carries the
50/// session as its payload.
51///
52/// # Errors
53/// [`ScllError::ScpProtocolUnsupported`] if no supported variant is available,
54/// [`ScllError::CardCryptogramFail`] if the card cryptogram does not verify,
55/// [`ScllError::ExternalAuthFail`] if EXTERNAL AUTHENTICATE is rejected, or a
56/// transport / backend / [`ScllError::Card`] error.
57#[allow(clippy::similar_names)] // iu_capdu / iu_data are the IU command vs response
58pub fn open_scp<B>(
59    t: &mut dyn Transport,
60    backend: &B,
61    args: &OpenScpArgs<'_>,
62) -> Result<OpenScpReport, ScllError>
63where
64    B: KeyBackend + Scp02Backend + Scp03Backend,
65{
66    let variant =
67        scp::select(args.advertised, args.force_scp).ok_or(ScllError::ScpProtocolUnsupported)?;
68
69    // SELECT the target (SD for direct targeting; applet for cooperative routing).
70    let fci = session::select(t, args.target_aid)?;
71
72    // §6.2.2.1: the card derives the pseudo-random card challenge over the
73    // application's *full* registered AID. A caller may SELECT with a partial
74    // (RID-only / truncated) AID, which would make the recomputed challenge —
75    // and thus the defence-in-depth check in `scp03::begin` — fail spuriously.
76    // Prefer the DF name (tag `'84'`) the card echoes in the FCI `6F` template
77    // (GPCS v2.3.1 §11.1.4 / ISO/IEC 7816-4 §7.4.3.3); fall back to the SELECT
78    // target only when the FCI omits it or is not a valid AID. A malformed FCI
79    // is best-effort: it degrades to the fallback rather than aborting the open.
80    let target_aid = Aid::new(args.target_aid)?;
81    let invoker_aid = session::fci_df_name(&fci)
82        .ok()
83        .flatten()
84        .and_then(|name| Aid::new(name).ok())
85        .unwrap_or_else(|| target_aid.clone());
86
87    let (session, kvn_effective, i_param_effective, security_level_effective) = match variant {
88        ScpVariant::Scp03 { i_param } => {
89            // Mode (S8/S16) fixes the host-challenge length (8 or 16 bytes).
90            let mode = ScpMode::from_i(i_param);
91            let mut host_buf = [0u8; SCP03_S16_MAX];
92            let host_len = mode.field_len();
93            backend.random_bytes(&mut host_buf[..host_len])?;
94            let host_challenge = &host_buf[..host_len];
95
96            let iu_capdu = scp03::iu_command(args.kvn, host_challenge)?;
97            let (iu_data, sw) = session::transmit_plain(t, &iu_capdu)?;
98            if sw != SW_OK {
99                return Err(ScllError::from_general_sw(sw));
100            }
101            let iu = scp03::parse_iu_response(&iu_data)?;
102            let (state, ea) = scp03::begin(
103                backend,
104                &args.sd_keys.enc,
105                &args.sd_keys.mac,
106                args.kvn,
107                args.requested_level,
108                host_challenge,
109                invoker_aid.as_bytes(), // full AID (FCI tag '84') for the §6.2.2.1 check
110                &iu_data,
111            )?;
112            let level = state.security_level();
113            let (_d, ea_sw) = session::transmit_plain(t, &ea)?;
114            if ea_sw != SW_OK {
115                return Err(ScllError::ExternalAuthFail { sw: ea_sw });
116            }
117            (ScpSession::Scp03(state), iu.kvn, iu.i_param, level)
118        }
119        ScpVariant::Scp02 { i_param } => {
120            // SCP02 always uses an 8-byte host challenge (GPCS §E).
121            let mut host_challenge = [0u8; 8];
122            backend.random_bytes(&mut host_challenge)?;
123            let iu_capdu = scp02::iu_command(args.kvn, SCP02_KEY_ID, &host_challenge)?;
124            let (iu_data, sw) = session::transmit_plain(t, &iu_capdu)?;
125            if sw != SW_OK {
126                return Err(ScllError::from_general_sw(sw));
127            }
128            let iu = scp02::parse_iu_response(&iu_data)?;
129            let (state, ea) = scp02::begin(
130                backend,
131                &args.sd_keys.enc,
132                &args.sd_keys.mac,
133                &args.sd_keys.dek,
134                i_param,
135                args.kvn,
136                args.requested_level,
137                &host_challenge,
138                &iu_data,
139            )?;
140            let level = state.security_level();
141            let (_d, ea_sw) = session::transmit_plain(t, &ea)?;
142            if ea_sw != SW_OK {
143                return Err(ScllError::ExternalAuthFail { sw: ea_sw });
144            }
145            (ScpSession::Scp02(state), iu.kvn, i_param, level)
146        }
147    };
148
149    let session_id = session.session_id();
150    let scp_protocol_effective = session.protocol();
151
152    Ok(OpenScpReport {
153        session,
154        effective: OpenScpParams {
155            target_aid: target_aid.clone(),
156            target_kind: args.target_kind,
157            sd_aid_used_for_keys: target_aid,
158            scp_protocol_effective,
159            kvn_requested: args.kvn,
160            kvn_effective,
161            i_param_effective,
162            security_level_requested: args.requested_level,
163            security_level_effective,
164            session_id,
165            invoker_aid_used: invoker_aid,
166        },
167        warnings: Vec::new(),
168    })
169}