Skip to main content

scll_core/workflow/
ssd.rs

1//! Steps 4 / 4a / 5 — SSD create, package load, SSD delete (PDD §5.4/§5.4a/§5.5).
2//!
3//! `create_ssd`: INSTALL [for Install and Make Selectable] (P1 `0x0C`) on a
4//! resident SD module; refuses DAP / Mandated-DAP / Delegated-Management
5//! privileges (GPCS v2.3.1 Table 6-2). `load_package`: library-internal CAP
6//! parse → INSTALL [for Load] + chunked LOAD (240 B, ≤256 blocks), streaming the
7//! Load File Data Block through `cap::LoadFileDataBlock::next_block` — the whole
8//! LFDB is never materialised. `delete_ssd`: DELETE with explicit
9//! non-cascading vs cascading scope.
10//!
11//! `no_std` / crypto-free: the LFDB hash is **not** computed here (hashing is a
12//! crypto concern); the caller supplies `lfdb_hash` (empty ⇒ `Lh = 00`, accepted
13//! under Authorized Management). The 32 KiB inflate window is lent via
14//! [`InflateCtx`]; the per-block scratch is one stack-resident `LOAD_BLOCK_DATA`
15//! buffer. `ParentLacksAm` and the specific non-empty-SSD sub-error need the
16//! inventory snapshot that fast discovery skips; the card's SW is authoritative.
17
18use heapless::Vec;
19
20use crate::aid::Aid;
21use crate::backend::{Scp02Backend, Scp03Backend};
22use crate::cap::{parse as cap_parse, InflateCtx};
23use crate::command::delete::delete_object;
24use crate::command::install::{install_for_install_make_selectable, install_for_load, PrivLen};
25use crate::command::load::load_block;
26use crate::error::ScllError;
27use crate::limits::{HASH_MAX, INSTALL_PARAMS_MAX, LOAD_BLOCK_DATA};
28use crate::report::{
29    CreateSsdParams, CreateSsdReport, DeleteCascade, DeleteObjectParams, DeleteObjectReport,
30    DeleteTargetKind, LoadPackageParams, LoadPackageReport,
31};
32use crate::scp::ScpSession;
33use crate::transport::Transport;
34use crate::workflow::session::{self, SW_CONDITIONS, SW_OK, SW_REF_NOT_FOUND, SW_WRONG_DATA};
35
36/// Privileges an SSD created here must NOT carry (GPCS v2.3.1 Table 6-2,
37/// privilege byte 1): DAP Verification (`0x40`), Delegated Management (`0x20`),
38/// Mandated DAP Verification (`0x01`).
39const PRIV_REFUSED_MASK: u8 = 0x40 | 0x20 | 0x01;
40/// Short-APDU LOAD budget: ≤256 blocks of `LOAD_BLOCK_DATA` (PDD §5.4a).
41const MAX_LOAD_BYTES: usize = 256 * LOAD_BLOCK_DATA;
42
43/// Inputs to [`create_ssd`].
44pub struct CreateSsdArgs<'a> {
45    pub parent_sd_aid: &'a [u8],
46    pub ssd_aid: &'a [u8],
47    pub elf_aid: &'a [u8],
48    pub module_aid: &'a [u8],
49    pub privileges: [u8; 3],
50    /// Complete Install-Params field value (must already contain the mandatory
51    /// `'C9'` TLV, e.g. `C9 00`).
52    pub install_params: &'a [u8],
53}
54
55/// §5.4 — create a Supplementary Security Domain under a parent SD.
56///
57/// # Errors
58/// [`ScllError::UnsupportedPrivilege`] for a refused privilege,
59/// [`ScllError::ResidentSdNotFound`] (`6A88`), [`ScllError::AidAlreadyExists`]
60/// (`6A80`), or a transport / backend / [`ScllError::Card`] error.
61pub fn create_ssd<B>(
62    t: &mut dyn Transport,
63    backend: &B,
64    parent_session: &mut ScpSession,
65    args: &CreateSsdArgs<'_>,
66    priv_len: PrivLen,
67) -> Result<CreateSsdReport, ScllError>
68where
69    B: Scp02Backend + Scp03Backend,
70{
71    if args.privileges[0] & PRIV_REFUSED_MASK != 0 {
72        return Err(ScllError::UnsupportedPrivilege);
73    }
74    let capdu = install_for_install_make_selectable(
75        args.elf_aid,
76        args.module_aid,
77        args.ssd_aid,
78        args.privileges,
79        priv_len,
80        args.install_params,
81    )?;
82    let (_d, sw) = session::transmit_in_session(t, backend, parent_session, &capdu)?;
83    match sw {
84        SW_OK => {}
85        SW_REF_NOT_FOUND => return Err(ScllError::ResidentSdNotFound),
86        SW_WRONG_DATA => return Err(ScllError::AidAlreadyExists),
87        other => return Err(ScllError::from_general_sw(other)),
88    }
89    Ok(CreateSsdReport {
90        effective: CreateSsdParams {
91            ssd_aid_effective: Aid::new(args.ssd_aid)?,
92            aid_was_generated: false,
93            parent_sd_aid: Aid::new(args.parent_sd_aid)?,
94            privileges_used: args.privileges,
95            elf_aid_used: Aid::new(args.elf_aid)?,
96            module_aid_used: Aid::new(args.module_aid)?,
97            install_params_used: copy_bounded::<INSTALL_PARAMS_MAX>(args.install_params)?,
98        },
99        warnings: Vec::new(),
100    })
101}
102
103/// Inputs to [`load_package`]. `cap_zip` is the borrowed CAP (a ZIP; STORED or
104/// DEFLATE). `lfdb_hash` may be empty (`Lh = 00`).
105pub struct LoadPackageArgs<'a> {
106    pub target_sd_aid: &'a [u8],
107    pub cap_zip: &'a [u8],
108    pub lfdb_hash: &'a [u8],
109}
110
111/// §5.4a — load a CAP file under a target SD. `infl` lends the 32 KiB DEFLATE
112/// window (allocate once; large).
113///
114/// # Errors
115/// [`ScllError::Cap`] if the CAP cannot be parsed, [`ScllError::LoadTooLarge`]
116/// if it exceeds the short-APDU LOAD budget, [`ScllError::ResidentSdNotFound`]
117/// (`6A88`), [`ScllError::PackageAidExists`] (`6A80`), or a transport / backend /
118/// [`ScllError::Card`] error.
119pub fn load_package<B>(
120    t: &mut dyn Transport,
121    backend: &B,
122    parent_session: &mut ScpSession,
123    args: &LoadPackageArgs<'_>,
124    infl: &mut InflateCtx,
125) -> Result<LoadPackageReport, ScllError>
126where
127    B: Scp02Backend + Scp03Backend,
128{
129    let cap = cap_parse(args.cap_zip, infl)?;
130    let package_aid = cap.package_aid.clone();
131    let total = cap.lfdb().len();
132    if total > MAX_LOAD_BYTES {
133        return Err(ScllError::LoadTooLarge);
134    }
135
136    // INSTALL [for Load].
137    let install = install_for_load(package_aid.as_bytes(), args.target_sd_aid, args.lfdb_hash)?;
138    let (_d, sw) = session::transmit_in_session(t, backend, parent_session, &install)?;
139    match sw {
140        SW_OK => {}
141        SW_REF_NOT_FOUND => return Err(ScllError::ResidentSdNotFound),
142        SW_WRONG_DATA => return Err(ScllError::PackageAidExists),
143        other => return Err(ScllError::from_general_sw(other)),
144    }
145
146    // Stream the LFDB through chunked LOAD.
147    infl.reset();
148    let mut lfdb = cap.lfdb();
149    let mut out = [0u8; LOAD_BLOCK_DATA];
150    let mut sent = 0usize;
151    let mut block_no = 0u8;
152    let mut blocks_sent = 0u16;
153    loop {
154        let n = lfdb.next_block(infl, &mut out)?;
155        if n == 0 {
156            break;
157        }
158        sent += n;
159        let last = sent >= total;
160        let capdu = load_block(block_no, last, &out[..n])?;
161        let (_ld, lsw) = session::transmit_in_session(t, backend, parent_session, &capdu)?;
162        if lsw != SW_OK {
163            return Err(ScllError::from_general_sw(lsw));
164        }
165        block_no = block_no.wrapping_add(1);
166        blocks_sent += 1;
167        if last {
168            break;
169        }
170    }
171
172    Ok(LoadPackageReport {
173        effective: LoadPackageParams {
174            package_aid,
175            load_file_size: u32::try_from(total).unwrap_or(u32::MAX),
176            hash_value: copy_bounded::<HASH_MAX>(args.lfdb_hash)?,
177            block_count: blocks_sent,
178            target_sd_aid: Aid::new(args.target_sd_aid)?,
179        },
180        warnings: Vec::new(),
181    })
182}
183
184/// §5.5 — delete an SSD (optionally cascading its contents).
185///
186/// # Errors
187/// [`ScllError::SsdHasApplets`] if a non-cascading delete hits a non-empty SSD
188/// (`6985`; the precise sub-error needs the inventory), [`ScllError::TargetNoLongerExists`]
189/// (`6A88`), or a transport / backend / [`ScllError::Card`] error.
190pub fn delete_ssd<B>(
191    t: &mut dyn Transport,
192    backend: &B,
193    parent_session: &mut ScpSession,
194    ssd_aid: &[u8],
195    cascade: DeleteCascade,
196) -> Result<DeleteObjectReport, ScllError>
197where
198    B: Scp02Backend + Scp03Backend,
199{
200    let cascade_flag = matches!(cascade, DeleteCascade::Cascade | DeleteCascade::Always);
201    let capdu = delete_object(ssd_aid, cascade_flag)?;
202    let (_d, sw) = session::transmit_in_session(t, backend, parent_session, &capdu)?;
203    match sw {
204        SW_OK => {}
205        SW_REF_NOT_FOUND => return Err(ScllError::TargetNoLongerExists),
206        SW_CONDITIONS if !cascade_flag => return Err(ScllError::SsdHasApplets),
207        other => return Err(ScllError::from_general_sw(other)),
208    }
209    Ok(DeleteObjectReport {
210        effective: DeleteObjectParams {
211            target_aid: Aid::new(ssd_aid)?,
212            target_kind: DeleteTargetKind::Ssd,
213            cascade_requested: cascade,
214            cascade_used: cascade_flag,
215            instances_removed: Vec::new(),
216            elfs_removed: Vec::new(),
217        },
218        warnings: Vec::new(),
219    })
220}
221
222/// Copy a slice into a fresh bounded buffer; [`ScllError::Build`] overflow if too long.
223fn copy_bounded<const N: usize>(src: &[u8]) -> Result<Vec<u8, N>, ScllError> {
224    let mut v = Vec::new();
225    v.extend_from_slice(src)
226        .map_err(|()| ScllError::Build(crate::command::BuildError::Overflow))?;
227    Ok(v)
228}