pico-quorum

The OPTIGA Trust M beside the ATECC608

The board signs with either secure element. It finds out which one is on I2C0 at every boot and uses the same console, the same pin (QUORUM_EXPECT) and the same Safe owner scheme (a P-256 passkey signer). The Trust M gets its own profile, quorum-tm1, and its own ceremony. Both were built and tested in the emulator first. On a real chip (TM1), every step through the seal has run (T7, 2026-09-29), and a Trust M approval has executed onchain (Base Sepolia, 2026-09-30). T9, freezing the spare objects, ran on a sacrificial chip (TM2, 2026-10-01) along with the refusals a sealed and frozen chip must give; TM1 is next. The shareable docs for a mainnet Safe are in mainnet/.

Decisions (2026-09-28):

Which chip is in

signer.detect():

  1. One wake token for a sleeping ATECC, then i2c.scan().
  2. A short poll of the Trust M’s state register at 0x30. A sleeping Trust M NACKs its address, so scan() alone misses it: on the real board scan() returned [] with a Trust M on the bus.

signer.load(soft, pins) then picks one:

On the bus Signer
ATECC608 (0x60, 0x35, 0x6A) ChipSigner, as before
Trust M (0x30) TrustMSigner
both the pinned one (QUORUM_EXPECT); none pinned: the ATECC, and it logs that
neither NoSigner (production), or the software key (development)

The console never names a chip type. It goes through serial(fresh), pubkey(), sign(), key_row(), profile(), use_counter(), needs_ceremony() and info(). main_dual.which() routes a fresh or half-done Trust M to the ceremony, the same as an ATECC.

A Trust M pin names the object instead of a slot number:

{"serial": "0a091b5c000b0062006c", "slot": 0xE0F1, "owner": "0x…", "locked": True, "chip": "trustm"}

quorum-tm1

Object What it holds Factory state (read off the real chip) After the seal
E0F1 the Safe key, P-256, key usage Sign, Execute ALW empty, LcsO creation, Change LcsO<op LcsO operational: GenKeyPair can never run on it again
F1D0 the record, 110 bytes: PQ, v2, profile, serial, key OID, qx, qy, owner, date, build 140 bytes, zeros, Change LcsO<op, Read ALW frozen
F1D1 the binding, 83 bytes: PB, v1, E0F0’s (r, s) over binding_digest, SHA-256(cert)[:16] as F1D0 frozen
E0F0 Infineon’s factory key, certified in E0E0 Change NEV, Execute ALW, P-256, usage Auth untouched

The ceremony (firmware/provision_tm.py)

Step What Reversible?
T1 Pre-flight UID, LcsA, security events. The certificate is verified against Infineon’s CA key on the Pico, and E0F0 signs a probe that verifies under the certificate’s key. Checks E0F1, F1D0 and F1D1 are factory. Saves every object’s metadata to snapshot-tm-<serial>.json read-only
T2 Make the key GenKeyPair in E0F1 (ALLOW_GENKEY, the arm, a 3 s hold) yes, it can be made again until T7
T3 Test Two signatures (a plain digest and a WebAuthn assertion), verified on the Pico, low s read-only
T4 Bind E0F0 signs the binding, verified under the certificate read-only
T5 Owner The owner address, to compare on the laptop read-only
T6 Record F1D0 and F1D1 written and read back yes, until T7
T7 Seal LcsO operational on F1D1, F1D0, E0F1 (ALLOW_LOCK, the arm, a 3 s hold) no
T8 Sealed Checks everything is sealed, the record matches, the binding verifies, the key still signs, and E0F0 still signs as its certificate’s key (live) with Infineon’s rules unchanged. Registers the chip in chips.json read-only
T9 Freeze the spares LcsO operational on every object the profile doesn’t use (tmcfg.SPARES, 23 on the real chip) and on Infineon’s E0F0, E0E0, E140 with their rules as they came (tmcfg.FACTORY_OBJS), except any TRUSTM_KEEP_OPEN names (ALLOW_LOCK, the arm, a 3 s hold) no
T10 Done quorum-tm1 sealed, the spares frozen, the key signs; reminds you to turn ALLOW_* off read-only

On the board (the full runbook: mainnet/RUNBOOK.md):

  1. tools/fw ceremony start (--unpin for a pinned chip, --keep-open F1D2 to leave a spare open). One yes: ALLOW_* on, the pin out if asked, then the dual image pushed; the board restarts into the ceremony.
  2. A on the ceremony banner, then T1–T10.
  3. tools/fw ceremony finish. One yes: ALLOW_* off, pin, the console image pushed and verified.
  4. tools/fw attest, which only reads, writes the chip’s public record to docs/mainnet/attest/. tools/quorum verify checks it with no board.

The signing console never carries the ceremony (test-vectors/manifest.mjs). Flash was never the reason: Build 1’s filesystem is 2,560 KB with 2,208 KB free, the console image is 176 KB, and the Trust M ceremony adds about 22 KB.

X on the banner shows each object’s lifecycle and access conditions, read-only.

