Skip to main content

hsm_firmware/
tasks.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Async tasks running on the firmware.
17//!
18//! Two tasks live in this module:
19//!
20//! 1. [`usb_run_task`] : keeps the embassy-usb device stack alive by
21//!    polling its `run` future forever. Spawned at boot.
22//! 2. [`dispatch_loop`] : reads incoming HID reports, dispatches the
23//!    request to the [`CryptoService`], and writes the response back.
24//!    Runs in the main task because it owns the `CryptoService`.
25//!
26//! The dispatch loop also drives the [`crate::state`] state machine by
27//! posting events on [`crate::channels::EVENT_CHANNEL`]:
28//!
29//! - `Event::PinVerified` after a successful `verify_pin`.
30//! - `Event::SignRequested` at the start of a `sign` operation, then
31//!   blocks on [`crate::channels::TOUCH_CONFIRMED`] until the touch task
32//!   has confirmed the user pressed the button.
33//! - `Event::TouchTimeout` if the wait runs out.
34//! - `Event::SignComplete` once the signing call returned.
35//! - `Event::ErrorRaised` on any service error during signing.
36//!
37//! Splitting the device run loop from the dispatch loop is the canonical
38//! embassy-usb pattern: the stack handles control transfers and resets
39//! transparently while the application owns its own request/response
40//! cadence.
41
42use defmt::{info, warn};
43use embassy_futures::select::{select, Either};
44use embassy_time::{Duration, Timer};
45
46use atecc608b::AteccErrorKind;
47use atecc608b::AteccHal;
48use atecc608b::Slot;
49use hsm_crypto_service::{Clock, CryptoService, CryptoServiceError};
50use hsm_firmware_logic::{Event, TOUCH_TIMEOUT_MS};
51use hsm_usb_protocol::commands::
52{
53    parse_emergency_reset, parse_lock_config_zone, parse_lock_data_zone, parse_lock_slot,
54    parse_provision_slot, parse_read_slot_block, parse_read_slot_word, parse_set_pin,
55    parse_set_puk, parse_sign, parse_slot_only, parse_unblock_pin, parse_verify_pin,
56    parse_write_config_zone, CommandOpcode,
57};
58use hsm_usb_protocol::responses::ResponseStatus;
59use hsm_usb_protocol::Frame;
60
61use crate::channels::{post_event, TOUCH_CONFIRMED};
62use crate::usb::{HidRx, HidTx, REPORT_SIZE, UsbStack};
63
64/// Drive the embassy-usb device stack. Spawn this once at boot.
65#[embassy_executor::task]
66pub(crate) async fn usb_run_task(mut usb: UsbStack<'static>) -> !
67{
68    usb.run().await
69}
70
71/// Main request/response loop: read a HID report, dispatch on the opcode,
72/// write the response.
73///
74/// This task owns the [`CryptoService`] and therefore runs **sequentially**.
75/// Concurrent requests are not supported; the protocol does not need them.
76pub(crate) async fn dispatch_loop<H, C>
77(
78    mut rx: HidRx<'static>,
79    mut tx: HidTx<'static>,
80    mut service: CryptoService<H, C>,
81) -> !
82where
83    H: AteccHal,
84    C: Clock,
85    H::Error: core::fmt::Debug,
86{
87    let mut rx_buf = [0u8; REPORT_SIZE];
88    let mut tx_buf = [0u8; REPORT_SIZE];
89
90    loop
91    {
92        let n = match rx.read(&mut rx_buf).await
93        {
94            Ok(n) => n,
95            Err(_) =>
96            {
97                warn!("usb hid read error, restarting loop");
98                continue;
99            }
100        };
101
102        if n != REPORT_SIZE
103        {
104            warn!("unexpected short hid report ({} bytes), discarding", n);
105            continue;
106        }
107
108        let response_len = handle_one_request(&mut service, &rx_buf, &mut tx_buf).await;
109
110        if tx.write(&tx_buf[..response_len]).await.is_err()
111        {
112            warn!("usb hid write error");
113        }
114    }
115}
116
117/// Process one incoming report, write the response into `tx_buf`, return
118/// the number of bytes used.
119///
120/// Always writes exactly [`REPORT_SIZE`] bytes (a HID report is fixed-size)
121/// so the return value is currently always [`REPORT_SIZE`]. The signature
122/// keeps it explicit in case a future variant needs to send less.
123async fn handle_one_request<H, C>
124(
125    service: &mut CryptoService<H, C>,
126    rx_buf: &[u8],
127    tx_buf: &mut [u8],
128) -> usize
129where
130    H: AteccHal,
131    C: Clock,
132    H::Error: core::fmt::Debug,
133{
134    let frame = match Frame::parse(rx_buf)
135    {
136        Ok(f) => f,
137        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
138    };
139
140    let opcode = match CommandOpcode::try_from(frame.opcode)
141    {
142        Ok(op) => op,
143        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidCommand, &[]),
144    };
145
146    info!("dispatching opcode {:#x}", opcode as u8);
147
148    match opcode
149    {
150        CommandOpcode::Info => handle_info(service, tx_buf).await,
151        CommandOpcode::GetPubkey => handle_get_pubkey(service, frame.payload, tx_buf).await,
152        CommandOpcode::Sign => handle_sign(service, frame.payload, tx_buf).await,
153        CommandOpcode::GenKey => handle_genkey(service, frame.payload, tx_buf).await,
154        CommandOpcode::ReadConfigZone =>
155        {
156            handle_read_config_zone(service, frame.payload, tx_buf).await
157        }
158        CommandOpcode::VerifyPin => handle_verify_pin(service, frame.payload, tx_buf).await,
159        CommandOpcode::SetPin => handle_set_pin(service, frame.payload, tx_buf).await,
160        CommandOpcode::UnblockPin => handle_unblock_pin(service, frame.payload, tx_buf).await,
161        CommandOpcode::GetPinStatus => handle_get_pin_status(service, tx_buf).await,
162        CommandOpcode::SetPuk => handle_set_puk(service, frame.payload, tx_buf).await,
163        CommandOpcode::CloseSession => handle_close_session(service, tx_buf),
164        CommandOpcode::EmergencyReset =>
165        {
166            handle_emergency_reset(service, frame.payload, tx_buf).await
167        }
168        CommandOpcode::ReadSlotBlock =>
169        {
170            handle_read_slot_block(service, frame.payload, tx_buf).await
171        }
172        CommandOpcode::ReadSlotWord =>
173        {
174            handle_read_slot_word(service, frame.payload, tx_buf).await
175        }
176        CommandOpcode::ReadConfigSlot =>
177        {
178            handle_read_config_slot(service, frame.payload, tx_buf).await
179        }
180        CommandOpcode::WriteConfigZone =>
181        {
182            handle_write_config_zone(service, frame.payload, tx_buf).await
183        }
184        CommandOpcode::ProvisionSlot =>
185        {
186            handle_provision_slot(service, frame.payload, tx_buf).await
187        }
188        CommandOpcode::ProvisionInitialPin =>
189        {
190            handle_provision_initial_pin(service, tx_buf).await
191        }
192        CommandOpcode::ProvisionInitialPuk =>
193        {
194            handle_provision_initial_puk(service, tx_buf).await
195        }
196        CommandOpcode::ProvisionIoKey =>
197        {
198            handle_provision_io_key(service, tx_buf).await
199        }
200        CommandOpcode::ReadCounter =>
201        {
202            handle_read_counter(service, frame.payload, tx_buf).await
203        }
204        CommandOpcode::LockConfigZone =>
205        {
206            handle_lock_config_zone(service, frame.payload, tx_buf).await
207        }
208        CommandOpcode::LockDataZone =>
209        {
210            handle_lock_data_zone(service, frame.payload, tx_buf).await
211        }
212        CommandOpcode::LockSlot =>
213        {
214            handle_lock_slot(service, frame.payload, tx_buf).await
215        }
216    }
217}
218
219async fn handle_info<H, C>
220(
221    service: &mut CryptoService<H, C>,
222    tx_buf: &mut [u8],
223) -> usize
224where
225    H: AteccHal,
226    C: Clock,
227    H::Error: core::fmt::Debug,
228{
229    match service.info().await
230    {
231        Ok(info) =>
232        {
233            // Payload layout: revision(4) || serial(9) || provisioned_flag(1) = 14 bytes.
234            let mut payload = [0u8; 14];
235            payload[0..4].copy_from_slice(&info.revision);
236            payload[4..13].copy_from_slice(&info.serial);
237            payload[13] = u8::from(info.is_provisioned);
238            write_status(tx_buf, ResponseStatus::Ok, &payload)
239        }
240        Err(err) => write_error(tx_buf, &err),
241    }
242}
243
244async fn handle_get_pubkey<H, C>
245(
246    service: &mut CryptoService<H, C>,
247    payload: &[u8],
248    tx_buf: &mut [u8],
249) -> usize
250where
251    H: AteccHal,
252    C: Clock,
253    H::Error: core::fmt::Debug,
254{
255    let slot = match parse_slot_only(payload)
256    {
257        Ok(s) => s,
258        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
259    };
260    let slot = match Slot::new(slot)
261    {
262        Some(s) => s,
263        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
264    };
265    match service.get_pubkey(slot).await
266    {
267        Ok(pk) => write_status(tx_buf, ResponseStatus::Ok, &pk),
268        Err(err) => write_error(tx_buf, &err),
269    }
270}
271
272async fn handle_sign<H, C>
273(
274    service: &mut CryptoService<H, C>,
275    payload: &[u8],
276    tx_buf: &mut [u8],
277) -> usize
278where
279    H: AteccHal,
280    C: Clock,
281    H::Error: core::fmt::Debug,
282{
283    let (slot_idx, digest) = match parse_sign(payload)
284    {
285        Ok(v) => v,
286        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
287    };
288    let slot = match Slot::new(slot_idx)
289    {
290        Some(s) => s,
291        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
292    };
293
294    // Fail fast if there's no active PIN session, before arming the
295    // touch wait.
296    if !service.is_session_active()
297    {
298        return write_status(tx_buf, ResponseStatus::PinRequired, &[]);
299    }
300
301    // Drain any stale TOUCH_CONFIRMED pulse from a previous run before
302    // arming the wait.
303    TOUCH_CONFIRMED.reset();
304
305    // Notify the state machine that a sign request needs a touch.
306    post_event(Event::SignRequested);
307
308    // Wait for the user to physically touch the button. 
309    // Bail out after TOUCH_TIMEOUT_MS.
310    let timeout = Timer::after(Duration::from_millis(TOUCH_TIMEOUT_MS));
311    let confirmation = TOUCH_CONFIRMED.wait();
312    match select(timeout, confirmation).await
313    {
314        Either::First(()) =>
315        {
316            info!("touch timeout, cancelling sign");
317            post_event(Event::TouchTimeout);
318            return write_status(tx_buf, ResponseStatus::TouchTimeout, &[]);
319        }
320        Either::Second(()) =>
321        {
322            // Touch confirmed, proceed.
323        }
324    }
325
326    // Perform the actual signing. On error, also fire SignComplete so
327    // the SM does not stay stuck in Signing.
328    let result = service.sign(slot, &digest).await;
329    post_event(Event::SignComplete);
330
331    match result
332    {
333        Ok(sig) => write_status(tx_buf, ResponseStatus::Ok, &sig),
334        Err(err) =>
335        {
336            post_event(Event::ErrorRaised);
337            write_error(tx_buf, &err)
338        }
339    }
340}
341
342async fn handle_genkey<H, C>
343(
344    service: &mut CryptoService<H, C>,
345    payload: &[u8],
346    tx_buf: &mut [u8],
347) -> usize
348where
349    H: AteccHal,
350    C: Clock,
351    H::Error: core::fmt::Debug,
352{
353    // Payload: [slot: u8]. Returns the new public key (64 bytes).
354    let slot_idx = match parse_slot_only(payload)
355    {
356        Ok(s) => s,
357        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
358    };
359    let slot = match Slot::new(slot_idx)
360    {
361        Some(s) => s,
362        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
363    };
364    match service.genkey_create(slot).await
365    {
366        Ok(pubkey) => write_status(tx_buf, ResponseStatus::Ok, &pubkey),
367        Err(err) => write_error(tx_buf, &err),
368    }
369}
370
371async fn handle_read_config_zone<H, C>
372(
373    service: &mut CryptoService<H, C>,
374    payload: &[u8],
375    tx_buf: &mut [u8],
376) -> usize
377where
378    H: AteccHal,
379    C: Clock,
380    H::Error: core::fmt::Debug,
381{
382    // Payload: [block: u8]. Returns 32 bytes of the requested block.
383    let block = match parse_slot_only(payload)
384    {
385        Ok(b) => b,
386        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
387    };
388    match service.read_config_block(block).await
389    {
390        Ok(data) => write_status(tx_buf, ResponseStatus::Ok, &data),
391        Err(err) => write_error(tx_buf, &err),
392    }
393}
394
395async fn handle_read_config_slot<H, C>
396(
397    service: &mut CryptoService<H, C>,
398    payload: &[u8],
399    tx_buf: &mut [u8],
400) -> usize
401where
402    H: AteccHal,
403    C: Clock,
404    H::Error: core::fmt::Debug,
405{
406    // Payload: [slot: u8]. Returns 4 bytes: [SlotConfig lo/hi, KeyConfig lo/hi].
407    let slot_idx = match parse_slot_only(payload)
408    {
409        Ok(s) => s,
410        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
411    };
412    let slot = match Slot::new(slot_idx)
413    {
414        Some(s) => s,
415        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
416    };
417    match service.read_config_slot(slot).await
418    {
419        Ok(data) => write_status(tx_buf, ResponseStatus::Ok, &data),
420        Err(err) => write_error(tx_buf, &err),
421    }
422}
423
424async fn handle_write_config_zone<H, C>
425(
426    service: &mut CryptoService<H, C>,
427    payload: &[u8],
428    tx_buf: &mut [u8],
429) -> usize
430where
431    H: AteccHal,
432    C: Clock,
433    H::Error: core::fmt::Debug,
434{
435    // Payload: [block: u8, data: [u8; 32]]. Writes one block of the
436    // config zone.
437    let (block, data) = match parse_write_config_zone(payload)
438    {
439        Ok(v) => v,
440        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
441    };
442    match service.write_config_block(block, &data).await
443    {
444        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
445        Err(err) => write_error(tx_buf, &err),
446    }
447}
448
449async fn handle_provision_slot<H, C>
450(
451    service: &mut CryptoService<H, C>,
452    payload: &[u8],
453    tx_buf: &mut [u8],
454) -> usize
455where
456    H: AteccHal,
457    C: Clock,
458    H::Error: core::fmt::Debug,
459{
460    // Payload: [slot: u8, value: [u8; 32]]. Writes a 32-byte cleartext
461    // value into the requested slot. The service enforces the policy
462    // that only slots 5, 6, 8 are accepted.
463    let (slot_idx, value) = match parse_provision_slot(payload)
464    {
465        Ok(v) => v,
466        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
467    };
468    let slot = match Slot::new(slot_idx)
469    {
470        Some(s) => s,
471        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
472    };
473    match service.provision_slot(slot, &value).await
474    {
475        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
476        Err(err) => write_error(tx_buf, &err),
477    }
478}
479
480async fn handle_provision_initial_pin<H, C>
481(
482    service: &mut CryptoService<H, C>,
483    tx_buf: &mut [u8],
484) -> usize
485where
486    H: AteccHal,
487    C: Clock,
488    H::Error: core::fmt::Debug,
489{
490    match service.provision_initial_pin().await
491    {
492        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
493        Err(err) => write_error(tx_buf, &err),
494    }
495}
496
497async fn handle_provision_initial_puk<H, C>
498(
499    service: &mut CryptoService<H, C>,
500    tx_buf: &mut [u8],
501) -> usize
502where
503    H: AteccHal,
504    C: Clock,
505    H::Error: core::fmt::Debug,
506{
507    match service.provision_initial_puk().await
508    {
509        Ok(puk) => write_status(tx_buf, ResponseStatus::Ok, &puk),
510        Err(err) => write_error(tx_buf, &err),
511    }
512}
513
514async fn handle_provision_io_key<H, C>
515(
516    service: &mut CryptoService<H, C>,
517    tx_buf: &mut [u8],
518) -> usize
519where
520    H: AteccHal,
521    C: Clock,
522    H::Error: core::fmt::Debug,
523{
524    match service.provision_initial_io_key().await
525    {
526        Ok(io_key) => write_status(tx_buf, ResponseStatus::Ok, &io_key),
527        Err(err) => write_error(tx_buf, &err),
528    }
529}
530
531// -----------------------------------------------------------------------
532// Lock handlers -- IRREVERSIBLE
533// -----------------------------------------------------------------------
534//
535// All three follow the same pattern:
536// 1. Parse the payload. A magic-mismatch maps to `LockMagicMismatch`,
537//    a wrong length to `InvalidPayload`.
538// 2. Forward the validated arg(s) to the service.
539// 3. Map chip-side errors to a stable response. For `LockConfigZone`,
540//    a CRC mismatch from the chip surfaces as `ExecutionError` and is
541//    rewritten to `LockCrcMismatch` for clarity on the host side.
542//    `LockDataZone` and `LockSlot` do not ask the chip to verify any
543//    CRC (the host cannot compute one over `IsSecret=1` slots), so a
544//    `LockCrcMismatch` cannot occur for them.
545
546async fn handle_lock_config_zone<H, C>
547(
548    service: &mut CryptoService<H, C>,
549    payload: &[u8],
550    tx_buf: &mut [u8],
551) -> usize
552where
553    H: AteccHal,
554    C: Clock,
555    H::Error: core::fmt::Debug,
556{
557    let expected_crc = match parse_lock_config_zone(payload)
558    {
559        Ok(v) => v,
560        Err(hsm_usb_protocol::commands::PayloadError::MagicMismatch) =>
561        {
562            return write_status(tx_buf, ResponseStatus::LockMagicMismatch, &[]);
563        }
564        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
565    };
566    match service.lock_config_zone(expected_crc).await
567    {
568        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
569        Err(err) => write_lock_error(tx_buf, &err),
570    }
571}
572
573async fn handle_lock_data_zone<H, C>
574(
575    service: &mut CryptoService<H, C>,
576    payload: &[u8],
577    tx_buf: &mut [u8],
578) -> usize
579where
580    H: AteccHal,
581    C: Clock,
582    H::Error: core::fmt::Debug,
583{
584    match parse_lock_data_zone(payload)
585    {
586        Ok(()) => {}
587        Err(hsm_usb_protocol::commands::PayloadError::MagicMismatch) =>
588        {
589            return write_status(tx_buf, ResponseStatus::LockMagicMismatch, &[]);
590        }
591        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
592    }
593    match service.lock_data_zone().await
594    {
595        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
596        Err(err) => write_lock_error(tx_buf, &err),
597    }
598}
599
600async fn handle_lock_slot<H, C>
601(
602    service: &mut CryptoService<H, C>,
603    payload: &[u8],
604    tx_buf: &mut [u8],
605) -> usize
606where
607    H: AteccHal,
608    C: Clock,
609    H::Error: core::fmt::Debug,
610{
611    let slot_idx = match parse_lock_slot(payload)
612    {
613        Ok(v) => v,
614        Err(hsm_usb_protocol::commands::PayloadError::MagicMismatch) =>
615        {
616            return write_status(tx_buf, ResponseStatus::LockMagicMismatch, &[]);
617        }
618        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
619    };
620    let slot = match Slot::new(slot_idx)
621    {
622        Some(s) => s,
623        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
624    };
625    match service.lock_slot(slot).await
626    {
627        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
628        Err(err) => write_lock_error(tx_buf, &err),
629    }
630}
631
632/// Map a service error to a response status, with a lock-specific bias:
633/// the chip's "execution error" during a lock command almost always
634/// means the CRC fed by the host did not match the chip's recomputed
635/// CRC. Surface that as the dedicated `LockCrcMismatch` status to make
636/// triage trivial on the host side.
637fn write_lock_error<HalError>
638(
639    tx_buf: &mut [u8],
640    err: &CryptoServiceError<HalError>,
641) -> usize
642where
643    HalError: core::fmt::Debug,
644{
645    use atecc608b::{AteccError, ChipError};
646    use CryptoServiceError as E;
647
648    if let E::Atecc(AteccError::Chip(ChipError::ExecutionError)) = err
649    {
650        return write_status(tx_buf, ResponseStatus::LockCrcMismatch, &[]);
651    }
652    write_error(tx_buf, err)
653}
654
655async fn handle_verify_pin<H, C>
656(
657    service: &mut CryptoService<H, C>,
658    payload: &[u8],
659    tx_buf: &mut [u8],
660) -> usize
661where
662    H: AteccHal,
663    C: Clock,
664    H::Error: core::fmt::Debug,
665{
666    let pin = match parse_verify_pin(payload)
667    {
668        Ok(p) => p,
669        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
670    };
671    match service.verify_pin(&pin).await
672    {
673        Ok(()) =>
674        {
675            post_event(Event::PinVerified);
676            write_status(tx_buf, ResponseStatus::Ok, &[])
677        }
678        Err(err) => write_error(tx_buf, &err),
679    }
680}
681
682async fn handle_set_pin<H, C>
683(
684    service: &mut CryptoService<H, C>,
685    payload: &[u8],
686    tx_buf: &mut [u8],
687) -> usize
688where
689    H: AteccHal,
690    C: Clock,
691    H::Error: core::fmt::Debug,
692{
693    let (old, new, io_key) = match parse_set_pin(payload)
694    {
695        Ok(v) => v,
696        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
697    };
698    match service.set_pin(&old, &new, &io_key).await
699    {
700        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
701        Err(err) => write_error(tx_buf, &err),
702    }
703}
704
705async fn handle_unblock_pin<H, C>
706(
707    service: &mut CryptoService<H, C>,
708    payload: &[u8],
709    tx_buf: &mut [u8],
710) -> usize
711where
712    H: AteccHal,
713    C: Clock,
714    H::Error: core::fmt::Debug,
715{
716    let (puk, new_pin, io_key) = match parse_unblock_pin(payload)
717    {
718        Ok(v) => v,
719        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
720    };
721    match service.unblock_pin(&puk, &new_pin, &io_key).await
722    {
723        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
724        Err(err) => write_error(tx_buf, &err),
725    }
726}
727
728async fn handle_set_puk<H, C>
729(
730    service: &mut CryptoService<H, C>,
731    payload: &[u8],
732    tx_buf: &mut [u8],
733) -> usize
734where
735    H: AteccHal,
736    C: Clock,
737    H::Error: core::fmt::Debug,
738{
739    let (old, new, io_key) = match parse_set_puk(payload)
740    {
741        Ok(v) => v,
742        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
743    };
744    match service.set_puk(&old, &new, &io_key).await
745    {
746        Ok(()) => write_status(tx_buf, ResponseStatus::Ok, &[]),
747        Err(err) => write_error(tx_buf, &err),
748    }
749}
750
751/// Handler for the `EmergencyReset` opcode.
752///
753/// On success the response payload contains the freshly generated
754/// 8-digit PUK so the caller can display it to the user. The handler
755/// returns `EmergencyResetNotPermitted` (carrying the actual tries
756/// remaining) if the user still has any PIN or PUK attempts.
757async fn handle_emergency_reset<H, C>
758(
759    service: &mut CryptoService<H, C>,
760    payload: &[u8],
761    tx_buf: &mut [u8],
762) -> usize
763where
764    H: AteccHal,
765    C: Clock,
766    H::Error: core::fmt::Debug,
767{
768    let io_key = match parse_emergency_reset(payload)
769    {
770        Ok(v) => v,
771        Err(hsm_usb_protocol::commands::PayloadError::MagicMismatch) =>
772        {
773            return write_status(tx_buf, ResponseStatus::LockMagicMismatch, &[]);
774        }
775        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
776    };
777    match service.emergency_reset(&io_key).await
778    {
779        Ok(new_puk) => write_status(tx_buf, ResponseStatus::Ok, &new_puk),
780        Err(err) => write_error(tx_buf, &err),
781    }
782}
783
784/// Synchronous handler: closes the PIN session in the host-side state
785/// only, no chip interaction. Always succeeds (idempotent).
786fn handle_close_session<H, C>
787(
788    service: &mut CryptoService<H, C>,
789    tx_buf: &mut [u8],
790) -> usize
791where
792    H: AteccHal,
793    C: Clock,
794    H::Error: core::fmt::Debug,
795{
796    service.close_session();
797    write_status(tx_buf, ResponseStatus::Ok, &[])
798}
799
800async fn handle_read_slot_block<H, C>
801(
802    service: &mut CryptoService<H, C>,
803    payload: &[u8],
804    tx_buf: &mut [u8],
805) -> usize
806where
807    H: AteccHal,
808    C: Clock,
809    H::Error: core::fmt::Debug,
810{
811    let (slot_idx, block) = match parse_read_slot_block(payload)
812    {
813        Ok(v) => v,
814        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
815    };
816    let slot = match Slot::new(slot_idx)
817    {
818        Some(s) => s,
819        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
820    };
821    match service.read_slot_block(slot, block).await
822    {
823        Ok(data) => write_status(tx_buf, ResponseStatus::Ok, &data),
824        Err(err) => write_error(tx_buf, &err),
825    }
826}
827
828async fn handle_read_slot_word<H, C>
829(
830    service: &mut CryptoService<H, C>,
831    payload: &[u8],
832    tx_buf: &mut [u8],
833) -> usize
834where
835    H: AteccHal,
836    C: Clock,
837    H::Error: core::fmt::Debug,
838{
839    let (slot_idx, block, offset) = match parse_read_slot_word(payload)
840    {
841        Ok(v) => v,
842        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
843    };
844    let slot = match Slot::new(slot_idx)
845    {
846        Some(s) => s,
847        None => return write_status(tx_buf, ResponseStatus::InvalidSlot, &[]),
848    };
849    match service.read_slot_word(slot, block, offset).await
850    {
851        Ok(data) => write_status(tx_buf, ResponseStatus::Ok, &data),
852        Err(err) => write_error(tx_buf, &err),
853    }
854}
855
856async fn handle_get_pin_status<H, C>
857(
858    service: &mut CryptoService<H, C>,
859    tx_buf: &mut [u8],
860) -> usize
861where
862    H: AteccHal,
863    C: Clock,
864    H::Error: core::fmt::Debug,
865{
866    match service.get_pin_status().await
867    {
868        Ok(status) =>
869        {
870            let payload = [
871                status.pin_tries_remaining,
872                status.puk_tries_remaining,
873                u8::from(status.session_active),
874            ];
875            write_status(tx_buf, ResponseStatus::Ok, &payload)
876        }
877        Err(err) => write_error(tx_buf, &err),
878    }
879}
880
881/// Read one of the chip's monotonic counters and return its raw value.
882///
883/// Payload format: `[counter_id: u8]` (0 = Counter0, 1 = Counter1). Any
884/// other value yields [`ResponseStatus::InvalidPayload`].
885///
886/// Response payload: 4 bytes little-endian, the chip's `u32` count
887/// unaltered. The host CLI decodes this into a decimal + hex value for
888/// the operator. Unlike [`handle_get_pin_status`] this does **not** map
889/// the count to "tries remaining", the goal is diagnostic
890/// transparency on what the chip actually stores.
891async fn handle_read_counter<H, C>
892(
893    service: &mut CryptoService<H, C>,
894    payload: &[u8],
895    tx_buf: &mut [u8],
896) -> usize
897where
898    H: AteccHal,
899    C: Clock,
900    H::Error: core::fmt::Debug,
901{
902    let counter_byte = match parse_slot_only(payload)
903    {
904        Ok(b) => b,
905        Err(_) => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
906    };
907    let counter = match counter_byte
908    {
909        0 => atecc608b::command::counter::CounterId::Counter0,
910        1 => atecc608b::command::counter::CounterId::Counter1,
911        _ => return write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
912    };
913    match service.read_counter(counter).await
914    {
915        Ok(value) =>
916        {
917            let bytes = value.to_le_bytes();
918            write_status(tx_buf, ResponseStatus::Ok, &bytes)
919        }
920        Err(err) => write_error(tx_buf, &err),
921    }
922}
923
924/// Map a [`CryptoServiceError`] into a status + payload and write it.
925///
926/// Driver-level failures are split into two cases:
927///
928/// - [`AteccErrorKind::Chip`]: the chip itself returned a non-zero status
929///   byte. Surface that as [`ResponseStatus::AteccChipError`] (0x05) with
930///   the raw chip status byte in `payload[0]`. Lets the host translate to
931///   `ParseError`, `ExecutionError`, etc., for triage.
932/// - Anything else (HAL nack, wake failure, CRC mismatch, timeout, ...):
933///   surface as [`ResponseStatus::AteccCommunicationError`] (0x04) with a
934///   one-byte sub-code in `payload[0]` derived from
935///   [`AteccErrorKind::as_sub_code`].
936fn write_error<HalError>
937(
938    tx_buf: &mut [u8],
939    err: &CryptoServiceError<HalError>,
940) -> usize
941where
942    HalError: core::fmt::Debug,
943{
944    use CryptoServiceError as E;
945    match err
946    {
947        E::Atecc(atecc_err) => match atecc_err.kind()
948        {
949            AteccErrorKind::Chip(chip_err) =>
950            {
951                write_status
952                (
953                    tx_buf,
954                    ResponseStatus::AteccChipError,
955                    &[chip_err.as_status_byte()],
956                )
957            }
958            other =>
959            {
960                write_status
961                (
962                    tx_buf,
963                    ResponseStatus::AteccCommunicationError,
964                    &[other.as_sub_code()],
965                )
966            }
967        },
968        E::InvalidFormat(_) => write_status(tx_buf, ResponseStatus::InvalidPayload, &[]),
969        E::PinIncorrect { tries_remaining } =>
970        {
971            write_status(tx_buf, ResponseStatus::WrongPin, &[*tries_remaining])
972        }
973        E::PinBlocked => write_status(tx_buf, ResponseStatus::PinBlocked, &[]),
974        E::PukIncorrect { tries_remaining } =>
975        {
976            write_status(tx_buf, ResponseStatus::WrongPuk, &[*tries_remaining])
977        }
978        E::Bricked => write_status(tx_buf, ResponseStatus::Bricked, &[]),
979        E::PinRequired => write_status(tx_buf, ResponseStatus::PinRequired, &[]),
980        E::NotProvisioned => write_status(tx_buf, ResponseStatus::NotProvisioned, &[]),
981        E::EmergencyResetNotPermitted { pin_tries_remaining, puk_tries_remaining } =>
982        {
983            write_status(
984                tx_buf,
985                ResponseStatus::EmergencyResetNotPermitted,
986                &[*pin_tries_remaining, *puk_tries_remaining],
987            )
988        }
989        E::InvalidSlot { slot } =>
990        {
991            write_status(tx_buf, ResponseStatus::InvalidSlot, &[slot.as_u8()])
992        }
993    }
994}
995
996/// Build a response frame in `tx_buf` and return its length.
997fn write_status(tx_buf: &mut [u8], status: ResponseStatus, payload: &[u8]) -> usize
998{
999    // If the payload is too large for one report, truncate. This should
1000    // not happen in practice because all our responses fit. Defensive
1001    // programming.
1002    if Frame::write(status.as_u8(), payload, tx_buf).is_err()
1003    {
1004        // Degrade gracefully: write a short InvalidPayload status.
1005        let _ = Frame::write(ResponseStatus::InvalidPayload.as_u8(), &[], tx_buf);
1006    }
1007    REPORT_SIZE
1008}