atecc608b/command/read_write.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//! `Read` and `Write` commands.
17//!
18//! These access the config, data, or OTP zones. They support 4-byte and
19//! 32-byte transfers. Writes to the data zone are subject to the per-slot
20//! `SlotConfig.WriteConfig` rules captured in `crates/hsm-crypto-service`.
21//!
22//! # Address encoding
23//!
24//! The ATECC608B encodes the target of a Read or Write into the 16-bit
25//! `param2` of the command frame. The format depends on the zone.
26//!
27//! - **Config zone**. The zone is 128 bytes laid out as 4 blocks of 32 bytes.
28//! Each block is addressable as 8 "words" of 4 bytes.
29//!
30//! ```text
31//! bits 0..=2 : offset within block (word index, 0..=7).
32//! bits 3..=4 : block index (0..=3).
33//! bits 5..=15 : reserved (zero).
34//! ```
35//!
36//! - **OTP zone**. Same layout as the config zone but only 2 blocks of 32
37//! bytes (block index in 0..=1).
38//!
39//! - **Data zone**. One slot per row.
40//!
41//! ```text
42//! bits 0..=2 : offset within block (word index).
43//! bits 3..=7 : slot index (0..=15).
44//! bits 8..=15 : block index.
45//! ```
46//!
47//! In 32-byte transfers the offset bits must be zero, the chip rejects the
48//! command otherwise.
49//!
50//! # `param1` encoding
51//!
52//! `param1` carries the zone identifier in its low two bits, an optional
53//! encryption flag, and a single "this is a 32-byte transfer" flag.
54//!
55//! ```text
56//! bits 0..=1 : zone (0 = Config, 1 = OTP, 2 = Data).
57//! bit 6 : 1 -> data field is encrypted with TempKey + MAC (Write only).
58//! bit 7 : 1 -> 32-byte transfer, 0 -> 4-byte transfer.
59//! ```
60//!
61//! Reference: `CryptoAuthLib` `lib/calib/calib_read.c` and
62//! `lib/calib/calib_write.c`, constants `ATCA_ZONE_CONFIG`, `ATCA_ZONE_OTP`,
63//! `ATCA_ZONE_DATA`, `ATCA_ZONE_READWRITE_32`, `ATCA_ZONE_ENCRYPTED`.
64
65use crate::driver::AteccChannel;
66use crate::error::AteccError;
67use crate::hal::AteccHal;
68use crate::opcodes::{EXEC_TIME_READ_MS, EXEC_TIME_WRITE_MS, OP_READ, OP_WRITE};
69use crate::slot::Slot;
70
71/// One of the three addressable zones of the ATECC608B.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[cfg_attr(feature = "defmt", derive(defmt::Format))]
74pub enum Zone
75{
76 /// 128-byte configuration zone. Contains slot policies, counters, lock
77 /// bytes, `KeyConfig`, and factory data (serial number, revision).
78 Config,
79 /// 64-byte one-time-programmable zone.
80 Otp,
81 /// 16 slots of variable size holding ECC keys and arbitrary data.
82 Data,
83}
84
85impl Zone
86{
87 /// Numeric value of the zone as it appears in the low bits of `param1`.
88 const fn as_param1_bits(self) -> u8
89 {
90 match self
91 {
92 Zone::Config => 0x00,
93 Zone::Otp => 0x01,
94 Zone::Data => 0x02,
95 }
96 }
97}
98
99/// Number of 32-byte blocks in the config zone (128 / 32 = 4).
100pub(crate) const CONFIG_ZONE_BLOCK_COUNT: u8 = 4;
101
102/// Size of the config zone in bytes.
103pub(crate) const CONFIG_ZONE_SIZE: usize = 128;
104
105/// Size of one block transferred in a 32-byte read or write.
106pub const BLOCK_SIZE: usize = 32;
107
108/// Size of one word transferred in a 4-byte read or write.
109pub const WORD_SIZE: usize = 4;
110
111/// Size of the MAC appended to an encrypted 32-byte write.
112pub(crate) const ENCRYPTED_WRITE_MAC_SIZE: usize = 32;
113
114/// Total size of the data field for an encrypted 32-byte write: 32 bytes of
115/// ciphertext followed by 32 bytes of MAC.
116pub const ENCRYPTED_WRITE_DATA_SIZE: usize = BLOCK_SIZE + ENCRYPTED_WRITE_MAC_SIZE;
117
118/// Flag OR'd into `param1` to switch from a 4-byte to a 32-byte transfer.
119const PARAM1_FLAG_32_BYTES: u8 = 0x80;
120
121/// Flag OR'd into `param1` to indicate the data field is encrypted with
122/// `TempKey` and is followed by a MAC. Only valid for 32-byte Writes.
123const PARAM1_FLAG_ENCRYPTED: u8 = 0x40;
124
125/// Build the `param2` address for a config or OTP zone access.
126///
127/// `block` indexes one of the 32-byte blocks of the zone. `offset_words`
128/// indexes a 4-byte word inside that block. For 32-byte transfers
129/// `offset_words` must be 0.
130#[must_use]
131pub const fn config_or_otp_address(block: u8, offset_words: u8) -> u16
132{
133 let block_bits = ((block & 0x03) as u16) << 3;
134 let offset_bits = (offset_words & 0x07) as u16;
135 block_bits | offset_bits
136}
137
138/// Build the `param2` address for a data zone access.
139///
140/// `slot` is the target slot. `block` is the 32-byte block within that slot
141/// (slots have variable sizes, slot 8 alone has 11 blocks). `offset_words`
142/// indexes a 4-byte word inside the block. For 32-byte transfers
143/// `offset_words` must be 0.
144#[must_use]
145pub const fn data_address(slot: Slot, block: u8, offset_words: u8) -> u16
146{
147 let block_bits = (block as u16) << 8;
148 let slot_bits = ((slot.as_u8() & 0x1F) as u16) << 3;
149 let offset_bits = (offset_words & 0x07) as u16;
150 block_bits | slot_bits | offset_bits
151}
152
153/// Compose the `param1` byte for a Read.
154const fn read_param1(zone: Zone, is_32_bytes: bool) -> u8
155{
156 let flag = if is_32_bytes { PARAM1_FLAG_32_BYTES } else { 0 };
157 zone.as_param1_bits() | flag
158}
159
160/// Compose the `param1` byte for a Write.
161///
162/// `encrypted` is only meaningful in 32-byte mode. The chip silently
163/// accepts the bit in 4-byte mode and rejects the command at execution,
164/// callers should pass `false` for `encrypted` when `is_32_bytes` is false.
165const fn write_param1(zone: Zone, is_32_bytes: bool, encrypted: bool) -> u8
166{
167 let mut p1 = zone.as_param1_bits();
168 if is_32_bytes
169 {
170 p1 |= PARAM1_FLAG_32_BYTES;
171 }
172 if encrypted
173 {
174 p1 |= PARAM1_FLAG_ENCRYPTED;
175 }
176 p1
177}
178
179impl<H> AteccChannel<'_, H>
180where
181 H: AteccHal,
182{
183 /// Read a 4-byte word from the given zone at the given address.
184 ///
185 /// `address` is the raw 16-bit value encoded in `param2`. Callers
186 /// generally build it with [`config_or_otp_address`] or [`data_address`].
187 ///
188 /// # Errors
189 /// See [`AteccChannel::execute_command`]. In particular, reads of locked
190 /// or permission-restricted regions surface as [`AteccError::Chip`] with
191 /// the relevant status byte.
192 pub(crate) async fn read_4
193 (
194 &mut self,
195 zone: Zone,
196 address: u16,
197 ) -> Result<[u8; WORD_SIZE], AteccError<H::Error>>
198 {
199 // Response: count(1) + 4 data + crc(2) = 7 bytes.
200 let mut response_buf = [0u8; 1 + WORD_SIZE + 2];
201 let payload = self
202 .execute_command
203 (
204 OP_READ,
205 read_param1(zone, false),
206 address,
207 &[],
208 EXEC_TIME_READ_MS,
209 &mut response_buf,
210 )
211 .await?;
212
213 let bytes: &[u8; WORD_SIZE] = payload
214 .try_into()
215 .map_err(|_| AteccError::MalformedResponse)?;
216 Ok(*bytes)
217 }
218
219 /// Read a 32-byte block from the given zone at the given address.
220 ///
221 /// `address` is the raw 16-bit value encoded in `param2`. The offset
222 /// bits must be zero, the chip rejects 32-byte transfers otherwise.
223 ///
224 /// # Errors
225 /// See [`AteccChannel::execute_command`].
226 pub(crate) async fn read_32
227 (
228 &mut self,
229 zone: Zone,
230 address: u16,
231 ) -> Result<[u8; BLOCK_SIZE], AteccError<H::Error>>
232 {
233 // Response: count(1) + 32 data + crc(2) = 35 bytes.
234 let mut response_buf = [0u8; 1 + BLOCK_SIZE + 2];
235 let payload = self
236 .execute_command
237 (
238 OP_READ,
239 read_param1(zone, true),
240 address,
241 &[],
242 EXEC_TIME_READ_MS,
243 &mut response_buf,
244 )
245 .await?;
246
247 let bytes: &[u8; BLOCK_SIZE] = payload
248 .try_into()
249 .map_err(|_| AteccError::MalformedResponse)?;
250 Ok(*bytes)
251 }
252
253 /// Read the entire 128-byte config zone into `out`.
254 ///
255 /// Internally performs four 32-byte reads, one per block. The channel
256 /// stays open between the reads.
257 ///
258 /// # Errors
259 /// See [`AteccChannel::execute_command`]. The first failing block aborts
260 /// the whole operation.
261 pub async fn read_config_zone
262 (
263 &mut self,
264 out: &mut [u8; CONFIG_ZONE_SIZE],
265 ) -> Result<(), AteccError<H::Error>>
266 {
267 for block in 0..CONFIG_ZONE_BLOCK_COUNT
268 {
269 let address = config_or_otp_address(block, 0);
270 let chunk = self.read_32(Zone::Config, address).await?;
271 let start = usize::from(block) * BLOCK_SIZE;
272 out[start..start + BLOCK_SIZE].copy_from_slice(&chunk);
273 }
274 Ok(())
275 }
276
277 /// Read a 4-byte word from a data slot.
278 ///
279 /// `block` and `offset_words` are interpreted per the ATECC608B address
280 /// layout for the data zone.
281 ///
282 /// # Errors
283 /// See [`AteccChannel::execute_command`].
284 pub async fn read_slot_word
285 (
286 &mut self,
287 slot: Slot,
288 block: u8,
289 offset_words: u8,
290 ) -> Result<[u8; WORD_SIZE], AteccError<H::Error>>
291 {
292 self.read_4(Zone::Data, data_address(slot, block, offset_words)).await
293 }
294
295 /// Read a 32-byte block from a data slot.
296 ///
297 /// # Errors
298 /// See [`AteccChannel::execute_command`].
299 pub async fn read_slot_block
300 (
301 &mut self,
302 slot: Slot,
303 block: u8,
304 ) -> Result<[u8; BLOCK_SIZE], AteccError<H::Error>>
305 {
306 self.read_32(Zone::Data, data_address(slot, block, 0)).await
307 }
308
309 /// Write a 4-byte word to the given zone at the given address.
310 ///
311 /// Cleartext only: encrypted writes are not supported in 4-byte mode
312 /// (this is a chip limitation, not a driver one).
313 ///
314 /// # Errors
315 /// See [`AteccChannel::execute_command_status`]. Writes to locked
316 /// regions or to slots whose `SlotConfig.WriteConfig` forbids cleartext
317 /// writes surface as [`AteccError::Chip`].
318 pub async fn write_4
319 (
320 &mut self,
321 zone: Zone,
322 address: u16,
323 data: &[u8; WORD_SIZE],
324 ) -> Result<(), AteccError<H::Error>>
325 {
326 self.execute_command_status
327 (
328 OP_WRITE,
329 write_param1(zone, false, false),
330 address,
331 data,
332 EXEC_TIME_WRITE_MS,
333 )
334 .await
335 }
336
337 /// Write a 32-byte block to the given zone in cleartext.
338 ///
339 /// `address` must have its offset bits set to zero. The block index is
340 /// the upper bits per the zone layout.
341 ///
342 /// # Errors
343 /// See [`AteccChannel::execute_command_status`].
344 pub async fn write_32
345 (
346 &mut self,
347 zone: Zone,
348 address: u16,
349 data: &[u8; BLOCK_SIZE],
350 ) -> Result<(), AteccError<H::Error>>
351 {
352 self.execute_command_status
353 (
354 OP_WRITE,
355 write_param1(zone, true, false),
356 address,
357 data,
358 EXEC_TIME_WRITE_MS,
359 )
360 .await
361 }
362
363 /// Write a 32-byte block to a data slot in encrypted mode.
364 ///
365 /// The caller must supply the ciphertext and the precomputed MAC. The
366 /// derivation of both is the responsibility of the higher-level
367 /// `hsm-crypto-service` (see its provisioning module).
368 ///
369 /// This entry point exists in the driver so that the encrypted-write
370 /// path can be exercised against the mock HAL. It assumes the chip is
371 /// already loaded with a fresh `GenDig`-derived `TempKey` for the I/O
372 /// protection slot. Calling this without that prior step yields a chip
373 /// error.
374 ///
375 /// # Errors
376 /// See [`AteccChannel::execute_command_status`].
377 pub async fn write_32_encrypted
378 (
379 &mut self,
380 zone: Zone,
381 address: u16,
382 ciphertext_and_mac: &[u8; ENCRYPTED_WRITE_DATA_SIZE],
383 ) -> Result<(), AteccError<H::Error>>
384 {
385 self.execute_command_status
386 (
387 OP_WRITE,
388 write_param1(zone, true, true),
389 address,
390 ciphertext_and_mac,
391 EXEC_TIME_WRITE_MS,
392 )
393 .await
394 }
395
396 /// Write a 4-byte word into a data slot in cleartext.
397 ///
398 /// # Errors
399 /// See [`AteccChannel::execute_command_status`].
400 pub async fn write_slot_word
401 (
402 &mut self,
403 slot: Slot,
404 block: u8,
405 offset_words: u8,
406 data: &[u8; WORD_SIZE],
407 ) -> Result<(), AteccError<H::Error>>
408 {
409 self.write_4(Zone::Data, data_address(slot, block, offset_words), data).await
410 }
411
412 /// Write a 32-byte block into a data slot in cleartext.
413 ///
414 /// # Errors
415 /// See [`AteccChannel::execute_command_status`].
416 pub async fn write_slot_block
417 (
418 &mut self,
419 slot: Slot,
420 block: u8,
421 data: &[u8; BLOCK_SIZE],
422 ) -> Result<(), AteccError<H::Error>>
423 {
424 self.write_32(Zone::Data, data_address(slot, block, 0), data).await
425 }
426}
427
428#[cfg(test)]
429mod tests
430{
431 use super::*;
432
433 #[test]
434 fn read_param1_config_4_bytes_is_zero()
435 {
436 assert_eq!(read_param1(Zone::Config, false), 0x00);
437 }
438
439 #[test]
440 fn read_param1_config_32_bytes_sets_flag()
441 {
442 assert_eq!(read_param1(Zone::Config, true), 0x80);
443 }
444
445 #[test]
446 fn read_param1_data_4_bytes()
447 {
448 assert_eq!(read_param1(Zone::Data, false), 0x02);
449 }
450
451 #[test]
452 fn read_param1_data_32_bytes()
453 {
454 assert_eq!(read_param1(Zone::Data, true), 0x82);
455 }
456
457 #[test]
458 fn read_param1_otp_variants()
459 {
460 assert_eq!(read_param1(Zone::Otp, false), 0x01);
461 assert_eq!(read_param1(Zone::Otp, true), 0x81);
462 }
463
464 #[test]
465 fn write_param1_cleartext_variants_match_read_param1()
466 {
467 // Cleartext writes share the same bit layout as reads in p1.
468 assert_eq!(write_param1(Zone::Config, false, false), 0x00);
469 assert_eq!(write_param1(Zone::Data, true, false), 0x82);
470 assert_eq!(write_param1(Zone::Otp, true, false), 0x81);
471 }
472
473 #[test]
474 fn write_param1_encrypted_sets_bit_6()
475 {
476 assert_eq!(write_param1(Zone::Data, true, true), 0xC2);
477 assert_eq!(write_param1(Zone::Config, true, true), 0xC0);
478 }
479
480 #[test]
481 fn config_address_block_0_offset_0_is_zero()
482 {
483 assert_eq!(config_or_otp_address(0, 0), 0x0000);
484 }
485
486 #[test]
487 fn config_address_block_index_in_bits_3_4()
488 {
489 assert_eq!(config_or_otp_address(1, 0), 0x0008);
490 assert_eq!(config_or_otp_address(2, 0), 0x0010);
491 assert_eq!(config_or_otp_address(3, 0), 0x0018);
492 }
493
494 #[test]
495 fn config_address_offset_in_low_bits()
496 {
497 assert_eq!(config_or_otp_address(0, 1), 0x0001);
498 assert_eq!(config_or_otp_address(0, 7), 0x0007);
499 // Block 2, offset 3.
500 assert_eq!(config_or_otp_address(2, 3), 0x0013);
501 }
502
503 #[test]
504 fn config_address_truncates_oversized_fields()
505 {
506 // Block is masked to 2 bits, offset to 3 bits. Anything else is
507 // silently dropped, callers are responsible for valid inputs.
508 assert_eq!(config_or_otp_address(0xFF, 0xFF), 0x001F);
509 }
510
511 #[test]
512 fn data_address_slot_5_block_0_offset_0()
513 {
514 let slot = Slot::const_new(5);
515 assert_eq!(data_address(slot, 0, 0), 0x0028);
516 }
517
518 #[test]
519 fn data_address_slot_8_block_0_offset_0()
520 {
521 let slot = Slot::const_new(8);
522 assert_eq!(data_address(slot, 0, 0), 0x0040);
523 }
524
525 #[test]
526 fn data_address_slot_0_block_1_offset_0()
527 {
528 let slot = Slot::const_new(0);
529 assert_eq!(data_address(slot, 1, 0), 0x0100);
530 }
531
532 #[test]
533 fn data_address_slot_15_block_0_offset_7()
534 {
535 let slot = Slot::const_new(15);
536 // (15 << 3) | 7 = 0x78 | 0x07 = 0x7F.
537 assert_eq!(data_address(slot, 0, 7), 0x007F);
538 }
539
540 #[test]
541 fn encrypted_data_size_is_64_bytes()
542 {
543 assert_eq!(ENCRYPTED_WRITE_DATA_SIZE, 64);
544 }
545}