Skip to main content

config_generator/
blob.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//! Construction of the 128-byte configuration zone blob.
17//!
18//! Every field, every bit, every value here matches
19//! `docs/config-zone-layout.md` section by section. If you change anything
20//! in this file, update the doc and re-run the tests.
21
22// ---------------------------------------------------------------------------
23// SlotConfig values, computed bit by bit.
24//
25// For ECC private key slots, the SlotConfig u16 layout is:
26//
27// bit 0    : ExtSig     (1 = allow ECDSA Sign on external messages)
28// bit 1    : IntSig     (1 = allow internal-message sign, unused here)
29// bit 2    : ECDH       (1 = allow ECDH, unused here)
30// bit 3    : WriteECDH  (1 = allow writing ECDH output, unused)
31// bit 4    : NoMac      (1 = forbid use in MAC commands)
32// bit 5    : LimitedUse (1 = rate-limit usage via Counter0)
33// bit 6    : EncRead    (1 = encrypted reads required)
34// bit 7    : IsSecret   (1 = slot value never readable in cleartext)
35// bit 8    : GenKey     (1 = allow GenKey to (re)generate the key)
36// bit 9    : PrivWrite  (1 = allow PrivWrite encrypted import)
37// bits 10-11: reserved
38// bits 12-15: WriteConfig (see below)
39//
40// For data slots, the layout is:
41//
42// bits 0-3  : ReadKey
43// bit  4    : NoMac
44// bit  5    : LimitedUse
45// bit  6    : EncRead
46// bit  7    : IsSecret
47// bits 8-11 : WriteKey
48// bits 12-15: WriteConfig
49//
50// WriteConfig values:
51//
52// 0x0 : Always              (cleartext until data lock, never after)
53// 0x2 : Never               (cleartext write never permitted)
54// 0x4 : Always_then_Encrypt (cleartext pre-lock, encrypted via WriteKey post-lock)
55// 0x6 : Encrypt             (encrypted only, never cleartext)
56// 0x8 : Never               (alternate encoding)
57// 0xC : Never_then_Encrypt  (never cleartext, encrypted post-lock)
58//
59// Reference: docs/config-zone-layout.md and Microchip CryptoAuthLib
60// `lib/calib/calib_device.h`.
61// ---------------------------------------------------------------------------
62
63/// SlotConfig for ECC P-256 slots that allow GenKey but not PrivWrite.
64///
65/// Used by slots 0 and 1, the maximum-security identity slots.
66///
67/// Bits: `IsSecret=1, GenKey=1, ExtSig=1, WriteConfig=Never`.
68const SLOT_CFG_ECC_GENKEY_ONLY: u16 = 0x2181;
69
70/// SlotConfig for ECC P-256 slots that allow both GenKey and encrypted
71/// PrivWrite import.
72///
73/// Used by slots 2, 3, 4, 7, and 9 through 15.
74///
75/// Same as `SLOT_CFG_ECC_GENKEY_ONLY` plus the `PrivWrite` bit set.
76const SLOT_CFG_ECC_GENKEY_OR_IMPORT: u16 = 0x2381;
77
78/// SlotConfig for the PIN hash slot (slot 5) and the PUK hash slot (slot 6).
79///
80/// Bits: `ReadKey=8, LimitedUse=1, IsSecret=1, WriteKey=8,
81/// WriteConfig=Always_then_Encrypt`.
82const SLOT_CFG_DATA_PIN_PUK: u16 = 0x48A8;
83
84/// SlotConfig for the I/O Protection master key (slot 8).
85///
86/// Bits: `IsSecret=1, WriteConfig=Always`.
87///
88/// `Always` means the slot accepts cleartext writes only until the data
89/// zone is locked. After data lock, the slot becomes immutable.
90const SLOT_CFG_DATA_IO_KEY: u16 = 0x0080;
91
92// ---------------------------------------------------------------------------
93// KeyConfig values.
94//
95// KeyConfig u16 layout:
96//
97// bit 0     : Private    (1 = ECC private key)
98// bit 1     : PubInfo    (1 = GenKey mode=public allowed)
99// bits 2-4  : KeyType    (4 = P-256, 7 = Data 32B)
100// bit 5     : Lockable   (1 = Lock(mode=slot) is permitted)
101// bit 6     : ReqRandom  (1 = chip-side random nonce required before use)
102// bit 7     : ReqAuth    (1 = CheckMac on AuthKey must succeed first)
103// bits 8-11 : AuthKey    (slot index that authorizes use)
104// bit 12    : PersistDis
105// bit 13    : reserved
106// bits 14-15: X509id
107// ---------------------------------------------------------------------------
108
109/// KeyConfig for ECC P-256 slots with `Lockable = 0`.
110///
111/// Used by slots 0 and 1. Sign and GenKey are PIN-gated (ReqAuth = 1,
112/// AuthKey = 5). The slot cannot be individually locked; the key can be
113/// regenerated indefinitely.
114const KEY_CFG_ECC_LOCKABLE_OFF: u16 = 0x0593;
115
116/// KeyConfig for ECC P-256 slots with `Lockable = 1`.
117///
118/// Used by slots 2-4, 7, 9-15. Same policy as `KEY_CFG_ECC_LOCKABLE_OFF`
119/// except that the slot can be individually locked via Lock(mode=slot)
120/// if the operator decides to freeze a particular key permanently.
121const KEY_CFG_ECC_LOCKABLE_ON: u16 = 0x05B3;
122
123/// KeyConfig for generic data slots.
124///
125/// Used by slots 5, 6, and 8. `KeyType = 7` (Data 32 B), all other bits
126/// at zero.
127const KEY_CFG_DATA_GENERIC: u16 = 0x001C;
128
129/// I2C address byte stored at offset 16.
130///
131/// The chip stores the I2C 7-bit address in 8-bit form (shifted left by
132/// one). For our default 7-bit address 0x60, the stored value is 0xC0.
133const I2C_ADDRESS_BYTE: u8 = 0x60u8 << 1;
134
135/// Value of byte 17. Required to be zero by the ATECC608B reference.
136const RESERVED2: u8 = 0x00;
137
138/// Value of byte 18, CountMatch. The CountMatch feature is unused, the
139/// byte stays at zero.
140const COUNT_MATCH: u8 = 0x00;
141
142/// Value of byte 19, ChipMode.
143///
144/// Layout:
145/// - bit 0    : I2C_Extra      = 0 (extra I2C address feature disabled)
146/// - bit 1    : TTL_Enable     = 0 (fixed Vcc reference)
147/// - bit 2    : Watchdog_Long  = 0 (short watchdog, about 1.3 s)
148/// - bits 3-7 : Clock_Divider  = 0 (M0, maximum speed)
149///
150/// All zero gives us M0 mode with the short watchdog.
151const CHIP_MODE: u8 = 0x00;
152
153/// Value placed at the LockValue (byte 86) field. `0x55` means the data
154/// zone is unlocked. The chip overwrites this with `0x00` when the
155/// `Lock(data)` command is issued.
156const LOCK_VALUE_UNLOCKED: u8 = 0x55;
157
158/// Value placed at the LockConfig (byte 87) field. `0x55` means the
159/// configuration zone is unlocked. The chip overwrites this with `0x00`
160/// when the `Lock(config)` command is issued.
161const LOCK_CONFIG_UNLOCKED: u8 = 0x55;
162
163/// Total size of the configuration zone.
164pub(crate) const CONFIG_ZONE_SIZE: usize = 128;
165
166/// Build the full 128-byte configuration zone blob, including factory-area
167/// placeholders at bytes 0-15.
168///
169/// The factory area is filled with zeros as a placeholder. The chip refuses
170/// to overwrite it during the Write command, so its content in the blob is
171/// irrelevant to the actual provisioning operation. The firmware reads the
172/// real factory values from the chip at runtime.
173#[must_use]
174pub(crate) fn build() -> [u8; CONFIG_ZONE_SIZE]
175{
176    let mut cfg = [0u8; CONFIG_ZONE_SIZE];
177
178    // Bytes 0-15: factory area, left at zero. The chip ignores Write
179    // commands targeted here.
180
181    // Bytes 16-19: device-level configuration.
182    cfg[16] = I2C_ADDRESS_BYTE;
183    cfg[17] = RESERVED2;
184    cfg[18] = COUNT_MATCH;
185    cfg[19] = CHIP_MODE;
186
187    // Bytes 20-51: SlotConfig[16]. Each slot occupies 2 bytes in
188    // little-endian form.
189    for slot in 0..16u8
190    {
191        let sc = slot_config_for(slot);
192        let offset = 20 + 2 * (slot as usize);
193        cfg[offset]     = (sc & 0xFF) as u8;
194        cfg[offset + 1] = (sc >> 8)   as u8;
195    }
196
197    // Bytes 52-67: Counter0 and Counter1 initial values, 8 bytes each.
198    //
199    // The ATECC608B stores each monotonic counter as a redundant 8-byte
200    // structure (two linear "lin" 16-bit halves + two binary "bin"
201    // 16-bit halves) that the chip decodes to a single u32 count when
202    // queried via the `Counter` command. Writing `0xFF` to all eight
203    // bytes does NOT mean "count = 0". It decodes (per CryptoAuthLib's
204    // `calib_write_config_counter`) to `count = 2_097_120`, which is
205    // just 31 increments away from the chip's hardware ceiling of
206    // `2^21 - 1`. Such a chip comes out of `Lock(config)` with almost
207    // its entire lifetime budget already consumed.
208    //
209    // The correct factory initialization is `FF FF FF FF 00 00 00 00`
210    // (decodes to count = 0). The `counter_encoding` module produces
211    // it via the same formula CryptoAuthLib uses.
212    let counter_factory_init = crate::counter_encoding::encode_counter_value(0);
213    cfg[52..60].copy_from_slice(&counter_factory_init);
214    cfg[60..68].copy_from_slice(&counter_factory_init);
215
216    // Bytes 68-83: feature configuration fields, all unused, all zero.
217    // Already initialized to zero by the `cfg` initializer.
218
219    // Bytes 84-85: UserExtra and UserExtraAdd, kept at zero.
220    // Already initialized to zero.
221
222    // Bytes 86-87: lock state bytes, 0x55 = unlocked at provisioning.
223    cfg[86] = LOCK_VALUE_UNLOCKED;
224    cfg[87] = LOCK_CONFIG_UNLOCKED;
225
226    // Bytes 88-89: SlotLocked bitmap, all slots unlocked = 0xFFFF.
227    cfg[88] = 0xFF;
228    cfg[89] = 0xFF;
229
230    // Bytes 90-91: ChipOptions, all features disabled = 0x0000.
231    // Already initialized to zero.
232
233    // Bytes 92-95: X509format, unused = 0x00 0x00 0x00 0x00.
234    // Already initialized to zero.
235
236    // Bytes 96-127: KeyConfig[16]. Same layout as SlotConfig area, two
237    // bytes per slot, little-endian.
238    for slot in 0..16u8
239    {
240        let kc = key_config_for(slot);
241        let offset = 96 + 2 * (slot as usize);
242        cfg[offset]     = (kc & 0xFF) as u8;
243        cfg[offset + 1] = (kc >> 8)   as u8;
244    }
245
246    cfg
247}
248
249/// Return the SlotConfig value for a given slot index.
250fn slot_config_for(slot: u8) -> u16
251{
252    match slot
253    {
254        0 | 1                   => SLOT_CFG_ECC_GENKEY_ONLY,
255        5 | 6                   => SLOT_CFG_DATA_PIN_PUK,
256        8                       => SLOT_CFG_DATA_IO_KEY,
257        2 | 3 | 4 | 7           => SLOT_CFG_ECC_GENKEY_OR_IMPORT,
258        9..=15                  => SLOT_CFG_ECC_GENKEY_OR_IMPORT,
259        _                       => SLOT_CFG_ECC_GENKEY_OR_IMPORT,
260    }
261}
262
263/// Return the KeyConfig value for a given slot index.
264fn key_config_for(slot: u8) -> u16
265{
266    match slot
267    {
268        0 | 1                   => KEY_CFG_ECC_LOCKABLE_OFF,
269        5 | 6 | 8               => KEY_CFG_DATA_GENERIC,
270        2 | 3 | 4 | 7           => KEY_CFG_ECC_LOCKABLE_ON,
271        9..=15                  => KEY_CFG_ECC_LOCKABLE_ON,
272        _                       => KEY_CFG_ECC_LOCKABLE_ON,
273    }
274}
275
276#[cfg(test)]
277mod tests
278{
279    use super::*;
280    use crate::crc::crc16;
281
282    /// Expected byte map for the writable portion of the blob, taken
283    /// verbatim from `docs/config-zone-layout.md`.
284    const EXPECTED_WRITABLE: [u8; 112] =
285    [
286        // Bytes 16-31
287        0xC0, 0x00, 0x00, 0x00, 0x81, 0x21, 0x81, 0x21,
288        0x81, 0x23, 0x81, 0x23, 0x81, 0x23, 0xA8, 0x48,
289        // Bytes 32-47
290        0xA8, 0x48, 0x81, 0x23, 0x80, 0x00, 0x81, 0x23,
291        0x81, 0x23, 0x81, 0x23, 0x81, 0x23, 0x81, 0x23,
292        // Bytes 48-63
293        0x81, 0x23, 0x81, 0x23, 0xFF, 0xFF, 0xFF, 0xFF,
294        0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
295        // Bytes 64-79
296        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
297        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
298        // Bytes 80-95
299        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55,
300        0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
301        // Bytes 96-111
302        0x93, 0x05, 0x93, 0x05, 0xB3, 0x05, 0xB3, 0x05,
303        0xB3, 0x05, 0x1C, 0x00, 0x1C, 0x00, 0xB3, 0x05,
304        // Bytes 112-127
305        0x1C, 0x00, 0xB3, 0x05, 0xB3, 0x05, 0xB3, 0x05,
306        0xB3, 0x05, 0xB3, 0x05, 0xB3, 0x05, 0xB3, 0x05,
307    ];
308
309    #[test]
310    fn blob_size_is_128_bytes()
311    {
312        let blob = build();
313        assert_eq!(blob.len(), CONFIG_ZONE_SIZE);
314    }
315
316    #[test]
317    fn writable_portion_matches_specification()
318    {
319        let blob = build();
320        let writable = &blob[16..128];
321        assert_eq!(writable, &EXPECTED_WRITABLE);
322    }
323
324    #[test]
325    fn crc_of_writable_portion_is_canonical()
326    {
327        let blob = build();
328        let crc = crc16(&blob[16..128]);
329        assert_eq!(crc, 0xC92D, "expected 0xC92D, got 0x{crc:04X}");
330    }
331
332    #[test]
333    fn slot_configs_match_documented_values()
334    {
335        let blob = build();
336        let expected: [(u8, u16); 16] =
337        [
338            (0,  0x2181), (1,  0x2181),
339            (2,  0x2381), (3,  0x2381), (4,  0x2381),
340            (5,  0x48A8), (6,  0x48A8),
341            (7,  0x2381),
342            (8,  0x0080),
343            (9,  0x2381), (10, 0x2381), (11, 0x2381),
344            (12, 0x2381), (13, 0x2381), (14, 0x2381), (15, 0x2381),
345        ];
346        for (slot, expected_sc) in expected
347        {
348            let offset = 20 + 2 * (slot as usize);
349            let actual_sc = u16::from(blob[offset]) | (u16::from(blob[offset + 1]) << 8);
350            assert_eq!
351            (
352                actual_sc, expected_sc,
353                "SlotConfig[{slot}] expected 0x{expected_sc:04X}, got 0x{actual_sc:04X}",
354            );
355        }
356    }
357
358    #[test]
359    fn key_configs_match_documented_values()
360    {
361        let blob = build();
362        let expected: [(u8, u16); 16] =
363        [
364            (0,  0x0593), (1,  0x0593),
365            (2,  0x05B3), (3,  0x05B3), (4,  0x05B3),
366            (5,  0x001C), (6,  0x001C),
367            (7,  0x05B3),
368            (8,  0x001C),
369            (9,  0x05B3), (10, 0x05B3), (11, 0x05B3),
370            (12, 0x05B3), (13, 0x05B3), (14, 0x05B3), (15, 0x05B3),
371        ];
372        for (slot, expected_kc) in expected
373        {
374            let offset = 96 + 2 * (slot as usize);
375            let actual_kc = u16::from(blob[offset]) | (u16::from(blob[offset + 1]) << 8);
376            assert_eq!
377            (
378                actual_kc, expected_kc,
379                "KeyConfig[{slot}] expected 0x{expected_kc:04X}, got 0x{actual_kc:04X}",
380            );
381        }
382    }
383
384    #[test]
385    fn lock_bytes_initially_unlocked()
386    {
387        let blob = build();
388        assert_eq!(blob[86], 0x55, "LockValue (data zone) must be unlocked at provisioning");
389        assert_eq!(blob[87], 0x55, "LockConfig (config zone) must be unlocked at provisioning");
390    }
391
392    #[test]
393    fn slotlocked_bitmap_all_unlocked()
394    {
395        let blob = build();
396        assert_eq!(blob[88], 0xFF);
397        assert_eq!(blob[89], 0xFF);
398    }
399
400    #[test]
401    fn i2c_address_is_0x60_in_8bit_form()
402    {
403        let blob = build();
404        assert_eq!(blob[16], 0xC0, "0x60 << 1 = 0xC0");
405    }
406
407    #[test]
408    fn chip_mode_is_m0_short_watchdog()
409    {
410        let blob = build();
411        assert_eq!(blob[19], 0x00, "M0 + short watchdog + TTL off + no extra I2C addr");
412    }
413
414    #[test]
415    fn counters_are_factory_default()
416    {
417        let blob = build();
418        // Counter0 (bytes 52..60) and Counter1 (bytes 60..68) both
419        // initialized via CryptoAuthLib's counter encoding for count=0,
420        // which is `FF FF FF FF 00 00 00 00` (NOT plain `0xFF` repeated).
421        // See `counter_encoding::encode_counter_value` for the formula.
422        let expected = crate::counter_encoding::encode_counter_value(0);
423        assert_eq!(&blob[52..60], &expected, "Counter0 must be encoded for count=0");
424        assert_eq!(&blob[60..68], &expected, "Counter1 must be encoded for count=0");
425    }
426}