What the real chip showed (2026-09-28)

The chip: Adafruit 4351, UID cd16338401001c000100000a091b5c000b0062006c801010710809, serial 0a091b5c000b0062006c.

What it is:

Certificate and signing:

The security event counter (E0C5):

Error codes (F1C2): 0x01 unknown OID, 0x07 reading a key object’s data, 0x0B CalcSign with an empty key.

T1–T6 passed on this chip, driven headlessly from the laptop (firmware mounted over USB, flags injected in RAM, nothing written to the board’s flash):

Not yet seen on silicon: whether GenKeyPair raises the security event counter (TM2’s T2 read 0 after, but on an idle credit).

TM2, the sacrificial chip (2026-10-01)

Serial 0a091b5c000b00620076, same batch as TM1 (firmware 80101071, build 0809, CA 101, certificate 34dc2cab). The whole ceremony on the device (the Waveshare board, dual image), then the refusals from the laptop (tools/board/tm_refusals.py, which runs only on the serial it names).

The ceremony: T1–T10 all green, security counter 0 throughout. Every step’s result is in the board’s snapshot-tm-<serial>-steps.json.

The refusals, each tried for real:

Tried Answer
GenKeyPair on the sealed E0F1 refused, error 07
SetDataObject on the sealed F1D0, the same bytes refused, 07
SetDataObject on a frozen spare (F1D2) refused, 07; still zeros
GenKeyPair on a frozen spare key slot (E0F2) refused, 07
Lowering a frozen lifecycle (F1D3, op to init) refused, 05
Changing a frozen object’s Change condition (F1D4 to ALW) refused, 07

Afterwards the key still signed as the record’s key, F1D0 was unchanged, and no metadata had moved. So “sealed” and “frozen” mean what this profile says: nothing can make a key, write data or change the rules of an operational object with Change LcsO<op. tools/quorum verify on TM2’s record passes every line, “23 of 23 frozen” included.

The emulator now gives the same codes (trustm_sim.py: 05 for a lower lifecycle, checked before the access condition; 07 for the rest).

Every other way to rewrite it (tools/board/tm_rewrites.py, later that night):

Tried on TM2 Answer
Frozen F1D2: a write at offset 0 or 100, an erase-and-write refused, 07
Frozen F1D2: a write past the end (offset 140) refused, 08 (the bounds are checked first)
Frozen F1D2: a write of nothing refused, 04
Sealed F1D0: one byte at 109 refused, 07
Frozen F1E0, E0E1 (cert slot), E0E8 (trust anchor), E120 (8 bytes), the UID refused, 07
Frozen counter E120: count +1 refused, 0E
Frozen F1D2: Read NEV, or LcsO op + Change ALW refused, 07
Sealed E0F1: Execute NEV refused, 07
Frozen F1D2: LcsO op again (the same value) accepted, nothing changes
An empty metadata TLV refused, 05
LcsA lowered to initialization refused, 05
E0F0 (factory key, LcsO creation): Change NEV to ALW ACCEPTED
E0F0, E0E0, E140: LcsO raised to operational accepted

The security event counter under a burst (tools/board/tm_burst.py; raw numbers in the local chips/trustm-0a091b5c000b00620076-sec.json):

The console on the real chip (2026-09-28). The dual image ran with TM1 pinned as locked: False, against the laptop’s mock Safe (tools/quorum serve), with nothing on chain.

The spares (T9)

The real chip lists 23 objects that quorum-tm1 doesn’t use, all with the factory Change condition LcsO < op:

T9 raises each one’s LcsO to operational, and nothing else, exactly as T7 did for the key. From then on nothing can write them or make a key in them. Before it writes, T9 reads each object again and touches only one that is still as it came.

Infineon’s own objects too (since TM2’s rewrite tests). The factory key E0F0, its certificate E0E0 and the binding secret E140 also come at LcsO creation. TM2 showed that, below operational, an object’s metadata can be rewritten whatever its Change condition says, so T9 raises their lifecycle as well and leaves their rules as Infineon set them: E0F0 and E0E0 Change NEV, E140 LcsO < op || Conf(E140). E140 then can’t be read any more, which is what it’s for. That makes 26 objects in all. A factory object whose rules already differ counts as tampered, and T8 stops.

Why freeze them. The ATECC profile freezes every byte of its chip; until T9 a Trust M had three frozen objects and 23 writable ones. Writable spares don’t endanger the key. Frozen, the chip has nothing else on it to explain.

Keeping one open. TRUSTM_KEEP_OPEN = ["E0F2", "E0F3"] in secrets.py (or tools/fw ceremony start --keep-open E0F2,E0F3) keeps spares open, such as the spare key slots for a second key later (mainnet/CONFIGURATION.md).

The console’s Chip page shows them: “all 26 frozen” (23 spares and Infineon’s three), or which are still open. It reads them when the page opens, never at boot.

On the screens

While it signs, the console shows short facts read from the chip itself (tmcfg.info_lines, quorum.chip_info):

Walkthrough: docs/quorum-ui/README.md, iteration 5.

The emulator