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}