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}