Skip to main content

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}