config_generator/counter_encoding.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//! Encoding of the 8-byte initial value for the ATECC608B monotonic
17//! counters (`Counter0` at bytes 52..60 of the config zone, `Counter1` at
18//! bytes 60..68).
19//!
20//! The chip stores each counter as a redundant 8-byte structure split
21//! into two 16-bit linear ("lin") halves and two 16-bit binary ("bin")
22//! halves. Linear halves encode the low 5 bits of the count by clearing
23//! one bit per increment (popcount-style); binary halves encode the high
24//! 16 bits as a normal big-endian unsigned 16-bit integer. The two halves
25//! are offset by 16 increments so a corruption of either is detected by
26//! the chip.
27//!
28//! Writing `0xFF` to all 8 bytes does **not** represent "count = 0". It
29//! leaves `bin_a` and `bin_b` at `0xFFFF`, which the chip interprets as
30//! a high-bit count near the hardware ceiling (`2^21 - 1`). A chip that
31//! is config-locked with this stray initialization comes out of the lock
32//! with `count ≈ 2_097_120` (= `0xFFFF * 32`), losing virtually all of
33//! its `2^21` lifetime increments.
34//!
35//! The correct factory initialization is `FF FF FF FF 00 00 00 00`,
36//! which decodes to `count = 0`. The function in this module produces
37//! that, plus any other target count up to the maximum.
38//!
39//! # Reference
40//!
41//! Translation of `calib_write_config_counter` in
42//! `lib/calib/calib_basic.c` of Microchip CryptoAuthLib. The formula:
43//!
44//! ```text
45//! lin_a = 0xFFFF >> (counter_value % 32)
46//! lin_b = 0xFFFF >> ((counter_value - 16) % 32) if counter_value >= 16 else 0xFFFF
47//! bin_a = counter_value / 32
48//! bin_b = (counter_value - 16) / 32 if counter_value >= 16 else 0
49//! ```
50//!
51//! is serialized big-endian as:
52//!
53//! ```text
54//! bytes = [lin_a_hi, lin_a_lo, lin_b_hi, lin_b_lo,
55//! bin_a_hi, bin_a_lo, bin_b_hi, bin_b_lo]
56//! ```
57
58/// Maximum supported counter value on the ATECC608B (`2^21 - 1`).
59///
60/// Each successful "key-usage event" against a slot whose `LimitedUse`
61/// bit is set bumps the counter by one. When the counter reaches this
62/// ceiling the chip refuses further increments. The value is fixed by
63/// the chip's storage size (21 bits encoded in the 8-byte structure).
64pub const COUNTER_MAX_VALUE: u32 = 2_097_151;
65
66/// Number of bytes consumed by one counter in the config zone.
67pub const COUNTER_STORAGE_SIZE: usize = 8;
68
69/// Encode a target counter value into the 8-byte storage representation
70/// the ATECC608B expects in the configuration zone.
71///
72/// `value == 0` produces the factory initialization, the canonical
73/// "fresh chip" state. Any value in `0..=COUNTER_MAX_VALUE` is accepted
74/// and produces a valid storage; values above are clamped to
75/// `COUNTER_MAX_VALUE` because the chip cannot represent more.
76///
77/// Mirrors `calib_write_config_counter` from CryptoAuthLib exactly. The
78/// `[u8; 8]` returned is what gets written to bytes 52..60 (Counter0)
79/// or 60..68 (Counter1) of the configuration zone blob.
80///
81/// # Examples
82///
83/// ```ignore
84/// // Factory init: count = 0 → FF FF FF FF 00 00 00 00.
85/// assert_eq!(encode_counter_value(0), [0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0]);
86/// ```
87#[must_use]
88pub fn encode_counter_value(value: u32) -> [u8; COUNTER_STORAGE_SIZE]
89{
90 let value = value.min(COUNTER_MAX_VALUE);
91
92 let lin_a: u16 = (0xFFFFu32 >> (value % 32)) as u16;
93 let lin_b: u16 = if value >= 16
94 {
95 (0xFFFFu32 >> ((value - 16) % 32)) as u16
96 }
97 else
98 {
99 0xFFFF
100 };
101 // `bin_a` and `bin_b` fit in 16 bits because `value <= 2^21 - 1` and
102 // `bin_a = value / 32` is therefore at most `(2^21 - 1) / 32 = 65535`.
103 let bin_a: u16 = (value / 32) as u16;
104 let bin_b: u16 = if value >= 16
105 {
106 ((value - 16) / 32) as u16
107 }
108 else
109 {
110 0
111 };
112
113 [
114 (lin_a >> 8) as u8, (lin_a & 0xFF) as u8,
115 (lin_b >> 8) as u8, (lin_b & 0xFF) as u8,
116 (bin_a >> 8) as u8, (bin_a & 0xFF) as u8,
117 (bin_b >> 8) as u8, (bin_b & 0xFF) as u8,
118 ]
119}
120
121#[cfg(test)]
122mod tests
123{
124 use super::*;
125
126 /// Reference vectors generated from `encode_counter_cryptoauthlib`
127 /// (the Python translation we used to validate the chip's behavior
128 /// during bring-up). If this list ever diverges from CryptoAuthLib,
129 /// the test fails and we know we've drifted from the spec.
130 #[test]
131 fn matches_cryptoauthlib_reference_vectors()
132 {
133 let cases: &[(u32, [u8; 8])] =
134 &[
135 (0, [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
136 (1, [0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
137 (5, [0x07, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]),
138 (31, [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]),
139 (32, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]),
140 (100, [0x0F, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02]),
141 (2_097_120,[0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE]),
142 (COUNTER_MAX_VALUE, [0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF]),
143 ];
144 for (value, expected) in cases
145 {
146 let got = encode_counter_value(*value);
147 assert_eq!(
148 got, *expected,
149 "encode_counter_value({value}) = {got:02X?}, expected {expected:02X?}",
150 );
151 }
152 }
153
154 #[test]
155 fn factory_zero_is_all_ff_then_all_zero()
156 {
157 // The most important case: a freshly initialized chip must see
158 // its counter at 0. Any other value here means we ship a chip
159 // with a degraded budget, which is exactly the bug this module
160 // was created to fix.
161 assert_eq!
162 (
163 encode_counter_value(0),
164 [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
165 );
166 }
167
168 #[test]
169 fn values_above_max_clamp()
170 {
171 // u32::MAX is way above the chip's 2^21 - 1 ceiling. Encoding
172 // such a value must produce the same byte pattern as encoding
173 // exactly `COUNTER_MAX_VALUE`, never a wraparound that the chip
174 // would interpret as a small count.
175 assert_eq!(encode_counter_value(u32::MAX), encode_counter_value(COUNTER_MAX_VALUE));
176 }
177}