Skip to main content

hsm_crypto_service/
service.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//! High-level crypto service exposed to the USB layer.
17//!
18//! This is where the actual workflow logic lives: PIN session management,
19//! PIN/PUK verification with counter accounting, sign orchestration on top
20//! of `Nonce` + `Sign`, and public-key retrieval.
21//!
22//! The service is generic over both the HAL (so it can run against a real
23//! ATECC over I2C or against a `MockHal` in tests) and the [`Clock`] (so
24//! tests can drive time deterministically).
25//!
26//! # Channel discipline
27//!
28//! Every public method on [`CryptoService`] is responsible for opening
29//! and closing the chip channel(s) it needs.
30//!
31//! - For a single-shot chip command (e.g. read one counter), the method
32//!   opens a channel, runs the command, closes the channel.
33//! - For a multi-step chip workflow that **shares volatile state**
34//!   (Nonce + Sign, Nonce + `GenDig` + Write), the method opens **one**
35//!   channel that spans the whole sequence, so `TempKey` stays alive
36//!   across the steps.
37//! - For workflows that combine several independent chip commands
38//!   (e.g. read counter, then `CheckMac`, then read counter again), the
39//!   method may either keep one channel open for the whole flow or open
40//!   one per command. The choice is documented per method when it
41//!   matters; the default is to open one per logical step for clarity.
42//!
43//! The PIN "session" referred to elsewhere in this crate is unrelated to
44//! the chip channel. It is the host-side authentication window that
45//! says "the user has typed a valid PIN recently". See [`Session`].
46
47use core::fmt::Debug;
48
49use atecc608b::command::counter::CounterId;
50use atecc608b::command::gendig::GenDigZone;
51use atecc608b::command::nonce::NonceTarget;
52use atecc608b::command::read_write::{config_or_otp_address, data_address, Zone};
53use atecc608b::{Atecc, AteccError, AteccHal, ChipError, Slot};
54
55use crate::encrypted_write::
56{
57    build_encrypted_write_payload, derive_session_key, encrypt_payload, write_mac, SLOT_VALUE_LEN,
58};
59use crate::error::CryptoServiceError;
60use crate::pin::
61{
62    FormatError, HASH_LEN, PIN_LEN, PUK_LEN, checkmac_other_data, checkmac_response, pin_hash, pin_salt, puk_hash, puk_salt, validate_digits
63};
64use crate::session::{Clock, Session};
65use crate::slots::
66{
67    PIN_DEFAULT, PIN_MAX_RETRIES, PUK_MAX_RETRIES, SLOT_IO_KEY, SLOT_PIN_HASH, SLOT_PUK_HASH,
68};
69
70/// Convenience alias for the service's result type.
71pub(crate) type ServiceResult<T, HalError> = Result<T, CryptoServiceError<HalError>>;
72
73/// Length of the chip serial number, in bytes, as read from the config
74/// zone.
75pub(crate) const CHIP_SERIAL_LEN: usize = 9;
76
77/// 64-byte raw public key returned by `GenKey`.
78pub(crate) type PublicKey = [u8; 64];
79
80/// 64-byte raw `R || S` ECDSA signature returned by `Sign`.
81pub(crate) type Signature = [u8; 64];
82
83/// Returned by [`CryptoService::info`].
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(feature = "defmt", derive(defmt::Format))]
86pub struct DeviceInfo
87{
88    /// Chip revision (4 bytes returned by `Info(Revision)`).
89    pub revision: [u8; 4],
90    /// Chip serial number (9 bytes).
91    pub serial: [u8; CHIP_SERIAL_LEN],
92    /// `true` if both config and data zones are locked. Required for
93    /// real-world operation.
94    pub is_provisioned: bool,
95}
96
97/// Returned by [`CryptoService::get_pin_status`].
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[cfg_attr(feature = "defmt", derive(defmt::Format))]
100pub struct PinStatus
101{
102    /// Number of PIN attempts remaining before the slot is hardware-locked.
103    pub pin_tries_remaining: u8,
104    /// Number of PUK attempts remaining before the chip is bricked.
105    pub puk_tries_remaining: u8,
106    /// `true` if a PIN session is currently active (not timed out).
107    pub session_active: bool,
108}
109
110/// High-level crypto orchestrator.
111pub struct CryptoService<H, C>
112where
113    H: AteccHal,
114    C: Clock,
115{
116    atecc:   Atecc<H>,
117    clock:   C,
118    session: Session,
119    /// Cached chip serial, populated on first use.
120    serial:  Option<[u8; CHIP_SERIAL_LEN]>,
121}
122
123impl<H, C> CryptoService<H, C>
124where
125    H: AteccHal,
126    C: Clock,
127    H::Error: Debug,
128{
129    /// Wrap an existing [`Atecc`] handle and clock.
130    pub fn new(atecc: Atecc<H>, clock: C) -> Self
131    {
132        Self
133        {
134            atecc,
135            clock,
136            session: Session::new(),
137            serial:  None,
138        }
139    }
140
141    /// Return chip revision, serial, and provisioning state.
142    ///
143    /// Opens one channel for the revision read, then reuses a second
144    /// channel for the config-zone reads that produce serial and lock
145    /// status. The two channels are independent because there is no
146    /// volatile state to share.
147    ///
148    /// # Errors
149    /// See [`CryptoServiceError::Atecc`].
150    pub async fn info(&mut self) -> ServiceResult<DeviceInfo, H::Error>
151    {
152        let revision =
153        {
154            let mut channel = self.atecc.open_channel().await?;
155            let revision = channel.info_revision().await?;
156            channel.close().await?;
157            revision
158        };
159        let serial = self.cached_serial().await?;
160        let is_provisioned = self.is_provisioned().await?;
161        Ok(DeviceInfo { revision, serial, is_provisioned })
162    }
163
164    /// Read the 64-byte public key from `slot`.
165    ///
166    /// Does not require an active PIN session: the public key is, well,
167    /// public. Internally calls `GenKey` in mode 0 (compute pubkey from
168    /// existing private key, no mutation).
169    ///
170    /// # Errors
171    /// See [`CryptoServiceError::Atecc`].
172    pub async fn get_pubkey(&mut self, slot: Slot) -> ServiceResult<PublicKey, H::Error>
173    {
174        let mut channel = self.atecc.open_channel().await?;
175        let pk = channel.genkey_public(slot).await?;
176        channel.close().await?;
177        Ok(pk)
178    }
179
180    /// Generate a fresh ECC P-256 private key on chip in the specified
181    /// slot. The old key (if any) is destroyed. Returns the
182    /// corresponding public key (64 bytes: X || Y).
183    ///
184    /// The chip enforces the slot's policy: a slot configured as
185    /// `Locked` rejects this command, and a slot whose `KeyConfig` does
186    /// not allow `GenKey(create)` is also rejected.
187    ///
188    /// In this project, `genkey_create` is used:
189    /// - During provisioning to populate the primary identity key in
190    ///   slot 0 (and optionally extra user slots in 1..=4 and 7).
191    /// - In [`Self::emergency_reset`]
192    ///   to refresh every identity slot at once.
193    ///
194    /// # Errors
195    /// - [`CryptoServiceError::Atecc`] on chip-level failures.
196    pub async fn genkey_create
197    (
198        &mut self,
199        slot: Slot,
200    ) -> ServiceResult<PublicKey, H::Error>
201    {
202        let mut channel = self.atecc.open_channel().await?;
203        let pk = channel.genkey_create(slot).await?;
204        channel.close().await?;
205        Ok(pk)
206    }
207
208    /// Read the per-slot configuration bytes (`SlotConfig` + `KeyConfig`).
209    ///
210    /// Returns 4 bytes: `[SlotConfig_lo, SlotConfig_hi, KeyConfig_lo,
211    /// KeyConfig_hi]`. This is useful when the host wants to inspect a
212    /// slot's policy without re-downloading the full 128-byte config
213    /// zone (which `read_config_zone` already does block by block).
214    ///
215    /// `SlotConfig` for slot N lives at config byte `20 + 2*N` and
216    /// `KeyConfig` at `96 + 2*N`. This method reads the whole config
217    /// zone and extracts those 4 bytes. The chip cost is identical to
218    /// `read_config_zone` (4 reads of 32 bytes each).
219    ///
220    /// # Errors
221    /// - [`CryptoServiceError::Atecc`] on chip-level failures.
222    pub async fn read_config_slot(&mut self, slot: Slot) -> ServiceResult<[u8; 4], H::Error>
223    {
224        let mut zone = [0u8; 128];
225        {
226            let mut channel = self.atecc.open_channel().await?;
227            channel.read_config_zone(&mut zone).await?;
228            channel.close().await?;
229        }
230        let n = usize::from(slot.as_u8());
231        let sc_off = 20 + 2 * n;
232        let kc_off = 96 + 2 * n;
233        Ok([zone[sc_off], zone[sc_off + 1], zone[kc_off], zone[kc_off + 1]])
234    }
235
236    /// Read one 32-byte block of the configuration zone.
237    ///
238    /// `block` must be in `0..=3`. Each block covers a fixed 32-byte
239    /// region:
240    ///
241    /// - block 0 : factory area + `SlotConfig`[0..6] (bytes 0..32)
242    /// - block 1 : `SlotConfig`[6..16] + Counter0 + start of Counter1 (32..64)
243    /// - block 2 : end of Counter1 + UseLock..ChipOptions..X509format (64..96)
244    /// - block 3 : `KeyConfig`[0..16] (96..128)
245    ///
246    /// # Errors
247    /// - [`CryptoServiceError::InvalidFormat`] if `block > 3`.
248    /// - [`CryptoServiceError::Atecc`] on chip-level failures.
249    pub async fn read_config_block
250    (
251        &mut self,
252        block: u8,
253    ) -> ServiceResult<[u8; 32], H::Error>
254    {
255        if block > 3
256        {
257            return Err(CryptoServiceError::InvalidFormat(FormatError::OutOfRange));
258        }
259        let mut zone = [0u8; 128];
260        {
261            let mut channel = self.atecc.open_channel().await?;
262            channel.read_config_zone(&mut zone).await?;
263            channel.close().await?;
264        }
265        let start = usize::from(block) * 32;
266        let mut out = [0u8; 32];
267        out.copy_from_slice(&zone[start..start + 32]);
268        Ok(out)
269    }
270
271    /// Write one 32-byte block of the configuration zone (provisioning).
272    ///
273    /// The ATECC608B's `Write` command refuses to touch certain regions of
274    /// the configuration zone, even before lock. The reference Microchip
275    /// routine `calib_write_bytes_zone` in `CryptoAuthLib`
276    /// (`lib/calib/calib_basic.c`) handles this by switching from 32-byte
277    /// to 4-byte transfers on the affected blocks and by skipping the
278    /// non-writable words entirely. We follow the same strategy.
279    ///
280    /// Concretely:
281    ///
282    /// - **Block 0 (chip-side bytes 0..32).** Words 0..=3 (bytes 0..16) are
283    ///   the read-only factory area (serial, `RevNum`, reserved). Any 32-byte
284    ///   write that includes them is rejected with `ParseError 0x03`. Words
285    ///   4..=7 (bytes 16..32) are written one at a time in 4-byte mode.
286    /// - **Block 1 (bytes 32..64).** All writable. Single 32-byte write.
287    /// - **Block 2 (bytes 64..96).** Word 5 (bytes 84..88) covers
288    ///   `UserExtra`, Selector, `LockValue`, and `LockConfig`. Those are modified
289    ///   only via the dedicated `UpdateExtra` and `Lock` commands; the
290    ///   `Write` command rejects the whole 32-byte transfer if word 5 is
291    ///   part of it. Words 0..=4 and 6..=7 are written one at a time in
292    ///   4-byte mode; word 5 is skipped entirely. **The host-side blob's
293    ///   bytes 84..88 are therefore ignored by the chip**, make sure the
294    ///   factory defaults (`0x00 0x00 0x55 0x55`) match what the blob
295    ///   contains for these positions, or call `UpdateExtra` /  `Lock`
296    ///   separately if a different value is desired.
297    /// - **Block 3 (bytes 96..128).** All writable. Single 32-byte write.
298    ///
299    /// This operation is **reversible** while the config zone is unlocked
300    /// (`LockConfig != 0`). Once `LockConfigZone` has been issued, every
301    /// chip-side Write here will return a chip error.
302    ///
303    /// `block` must be in `0..=3`.
304    ///
305    /// # Errors
306    /// - [`CryptoServiceError::InvalidFormat`] if `block > 3`.
307    /// - [`CryptoServiceError::Atecc`] on chip-level failures.
308    pub async fn write_config_block
309    (
310        &mut self,
311        block: u8,
312        data: &[u8; 32],
313    ) -> ServiceResult<(), H::Error>
314    {
315        if block > 3
316        {
317            return Err(CryptoServiceError::InvalidFormat(FormatError::OutOfRange));
318        }
319        let mut channel = self.atecc.open_channel().await?;
320        if can_write_block_in_one_transfer(block)
321        {
322            let address = config_or_otp_address(block, 0);
323            channel.write_32(Zone::Config, address, data).await?;
324        }
325        else
326        {
327            // Word-by-word path for blocks that contain non-writable words.
328            // Driven by `writable_words_in_block` which encodes, per block,
329            // the exact set of word offsets the chip's Write command will
330            // accept. Words outside that set are silently skipped here.
331            for &word_offset in writable_words_in_block(block)
332            {
333                let address = config_or_otp_address(block, word_offset);
334                let payload_off = usize::from(word_offset) * 4;
335                // Copy the 4 source bytes into a fresh array. The slice
336                // bounds are statically valid (word_offset is in 0..=7,
337                // payload_off in 0..=28, data is fixed `&[u8; 32]`), so
338                // the index range is always in-bounds. A copy is used
339                // instead of `try_into` to keep this path free of any
340                // unreachable `expect` / `unwrap`.
341                let mut chunk = [0u8; 4];
342                chunk.copy_from_slice(&data[payload_off..payload_off + 4]);
343                channel.write_4(Zone::Config, address, &chunk).await?;
344            }
345        }
346        channel.close().await?;
347        Ok(())
348    }
349
350    /// Open a PIN session if the provided PIN is correct.
351    ///
352    /// The `CheckMac` of slot 5 bumps `Counter0` regardless of the outcome,
353    /// so each call costs one count. On success, the service bumps the
354    /// counter additionally to bring it back up to the next multiple of
355    /// [`PIN_MAX_RETRIES`], granting the user a fresh batch of 5 attempts.
356    ///
357    /// # Errors
358    /// - [`CryptoServiceError::InvalidFormat`] if `pin` is not 4 ASCII digits.
359    /// - [`CryptoServiceError::PinIncorrect`] with tries remaining.
360    /// - [`CryptoServiceError::PinBlocked`] if Counter0 already at threshold.
361    /// - [`CryptoServiceError::Atecc`] for I/O / chip errors.
362    pub async fn verify_pin
363    (
364        &mut self,
365        pin: &[u8; PIN_LEN],
366    ) -> ServiceResult<(), H::Error>
367    {
368        validate_digits(pin)?;
369
370        // Refuse early if PIN slot is already exhausted.
371        let count = self.read_counter(CounterId::Counter0).await?;
372        let tries_left = retries_remaining(count, PIN_MAX_RETRIES);
373        if tries_left == 0
374        {
375            return Err(CryptoServiceError::PinBlocked);
376        }
377
378        let serial = self.cached_serial().await?;
379        let salt = pin_salt(&serial);
380        let computed_pin_hash = pin_hash(*pin, &salt);
381
382        let ok = self
383            .checkmac_with_hash(SLOT_PIN_HASH, &computed_pin_hash, &serial)
384            .await?;
385
386        if !ok
387        {
388            // The chip bumped Counter0 by 1 already. Report the new tries
389            // remaining to the caller.
390            let new_count = self.read_counter(CounterId::Counter0).await?;
391            let remaining = retries_remaining(new_count, PIN_MAX_RETRIES);
392            return Err(CryptoServiceError::PinIncorrect { tries_remaining: remaining });
393        }
394
395        // Successful verify: the chip bumped Counter0 by 1, but the user
396        // has earned a fresh batch of attempts. Bump it up to the next
397        // multiple of PIN_MAX_RETRIES.
398        self.refresh_counter_batch(CounterId::Counter0, PIN_MAX_RETRIES).await?;
399
400        // Open the session.
401        let now = self.clock.now_ms();
402        self.session.open(now);
403        Ok(())
404    }
405
406    /// Reset the PIN slot via the PUK.
407    ///
408    /// On success, slot 5 is rewritten with the SHA-256 of the new PIN and
409    /// `Counter0` is refreshed back to a fresh batch.
410    ///
411    /// `io_key` is the 32-byte I/O Protection Key (slot 8 content) known
412    /// to the host that performed provisioning. The service uses it to
413    /// build the encrypted-write payload. The key is never stored in the
414    /// service. It lives only for the duration of this call.
415    ///
416    /// # Errors
417    /// See [`CryptoServiceError`] variants.
418    pub async fn unblock_pin
419    (
420        &mut self,
421        puk: &[u8; PUK_LEN],
422        new_pin: &[u8; PIN_LEN],
423        io_key: &[u8; SLOT_VALUE_LEN],
424    ) -> ServiceResult<(), H::Error>
425    {
426        validate_digits(puk)?;
427        validate_digits(new_pin)?;
428
429        // Refuse early if PUK is already exhausted.
430        let count = self.read_counter(CounterId::Counter1).await?;
431        let tries_left = retries_remaining(count, PUK_MAX_RETRIES);
432        if tries_left == 0
433        {
434            return Err(CryptoServiceError::Bricked);
435        }
436
437        let serial = self.cached_serial().await?;
438        let salt = puk_salt(&serial);
439        let computed_puk_hash = puk_hash(*puk, &salt);
440
441        let ok = self
442            .checkmac_with_hash(SLOT_PUK_HASH, &computed_puk_hash, &serial)
443            .await?;
444
445        if !ok
446        {
447            let new_count = self.read_counter(CounterId::Counter1).await?;
448            let remaining = retries_remaining(new_count, PUK_MAX_RETRIES);
449            return Err(CryptoServiceError::PukIncorrect { tries_remaining: remaining });
450        }
451
452        // PUK was correct. Compute the new PIN hash and rewrite slot 5.
453        let new_pin_hash = pin_hash(*new_pin, &pin_salt(&serial));
454        self.write_slot_encrypted(SLOT_PIN_HASH, &new_pin_hash, io_key, &serial)
455            .await?;
456
457        // Refresh PUK counter to a fresh batch. Done AFTER the encrypted
458        // write rather than before, so that a failure during the write
459        // (NACK, comm error, etc) leaves the PUK counter consumed instead
460        // of granting a free retry window. Conservative trade-off: a
461        // valid PUK followed by a hardware glitch costs one PUK attempt.
462        self.refresh_counter_batch(CounterId::Counter1, PUK_MAX_RETRIES).await?;
463
464        // Reset PIN counter too: the user got a fresh PIN slate.
465        self.refresh_counter_batch(CounterId::Counter0, PIN_MAX_RETRIES).await?;
466
467        Ok(())
468    }
469
470    /// Change the PIN.
471    ///
472    /// Defence in depth: the caller must supply both the current PIN
473    /// (`old_pin`) and the new one. The current PIN is re-checked
474    /// against slot 5 via the same `CheckMac` flow as [`Self::verify_pin`].
475    /// This protects against the case where a USB session is hijacked
476    /// while a PIN session is open. Knowing the active session is not
477    /// enough to rotate the PIN.
478    ///
479    /// On success, the PIN session is opened (or refreshed). Slot 5 is
480    /// rewritten with `SHA-256(new_pin || pin_salt)` via the
481    /// encrypted-write protocol. Counter0 is refreshed.
482    ///
483    /// # Errors
484    /// See [`CryptoServiceError`] variants.
485    pub async fn set_pin
486    (
487        &mut self,
488        old_pin: &[u8; PIN_LEN],
489        new_pin: &[u8; PIN_LEN],
490        io_key: &[u8; SLOT_VALUE_LEN],
491    ) -> ServiceResult<(), H::Error>
492    {
493        validate_digits(old_pin)?;
494        validate_digits(new_pin)?;
495
496        // Refuse early if PIN slot is already exhausted.
497        let count = self.read_counter(CounterId::Counter0).await?;
498        let tries_left = retries_remaining(count, PIN_MAX_RETRIES);
499        if tries_left == 0
500        {
501            return Err(CryptoServiceError::PinBlocked);
502        }
503
504        let serial = self.cached_serial().await?;
505        let old_pin_hash = pin_hash(*old_pin, &pin_salt(&serial));
506
507        let ok = self
508            .checkmac_with_hash(SLOT_PIN_HASH, &old_pin_hash, &serial)
509            .await?;
510        if !ok
511        {
512            let new_count = self.read_counter(CounterId::Counter0).await?;
513            let remaining = retries_remaining(new_count, PIN_MAX_RETRIES);
514            return Err(CryptoServiceError::PinIncorrect { tries_remaining: remaining });
515        }
516
517        let new_pin_hash = pin_hash(*new_pin, &pin_salt(&serial));
518        self.write_slot_encrypted(SLOT_PIN_HASH, &new_pin_hash, io_key, &serial)
519            .await?;
520
521        // Successful PIN change: refresh counter and open / refresh PIN
522        // session.
523        self.refresh_counter_batch(CounterId::Counter0, PIN_MAX_RETRIES).await?;
524        let now = self.clock.now_ms();
525        self.session.open(now);
526        Ok(())
527    }
528
529    /// Change the PUK.
530    ///
531    /// Requires an active PIN session AND the current PUK. Defence in
532    /// depth: knowing the active session is not enough; the current PUK
533    /// is re-verified via `CheckMac` on slot 6, consuming one Counter1
534    /// attempt internally (refreshed on success).
535    ///
536    /// # Errors
537    /// See [`CryptoServiceError`] variants.
538    pub async fn set_puk
539    (
540        &mut self,
541        old_puk: &[u8; PUK_LEN],
542        new_puk: &[u8; PUK_LEN],
543        io_key: &[u8; SLOT_VALUE_LEN],
544    ) -> ServiceResult<(), H::Error>
545    {
546        validate_digits(old_puk)?;
547        validate_digits(new_puk)?;
548
549        // Step 1: PIN session must be active.
550        let now = self.clock.now_ms();
551        if !self.session.is_active(now)
552        {
553            return Err(CryptoServiceError::PinRequired);
554        }
555
556        // Step 2: refuse if Counter1 is exhausted (PUK bricked).
557        let count = self.read_counter(CounterId::Counter1).await?;
558        if retries_remaining(count, PUK_MAX_RETRIES) == 0
559        {
560            return Err(CryptoServiceError::Bricked);
561        }
562
563        // Step 3: verify the current PUK via CheckMac on slot 6.
564        let serial = self.cached_serial().await?;
565        let old_puk_hash = puk_hash(*old_puk, &puk_salt(&serial));
566        let ok = self
567            .checkmac_with_hash(SLOT_PUK_HASH, &old_puk_hash, &serial)
568            .await?;
569        if !ok
570        {
571            // The chip auto-bumped Counter1 by 1. Report the new
572            // tries-remaining.
573            let new_count = self.read_counter(CounterId::Counter1).await?;
574            let remaining = retries_remaining(new_count, PUK_MAX_RETRIES);
575            return Err(CryptoServiceError::PukIncorrect { tries_remaining: remaining });
576        }
577
578        // Step 4: write the new PUK hash.
579        let new_puk_hash = puk_hash(*new_puk, &puk_salt(&serial));
580        self.write_slot_encrypted(SLOT_PUK_HASH, &new_puk_hash, io_key, &serial)
581            .await?;
582
583        // Refresh Counter1 so the user starts a fresh PUK batch with
584        // the new PUK.
585        self.refresh_counter_batch(CounterId::Counter1, PUK_MAX_RETRIES).await?;
586
587        self.session.touch(now);
588        Ok(())
589    }
590
591    /// Last-chance reset for the case where the user has forgotten both
592    /// the PIN and the PUK and has exhausted both `LimitedUse` batches.
593    ///
594    /// This is the recovery path of last resort. 
595    /// Discards **everything user-owned** and rebuilds a clean baseline. 
596    /// The user loses the ECC private keys
597    /// in slots 0..=4 and 7 forever.
598    ///
599    /// # Preconditions
600    ///
601    /// The service refuses to perform this operation unless **both**
602    /// counters report zero attempts remaining. This is the hard
603    /// guarantee that prevents the call from being a back door:
604    ///
605    /// - If the user has forgotten only the PIN but not the PUK, they
606    ///   should use [`Self::unblock_pin`].
607    /// - Only the combined "PIN forgotten + PUK forgotten + both
608    ///   batches exhausted" state authorises `emergency_reset`.
609    ///
610    /// # What it does
611    ///
612    /// 1. Verify the precondition: both Counter0 and Counter1 are at a
613    ///    multiple of their respective `batch_size` with value > 0.
614    /// 2. Regenerate ECC keys in slots 0, 1, 2, 3, 4, 7.
615    /// 3. Generate a fresh random 8-digit PUK on-chip, write its hash
616    ///    to slot 6, refresh Counter1.
617    /// 4. Reset slot 5 to `SHA-256("0000" || pin_salt)`, refresh
618    ///    Counter0.
619    /// 5. Return the new PUK to the caller for one-time display.
620    ///
621    /// # What it does NOT do
622    ///
623    /// - It cannot reset Counter0 or Counter1 to zero. Both counters
624    ///   are bumped further during the recovery (one increment to
625    ///   reach `multiple + 1` on each). The user is granted one fresh
626    ///   batch of PIN attempts and one fresh batch of PUK attempts.
627    /// - It does **not** rewrite slot 8 (IO key). The caller must
628    ///   supply the IO key, which is stored host-side (the host knows
629    ///   it from provisioning).
630    /// - If the chip's hardware counter limit (2^21) is reached during
631    ///   the refresh increments, the operation reports an error and
632    ///   the chip is genuinely bricked. There is nothing more software
633    ///   can do.
634    ///
635    /// # Errors
636    /// - [`CryptoServiceError::EmergencyResetNotPermitted`] if either
637    ///   counter still has attempts remaining.
638    /// - [`CryptoServiceError::Bricked`] if the chip's hardware counter
639    ///   limit is hit during refresh.
640    /// - [`CryptoServiceError::Atecc`] for chip-level errors.
641    pub async fn emergency_reset
642    (
643        &mut self,
644        io_key: &[u8; SLOT_VALUE_LEN],
645    ) -> ServiceResult<[u8; PUK_LEN], H::Error>
646    {
647        // Step 1: precondition check. Both counters must be saturated.
648        let c0 = self.read_counter(CounterId::Counter0).await?;
649        let c1 = self.read_counter(CounterId::Counter1).await?;
650        let pin_left = retries_remaining(c0, PIN_MAX_RETRIES);
651        let puk_left = retries_remaining(c1, PUK_MAX_RETRIES);
652        if pin_left != 0 || puk_left != 0
653        {
654            return Err(CryptoServiceError::EmergencyResetNotPermitted
655            {
656                pin_tries_remaining: pin_left,
657                puk_tries_remaining: puk_left,
658            });
659        }
660
661        let serial = self.cached_serial().await?;
662
663        // Step 2: regenerate identity ECC keys. We do this BEFORE the
664        // counter refresh so that if any GenKey fails (e.g. counter
665        // hardware-bricked at 2^21) we have not yet consumed precious
666        // counter cycles for nothing.
667        //
668        // All six keys are regenerated within a single channel: GenKey
669        // does not share volatile state with other commands, but reusing
670        // the channel avoids six wake/idle round-trips.
671        {
672            let mut channel = self.atecc.open_channel().await?;
673            for slot_idx in [0u8, 1, 2, 3, 4, 7]
674            {
675                let slot = Slot::const_new(slot_idx);
676                let _ = channel.genkey_create(slot).await?;
677            }
678            channel.close().await?;
679        }
680
681        // Step 3: generate a fresh PUK, write its hash, refresh Counter1.
682        let new_puk = self.generate_random_puk().await?;
683        let new_puk_hash = puk_hash(new_puk, &puk_salt(&serial));
684        self.write_slot_encrypted(SLOT_PUK_HASH, &new_puk_hash, io_key, &serial).await?;
685        self.refresh_counter_batch(CounterId::Counter1, PUK_MAX_RETRIES).await?;
686
687        // Step 4: reset PIN to default, refresh Counter0.
688        let default_hash = pin_hash(PIN_DEFAULT, &pin_salt(&serial));
689        self.write_slot_encrypted(SLOT_PIN_HASH, &default_hash, io_key, &serial).await?;
690        self.refresh_counter_batch(CounterId::Counter0, PIN_MAX_RETRIES).await?;
691
692        // No PIN session to close. There was none to begin with (the
693        // caller has no PIN to verify with).
694        Ok(new_puk)
695    }
696
697    /// Pull 8 ASCII digits from the chip's RNG to produce a new PUK.
698    /// The distribution is uniform over `[b'0'..=b'9']`, obtained by
699    /// modulo on each random byte (one random byte yields one digit).
700    /// 32 bytes of random are pulled for an 8-byte PUK, which leaves
701    /// ample headroom if any byte ever turned out to be unusable.
702    async fn generate_random_puk
703    (
704        &mut self,
705    ) -> ServiceResult<[u8; PUK_LEN], H::Error>
706    {
707        let random =
708        {
709            let mut channel = self.atecc.open_channel().await?;
710            let random = channel.random().await?;
711            channel.close().await?;
712            random
713        };
714        let mut puk = [b'0'; PUK_LEN];
715        // Modulo 10 introduces a tiny bias (256 % 10 = 6 over the first 6 digits)
716        // but the difference is negligible for an 8-digit PUK and the chip's RNG
717        // is uniform on full bytes.
718        for (digit, &r) in puk.iter_mut().zip(random.iter())
719        {
720            *digit = b'0' + (r % 10);
721        }
722        Ok(puk)
723    }
724
725    // -------------------------------------------------------------------
726    // Lock operations -- IRREVERSIBLE
727    // -------------------------------------------------------------------
728    //
729    // These three methods are the only ones in the service that bridge
730    // to the driver's lock functions. They are kept together at
731    // the bottom of the file for visibility.
732    //
733    // For the configuration zone the service does NOT compute the CRC
734    // itself. The host CLI reads the chip's current configuration zone,
735    // computes the CRC over the full 128 bytes, and passes the result
736    // through. The chip is the second line of defence: it recomputes
737    // the CRC of its own current state and refuses the lock if it does
738    // not match. The service is the third line of defence: it does not
739    // expose a "lock with no checks" path at all.
740    //
741    // For the data zone there is no CRC. Every secret-bearing slot has
742    // `IsSecret=1`, so the host cannot read the contents back to compute
743    // one. The double-confirmation prompt in the host CLI and the
744    // magic-word check in the firmware are the only guards.
745
746    /// Permanently lock the configuration zone.
747    ///
748    /// `expected_crc` is the CRC-16/CCITT of the configuration zone as
749    /// the host expects it to be. The chip recomputes its own and
750    /// rejects the lock if the two disagree.
751    ///
752    /// **Irreversible.** See [`atecc608b::AteccChannel::lock_config_zone`].
753    ///
754    /// # Errors
755    /// - [`CryptoServiceError::Atecc`] if the chip refuses (typically
756    ///   because the CRC does not match).
757    pub async fn lock_config_zone
758    (
759        &mut self,
760        expected_crc: u16,
761    ) -> ServiceResult<(), H::Error>
762    {
763        let mut channel = self.atecc.open_channel().await?;
764        // Best effort close on error: capture the lock result and always
765        // attempt to close. Idle preserves volatile state (which is none
766        // here, the lock command leaves no `TempKey`) and resets the
767        // watchdog so the next channel sees a clean baseline.
768        let result = channel.lock_config_zone(expected_crc).await;
769        channel.close().await?;
770        result?;
771        Ok(())
772    }
773
774    /// Permanently lock the data + OTP zones.
775    ///
776    /// **Irreversible.** See [`atecc608b::AteccChannel::lock_data_zone`].
777    /// No CRC is checked at the chip level: secret-bearing slots
778    /// (`IsSecret=1`) cannot be read back to compute one. The double
779    /// confirmation prompt in the host CLI and the magic-word check in
780    /// the firmware are the only guards.
781    ///
782    /// # Errors
783    /// - [`CryptoServiceError::Atecc`] if the chip refuses.
784    pub async fn lock_data_zone
785    (
786        &mut self,
787    ) -> ServiceResult<(), H::Error>
788    {
789        let mut channel = self.atecc.open_channel().await?;
790        let result = channel.lock_data_zone().await;
791        channel.close().await?;
792        result?;
793        Ok(())
794    }
795
796    /// Permanently lock an individual slot. The slot's config in the
797    /// configuration zone must have `Lockable=1` for this to succeed.
798    ///
799    /// **Irreversible.** See [`atecc608b::AteccChannel::lock_slot`].
800    ///
801    /// # Errors
802    /// - [`CryptoServiceError::Atecc`] if the chip refuses (slot not
803    ///   lockable, or already locked).
804    pub async fn lock_slot
805    (
806        &mut self,
807        slot: Slot,
808    ) -> ServiceResult<(), H::Error>
809    {
810        let mut channel = self.atecc.open_channel().await?;
811        let result = channel.lock_slot(slot).await;
812        channel.close().await?;
813        result?;
814        Ok(())
815    }
816
817    // -------------------------------------------------------------------
818    // Provisioning (data zone unlocked)
819    // -------------------------------------------------------------------
820
821    /// Write a 32-byte value in cleartext into one of the data slots.
822    ///
823    /// **Only legal while the data zone is unlocked.** After
824    /// [`Self::lock_data_zone`], writes to data slots must go through
825    /// [`Self::write_slot_encrypted`].
826    ///
827    /// Restricted by policy to the three slots that hold project-level
828    /// secrets and need to be initialised before lock:
829    ///
830    /// - Slot 5 (PIN hash). Written with `SHA-256("0000" || pin_salt)`
831    ///   to set the default PIN. Salt is derived from the chip serial.
832    /// - Slot 6 (PUK hash). Written with `SHA-256(random_puk || puk_salt)`.
833    /// - Slot 8 (IO Protection Key). Written with a random 32-byte
834    ///   value, kept secret host-side for later encrypted writes.
835    ///
836    /// Other slots are rejected at the service layer to avoid mistakes.
837    /// ECC slots (0..=4, 7) are populated via `GenKey`, not by write.
838    /// Reserve slots (9..=15) are kept unprovisioned for V2.
839    ///
840    /// # Errors
841    /// - [`CryptoServiceError::InvalidSlot`] if the slot is not one of
842    ///   the three policy-allowed targets.
843    /// - [`CryptoServiceError::Atecc`] if the chip refuses the write
844    ///   (most likely because the data zone is already locked).
845    pub async fn provision_slot
846    (
847        &mut self,
848        slot: Slot,
849        value: &[u8; 32],
850    ) -> ServiceResult<(), H::Error>
851    {
852        let allowed =
853            slot == SLOT_PIN_HASH || slot == SLOT_PUK_HASH || slot == SLOT_IO_KEY;
854        if !allowed
855        {
856            return Err(CryptoServiceError::InvalidSlot { slot });
857        }
858        let address = atecc608b::command::read_write::data_address(slot, 0, 0);
859        let mut channel = self.atecc.open_channel().await?;
860        channel.write_32(Zone::Data, address, value).await?;
861        channel.close().await?;
862        Ok(())
863    }
864
865    /// Generate a fresh random PUK and write its hash into slot 6, in
866    /// cleartext. Returns the PUK to the caller for one-time display.
867    ///
868    /// Only legal while the data zone is unlocked: used during
869    /// provisioning to set the initial PUK before locking.
870    ///
871    /// # Errors
872    /// - [`CryptoServiceError::Atecc`] for chip-level errors.
873    pub async fn provision_initial_puk
874    (
875        &mut self,
876    ) -> ServiceResult<[u8; PUK_LEN], H::Error>
877    {
878        let serial = self.cached_serial().await?;
879        let new_puk = self.generate_random_puk().await?;
880        let new_puk_hash = puk_hash(new_puk, &puk_salt(&serial));
881        self.provision_slot(SLOT_PUK_HASH, &new_puk_hash).await?;
882        Ok(new_puk)
883    }
884
885    /// Write the hash of the default PIN ("0000") into slot 5 in
886    /// cleartext. Used at provisioning, before data lock.
887    ///
888    /// # Errors
889    /// - [`CryptoServiceError::Atecc`] for chip-level errors.
890    pub async fn provision_initial_pin
891    (
892        &mut self,
893    ) -> ServiceResult<(), H::Error>
894    {
895        let serial = self.cached_serial().await?;
896        let hash = pin_hash(PIN_DEFAULT, &pin_salt(&serial));
897        self.provision_slot(SLOT_PIN_HASH, &hash).await?;
898        Ok(())
899    }
900
901    /// Generate a fresh random 32-byte I/O Protection Key from the
902    /// chip's RNG, write it into slot 8 in cleartext, and return it
903    /// to the caller.
904    ///
905    /// **The caller must persist this value immediately**: it is the
906    /// only opportunity to learn the IO key. After data lock, slot 8
907    /// becomes write-only via the encrypted-write protocol, which
908    /// itself depends on knowing the IO key. Losing the IO key turns
909    /// the chip into a permanently degraded device (no more PIN/PUK
910    /// changes possible).
911    ///
912    /// Only legal while the data zone is unlocked.
913    ///
914    /// # Errors
915    /// - [`CryptoServiceError::Atecc`] for chip-level errors.
916    pub async fn provision_initial_io_key
917    (
918        &mut self,
919    ) -> ServiceResult<[u8; SLOT_VALUE_LEN], H::Error>
920    {
921        let io_key =
922        {
923            let mut channel = self.atecc.open_channel().await?;
924            let io_key = channel.random().await?;
925            channel.close().await?;
926            io_key
927        };
928        self.provision_slot(SLOT_IO_KEY, &io_key).await?;
929        Ok(io_key)
930    }
931
932    /// Sign a 32-byte digest with the private key in `slot`.
933    ///
934    /// Requires an active PIN session.
935    ///
936    /// Internally this is a two-step chip workflow on the ATECC608:
937    /// `Nonce(passthrough, target=MsgDigBuf)` then
938    /// `Sign(external, source=MsgDigBuf, slot)`. Both steps run inside a
939    /// single chip channel so the `MsgDigBuf` register survives between
940    /// them. The `MsgDigBuf` source (rather than the older TempKey-based
941    /// path used by the 508A) is required on the 608 to make the chip
942    /// sign the supplied digest verbatim, without blending in any other
943    /// chip state. Returned `R || S` is 64 bytes big-endian.
944    ///
945    /// # Errors
946    /// - [`CryptoServiceError::PinRequired`] if no session is active.
947    /// - [`CryptoServiceError::Atecc`] for chip-level errors (slot
948    ///   misconfigured, authorization missing, etc).
949    pub async fn sign
950    (
951        &mut self,
952        slot: Slot,
953        digest: &[u8; 32],
954    ) -> ServiceResult<Signature, H::Error>
955    {
956        let now = self.clock.now_ms();
957        if !self.session.is_active(now)
958        {
959            return Err(CryptoServiceError::PinRequired);
960        }
961
962        // Load the digest into the Message Digest Buffer via passthrough
963        // Nonce, then Sign with mode = external + source=MsgDigBuf. Both
964        // commands must run inside the same channel: MsgDigBuf is volatile
965        // and idling the chip clears it.
966        //
967        // On the ATECC608 specifically, Sign(external) requires the
968        // MsgDigBuf source bit. Using TempKey instead (as the older 508A
969        // protocol did) makes the chip blend extra context bytes into the
970        // signed message, producing a signature that does not verify
971        // off-chip against the raw 32-byte digest. See
972        // `lib/calib/calib_sign.c::calib_sign` in CryptoAuthLib for the
973        // device-type branch that selects MsgDigBuf for ATECC608.
974        let signature =
975        {
976            let mut channel = self.atecc.open_channel().await?;
977            channel.nonce_passthrough(NonceTarget::MsgDigBuf, digest).await?;
978            let signature = channel.sign_external(slot).await?;
979            channel.close().await?;
980            signature
981        };
982
983        // Refresh session activity timestamp.
984        self.session.touch(now);
985        Ok(signature)
986    }
987
988    /// Report the current PIN / PUK retry counters and session state.
989    ///
990    /// Reads both counters within a single chip channel for efficiency.
991    ///
992    /// # Errors
993    /// See [`CryptoServiceError::Atecc`].
994    pub async fn get_pin_status(&mut self) -> ServiceResult<PinStatus, H::Error>
995    {
996        let (c0, c1) =
997        {
998            let mut channel = self.atecc.open_channel().await?;
999            let c0 = channel.counter_read(CounterId::Counter0).await?;
1000            let c1 = channel.counter_read(CounterId::Counter1).await?;
1001            channel.close().await?;
1002            (c0, c1)
1003        };
1004        Ok(PinStatus
1005        {
1006            pin_tries_remaining: retries_remaining(c0, PIN_MAX_RETRIES),
1007            puk_tries_remaining: retries_remaining(c1, PUK_MAX_RETRIES),
1008            session_active:      self.session.is_active(self.clock.now_ms()),
1009        })
1010    }
1011
1012    /// Terminate the active PIN session immediately, without waiting for
1013    /// the 30 s inactivity timeout.
1014    ///
1015    /// Idempotent: closing an already-closed session is a no-op. Useful
1016    /// when the user wants to lock the dongle proactively after a
1017    /// signing burst, instead of letting the timeout expire.
1018    pub fn close_session(&mut self)
1019    {
1020        self.session.close();
1021    }
1022
1023    /// Return whether a PIN session is currently active.
1024    ///
1025    /// Used by command handlers that need to refuse pre-touch on
1026    /// session-gated operations (notably `Sign`): without this early
1027    /// check the firmware would arm the touch wait then time out 30
1028    /// seconds later instead of failing immediately with
1029    /// [`CryptoServiceError::PinRequired`].
1030    #[must_use]
1031    pub fn is_session_active(&self) -> bool
1032    {
1033        self.session.is_active(self.clock.now_ms())
1034    }
1035
1036    /// Read one 32-byte block from a data slot.
1037    ///
1038    /// Used by `CommandOpcode::ReadSlotBlock` for bring-up diagnostics
1039    /// (verifying what `ProvisionSlot` wrote) and to inspect the IO key
1040    /// in slot 8 before locking the data zone.
1041    ///
1042    /// No PIN session check here: the chip itself enforces slot policy.
1043    /// A slot configured `IsSecret` or with `EncryptRead` returns a chip
1044    /// error, which surfaces here as [`CryptoServiceError::Atecc`].
1045    ///
1046    /// # Errors
1047    /// - [`CryptoServiceError::Atecc`] on chip-level errors (forbidden
1048    ///   read, bad address, etc).
1049    pub async fn read_slot_block
1050    (
1051        &mut self,
1052        slot: Slot,
1053        block: u8,
1054    ) -> ServiceResult<[u8; 32], H::Error>
1055    {
1056        let mut channel = self.atecc.open_channel().await?;
1057        let data = channel.read_slot_block(slot, block).await?;
1058        channel.close().await?;
1059        Ok(data)
1060    }
1061
1062    /// Read one 4-byte word from a data slot.
1063    ///
1064    /// See [`Self::read_slot_block`] for context. Same policy enforcement.
1065    ///
1066    /// # Errors
1067    /// - [`CryptoServiceError::Atecc`] on chip-level errors.
1068    pub async fn read_slot_word
1069    (
1070        &mut self,
1071        slot: Slot,
1072        block: u8,
1073        offset_words: u8,
1074    ) -> ServiceResult<[u8; 4], H::Error>
1075    {
1076        let mut channel = self.atecc.open_channel().await?;
1077        let data = channel.read_slot_word(slot, block, offset_words).await?;
1078        channel.close().await?;
1079        Ok(data)
1080    }
1081
1082    /// Read the raw value of one of the chip's monotonic counters.
1083    ///
1084    /// Returns the binary count as decoded by the chip (the chip's
1085    /// `Counter(mode=Read)` command performs the popcount-style decoding
1086    /// of its 8-byte storage into a `u32`). The returned value starts at
1087    /// 0 on a factory-fresh chip and increments by 1 on every key-usage
1088    /// event for keys whose `SlotConfig.LimitedUse == 1`.
1089    ///
1090    /// Used internally by [`Self::verify_pin`] and the PIN/PUK retry
1091    /// arithmetic; exposed publicly so the host CLI can read the raw
1092    /// value for bring-up diagnostics without going through
1093    /// `retries_remaining`.
1094    ///
1095    /// # Errors
1096    /// - [`CryptoServiceError::Atecc`] on chip-level failures.
1097    pub async fn read_counter
1098    (
1099        &mut self,
1100        counter: CounterId,
1101    ) -> ServiceResult<u32, H::Error>
1102    {
1103        let mut channel = self.atecc.open_channel().await?;
1104        let count = channel.counter_read(counter).await?;
1105        channel.close().await?;
1106        Ok(count)
1107    }
1108
1109    // -------------------------------------------------------------------
1110    // Private helpers (own their channel(s))
1111    // -------------------------------------------------------------------
1112
1113    /// Read the chip's 9-byte serial number, caching it on first call.
1114    async fn cached_serial(&mut self) -> ServiceResult<[u8; CHIP_SERIAL_LEN], H::Error>
1115    {
1116        if let Some(serial) = self.serial
1117        {
1118            return Ok(serial);
1119        }
1120
1121        let mut config = [0u8; 128];
1122        {
1123            let mut channel = self.atecc.open_channel().await?;
1124            channel.read_config_zone(&mut config).await?;
1125            channel.close().await?;
1126        }
1127        // SN layout per ATECC608 config zone:
1128        //   bytes 0..4 = SN[0..4]
1129        //   bytes 8..13 = SN[4..9]
1130        let mut serial = [0u8; CHIP_SERIAL_LEN];
1131        serial[0..4].copy_from_slice(&config[0..4]);
1132        serial[4..9].copy_from_slice(&config[8..13]);
1133        self.serial = Some(serial);
1134        Ok(serial)
1135    }
1136
1137    /// Check whether the chip is in its operational locked state.
1138    ///
1139    /// Inspects the config zone lock byte at offset 87. `0x55` = unlocked,
1140    /// `0x00` = locked. Data zone lock is at offset 86.
1141    async fn is_provisioned(&mut self) -> ServiceResult<bool, H::Error>
1142    {
1143        let mut config = [0u8; 128];
1144        {
1145            let mut channel = self.atecc.open_channel().await?;
1146            channel.read_config_zone(&mut config).await?;
1147            channel.close().await?;
1148        }
1149        let config_locked = config[87] == 0x00;
1150        let data_locked = config[86] == 0x00;
1151        Ok(config_locked && data_locked)
1152    }
1153
1154    /// Issue a `CheckMac` against `slot` with the host-computed hash that
1155    /// should match its content, and return whether the chip confirmed.
1156    ///
1157    /// Random + `CheckMac` run inside the same chip channel: this avoids two
1158    /// wake / idle round-trips and matches the natural "ask for a challenge,
1159    /// then use it" flow.
1160    async fn checkmac_with_hash
1161    (
1162        &mut self,
1163        slot: Slot,
1164        expected_slot_value: &[u8; HASH_LEN],
1165        serial: &[u8; CHIP_SERIAL_LEN],
1166    ) -> ServiceResult<bool, H::Error>
1167    {
1168        let mut channel = self.atecc.open_channel().await?;
1169        // Generate a fresh 32-byte challenge from the chip's RNG.
1170        let challenge = channel.random().await?;
1171        let other_data = checkmac_other_data(slot.as_u8(), serial);
1172        let response = checkmac_response(expected_slot_value, &challenge, &other_data, serial);
1173
1174        let result = channel.checkmac(slot, &challenge, &response, &other_data).await;
1175        channel.close().await?;
1176
1177        match result
1178        {
1179            Ok(true) => Ok(true),
1180            Ok(false) => Ok(false),
1181            // Some chip errors here may indicate a depleted counter. Map
1182            // them to a PinBlocked / Bricked outcome higher up. The driver
1183            // returns Chip(ExecutionError) for over-limit counters.
1184            Err(other) => Err(CryptoServiceError::Atecc(other)),
1185        }
1186    }
1187
1188    /// Increment `counter` until its value lands one past a multiple of
1189    /// `batch_size`, i.e. `count % batch_size == 1`.
1190    ///
1191    /// Called after a successful PIN or PUK verify to grant the user a
1192    /// fresh batch of attempts while keeping `count % batch == 0` as
1193    /// an **unambiguous saturation indicator**: if a future
1194    /// [`retries_remaining`] observes `count % batch == 0` with
1195    /// `count > 0`, it can conclude that the user has consumed a full
1196    /// batch without any successful verify in between (and route to
1197    /// emergency recovery).
1198    ///
1199    /// The cost is one attempt per refresh: a PIN batch effectively
1200    /// allows 4 tries (rather than 5), a PUK batch 9 (rather than 10).
1201    /// In exchange the host gains a reliable way to detect saturation,
1202    /// which enables [`Self::emergency_reset`].
1203    ///
1204    /// Reads the counter and all increments share one channel.
1205    async fn refresh_counter_batch
1206    (
1207        &mut self,
1208        counter: CounterId,
1209        batch_size: u8,
1210    ) -> ServiceResult<(), H::Error>
1211    {
1212        let mut channel = self.atecc.open_channel().await?;
1213        let current = channel.counter_read(counter).await?;
1214        let batch = u32::from(batch_size);
1215        let remainder = current % batch;
1216        // Target: remainder == 1 after refresh. Number of bumps:
1217        // - remainder == 0 : bump 1 (lands on multiple + 1).
1218        // - remainder == 1 : 0 bumps (already there).
1219        // - remainder == r > 1 : bump `(batch - r) + 1` to skip past
1220        //   the next multiple and land on multiple + 1.
1221        let bumps = if remainder == 0
1222        {
1223            1u32
1224        }
1225        else if remainder == 1
1226        {
1227            0u32
1228        }
1229        else
1230        {
1231            batch - remainder + 1
1232        };
1233
1234        for _ in 0..bumps
1235        {
1236            // Tolerate the chip refusing to bump further (counter at
1237            // its 2^21 hardware max).
1238            match channel.counter_increment(counter).await
1239            {
1240                Ok(_) => {}
1241                Err(AteccError::Chip(ChipError::ExecutionError)) =>
1242                {
1243                    channel.close().await?;
1244                    return Ok(());
1245                }
1246                Err(other) =>
1247                {
1248                    // Best effort close on error: ignore the close result.
1249                    let _ = channel.close().await;
1250                    return Err(CryptoServiceError::Atecc(other));
1251                }
1252            }
1253        }
1254        channel.close().await?;
1255        Ok(())
1256    }
1257
1258    /// Perform an encrypted 32-byte write into `target_slot`.
1259    ///
1260    /// Sequence:
1261    ///
1262    /// 1. Read a fresh 32-byte random from the chip and load it into
1263    ///    `TempKey` via `Nonce(passthrough)`.
1264    /// 2. Issue `GenDig(zone=Data, key_id=IO_KEY_SLOT)`. `TempKey` becomes
1265    ///    the derived session key.
1266    /// 3. Host computes the same session key locally.
1267    /// 4. Host XOR-encrypts `plaintext` and computes the Write MAC.
1268    /// 5. `Write(slot, encrypted)` with `ciphertext || mac`.
1269    ///
1270    /// All chip commands run within a single channel because `TempKey`
1271    /// must survive from `Nonce` to the encrypted `Write`.
1272    ///
1273    /// Used by [`Self::set_pin`] and [`Self::unblock_pin`] to update the
1274    /// PIN hash in slot 5.
1275    async fn write_slot_encrypted
1276    (
1277        &mut self,
1278        target_slot: Slot,
1279        plaintext: &[u8; SLOT_VALUE_LEN],
1280        io_key: &[u8; SLOT_VALUE_LEN],
1281        chip_serial: &[u8; CHIP_SERIAL_LEN],
1282    ) -> ServiceResult<(), H::Error>
1283    {
1284        let mut channel = self.atecc.open_channel().await?;
1285
1286        // 1. Generate a fresh nonce input and load it into TempKey.
1287        let nonce_input = channel.random().await?;
1288        channel.nonce_passthrough(NonceTarget::TempKey, &nonce_input).await?;
1289
1290        // 2. GenDig on the I/O key slot. The chip updates TempKey.
1291        let io_slot = SLOT_IO_KEY.as_u8();
1292        channel.gendig(GenDigZone::Data, u16::from(io_slot)).await?;
1293
1294        // 3. Replicate the chip-side TempKey on the host.
1295        let session_key = derive_session_key(io_key, &nonce_input, io_slot, chip_serial);
1296
1297        // 4. Encrypt and MAC the plaintext.
1298        let ciphertext = encrypt_payload(plaintext, &session_key);
1299        let mac = write_mac
1300        (
1301            &session_key,
1302            plaintext,
1303            target_slot,
1304            0,
1305            chip_serial,
1306        );
1307        let payload = build_encrypted_write_payload(&ciphertext, &mac);
1308
1309        // 5. Write to the slot. Slot block 0, offset 0 (single 32-byte
1310        // block written).
1311        let address = data_address(target_slot, 0, 0);
1312        channel.write_32_encrypted(Zone::Data, address, &payload).await?;
1313
1314        channel.close().await?;
1315        Ok(())
1316    }
1317}
1318
1319/// Whether the chip accepts a single 32-byte `Write` for the whole block.
1320///
1321/// Only blocks 1 and 3 of the config zone are fully writable in one shot.
1322/// Blocks 0 and 2 contain words that the chip refuses to overwrite via
1323/// `Write` (factory area, `UserExtra`, `Selector`, `LockValue`,
1324/// `LockConfig`) and must be written word-by-word, skipping the
1325/// non-writable words.
1326///
1327/// Mirrors the `!(zone == ATCA_ZONE_CONFIG && cur_block == 2u)` and
1328/// implicit "block 0 starts at offset 16" logic from `CryptoAuthLib`'s
1329/// `calib_write_bytes_zone`. See `lib/calib/calib_basic.c` in the
1330/// reference Microchip library.
1331const fn can_write_block_in_one_transfer(block: u8) -> bool
1332{
1333    matches!(block, 1 | 3)
1334}
1335
1336/// Set of word offsets (within a 32-byte config-zone block) that the chip's
1337/// `Write` command accepts in 4-byte mode.
1338///
1339/// Used only when [`can_write_block_in_one_transfer`] returns `false`. For
1340/// blocks 1 and 3 this helper returns an empty slice; those blocks should
1341/// be written with a single 32-byte transfer instead.
1342///
1343/// - Block 0 : words 4..=7 are writable (chip-side bytes 16..32). Words
1344///   0..=3 (bytes 0..16) are the read-only factory area: serial number,
1345///   `RevNum`, `Reserved`. Any write attempt is rejected by the chip with
1346///   `ParseError 0x03`.
1347/// - Block 2 : words 0..=4 and 6..=7 are writable. Word 5 (bytes 84..88)
1348///   covers `UserExtra`, `Selector`, `LockValue`, and `LockConfig`. These
1349///   are modified only via the dedicated `UpdateExtra` and `Lock`
1350///   commands; the `Write` command rejects them. `CryptoAuthLib`'s
1351///   `calib_write_bytes_zone` skips word 5 of block 2 explicitly with
1352///   `!(zone == ATCA_ZONE_CONFIG && cur_block == 2u && cur_word == 5u)`.
1353///
1354/// The slice is returned in ascending order of word offset; callers iterate
1355/// in that order to match the wire sequence used by the reference library
1356/// and pinned down by the corresponding integration tests.
1357const fn writable_words_in_block(block: u8) -> &'static [u8]
1358{
1359    match block
1360    {
1361        0 => &[4, 5, 6, 7],
1362        2 => &[0, 1, 2, 3, 4, 6, 7],
1363        _ => &[],
1364    }
1365}
1366
1367/// Compute how many `CheckMac` attempts remain before the next batch
1368/// threshold is hit.
1369///
1370/// `batch_size` is `PIN_MAX_RETRIES` for PIN, `PUK_MAX_RETRIES` for PUK.
1371/// The convention is that `refresh_counter_batch` is called on every
1372/// successful verify to push `count` to a non-multiple-of-batch value
1373/// (specifically, `next_multiple + 1`). So in normal operation:
1374///
1375/// - `count == 0` : freshly-provisioned chip, no verify attempted yet,
1376///   `batch_size` attempts remain.
1377/// - `count % batch == 1` : freshly refreshed after a successful verify,
1378///   `batch_size - 1` attempts remain in the current batch.
1379/// - `count % batch == r` in `2..batch` : `batch_size - r` attempts remain.
1380/// - `count % batch == 0` with `count > 0` : SATURATION. The user has
1381///   consumed a full batch with no successful verify in between. Returns
1382///   0 attempts.
1383fn retries_remaining(count: u32, batch_size: u8) -> u8
1384{
1385    let batch = u32::from(batch_size);
1386    let remainder = count % batch;
1387
1388    if count == 0
1389    {
1390        return batch_size;
1391    }
1392    if remainder == 0
1393    {
1394        // Saturation: user consumed an entire batch without verifying.
1395        return 0;
1396    }
1397    if remainder == 1
1398    {
1399        // Freshly refreshed: full batch minus the refresh bump.
1400        return batch_size - 1;
1401    }
1402    // Mid-batch: batch - remainder is the count to the next multiple.
1403    // `remainder = count % batch` is in `0..batch <= 255`, so the cast
1404    // to `u8` is lossless. We have already returned for `remainder == 0`
1405    // and `remainder == 1` above. here `2 <= remainder < batch_size`.
1406    #[allow(clippy::cast_possible_truncation)]
1407    let remainder_u8 = remainder as u8;
1408    batch_size - remainder_u8
1409}
1410
1411#[cfg(test)]
1412mod tests
1413{
1414    use super::*;
1415
1416    #[test]
1417    fn retries_remaining_zero_returns_full_batch()
1418    {
1419        assert_eq!(retries_remaining(0, 5), 5);
1420        assert_eq!(retries_remaining(0, 10), 10);
1421    }
1422
1423    #[test]
1424    fn retries_remaining_remainder_one_returns_batch_minus_one()
1425    {
1426        // Just refreshed after a successful verify.
1427        assert_eq!(retries_remaining(1, 5), 4);
1428        assert_eq!(retries_remaining(6, 5), 4);
1429        assert_eq!(retries_remaining(11, 5), 4);
1430
1431        assert_eq!(retries_remaining(1, 10), 9);
1432        assert_eq!(retries_remaining(11, 10), 9);
1433    }
1434
1435    #[test]
1436    fn retries_remaining_mid_batch_counts_down()
1437    {
1438        // remainder 2 -> batch - 2 = 3 tries left (for batch 5).
1439        assert_eq!(retries_remaining(2, 5), 3);
1440        assert_eq!(retries_remaining(3, 5), 2);
1441        assert_eq!(retries_remaining(4, 5), 1);
1442    }
1443
1444    #[test]
1445    fn retries_remaining_saturation_at_multiple_returns_zero()
1446    {
1447        // The 5th, 10th, 15th attempt without a successful verify
1448        // lands count on a multiple of 5, signalling saturation.
1449        assert_eq!(retries_remaining(5, 5), 0);
1450        assert_eq!(retries_remaining(10, 5), 0);
1451        assert_eq!(retries_remaining(15, 5), 0);
1452        assert_eq!(retries_remaining(10, 10), 0);
1453        assert_eq!(retries_remaining(20, 10), 0);
1454    }
1455
1456    #[test]
1457    fn one_transfer_blocks_are_1_and_3_only()
1458    {
1459        assert!(!can_write_block_in_one_transfer(0));
1460        assert!( can_write_block_in_one_transfer(1));
1461        assert!(!can_write_block_in_one_transfer(2));
1462        assert!( can_write_block_in_one_transfer(3));
1463    }
1464
1465    #[test]
1466    fn writable_words_block_0_skips_factory_area()
1467    {
1468        // Factory area = bytes 0..16 = words 0..=3. Writable = words 4..=7.
1469        assert_eq!(writable_words_in_block(0), &[4, 5, 6, 7]);
1470    }
1471
1472    #[test]
1473    fn writable_words_block_2_skips_word_5()
1474    {
1475        // Word 5 = bytes 84..88 = UserExtra/Selector/LockValue/LockConfig.
1476        // Not writable via the `Write` command. The remaining words of
1477        // block 2 are writable: 0..=4 and 6..=7.
1478        assert_eq!(writable_words_in_block(2), &[0, 1, 2, 3, 4, 6, 7]);
1479    }
1480
1481    #[test]
1482    fn writable_words_blocks_1_and_3_are_empty_word_lists()
1483    {
1484        // Blocks 1 and 3 are written via 32-byte transfer; the
1485        // word-by-word helper returns an empty list for them.
1486        assert!(writable_words_in_block(1).is_empty());
1487        assert!(writable_words_in_block(3).is_empty());
1488    }
1489
1490    #[test]
1491    fn writable_words_one_transfer_and_word_list_are_disjoint()
1492    {
1493        // Encoded contract: a block is either written in one 32-byte
1494        // transfer, or via the word list. Never both, never neither.
1495        for block in 0u8..=3u8
1496        {
1497            let one_shot = can_write_block_in_one_transfer(block);
1498            let words    = writable_words_in_block(block);
1499            assert_eq!(
1500                one_shot, words.is_empty(),
1501                "block {block}: one_transfer={one_shot}, word_list_empty={}",
1502                words.is_empty(),
1503            );
1504        }
1505    }
1506}