Wiki / Concepts

Zigbee install code

The per-device secret printed on a Zigbee 3.0 label, and the unique link key a coordinator derives from it instead of the well-known default.

What is printed on the label

An install code is the per-device secret printed on the label of a Zigbee 3.0 product. It exists to close the oldest hole in Zigbee: with standard security, a joining device receives the network key encrypted under the well-known default Trust Center link key ZigBeeAlliance09, so anyone who catches the join catches the whole network. An install code replaces that shared default with a link key nobody else has.

What it is

The label carries the device's 64-bit IEEE address and a code, printed as hex in groups of four characters, or encoded in a QR sticker alongside it. The code is a 16-octet secret with a short checksum appended, so a mistyped digit is caught by the commissioning tool instead of turning into a silent join failure. Codes of 6, 8, 12 and 16 octets are all legal; 16 is what consumer and commercial gear ships.

The coordinator does not use the code directly. It runs it through Zigbee's own hash primitive, AES-MMO-128 (Matyas-Meyer-Oseas, built from AES-128), and the 16-octet result is that device's unique Trust Center link key. zigpy.util.convert_install_code is the readable reference implementation, and it is worth reading before writing your own: the exact bytes that go into the hash are not obvious from the description.

How a join uses it

  1. The installer registers the device on the coordinator by its IEEE address and its printed code, before the device is powered on.
  2. The coordinator derives the link key from the code and stores it against that address.
  3. The device joins. The Trust Center sends the network key in an APS Transport Key command, encrypted under the key-transport key derived from that link key.
  4. Only that one device can read it. ZigBeeAlliance09 decrypts nothing.

Why it matters

For a defender this is the single most effective Zigbee hardening step, and it is the reason a modern commercial deployment does not fall to a passive sniffer. For an assessor it changes the shape of the work: recovering the network key stops being a one-command job and becomes a question of whether the codes leaked. They leak constantly. They are printed on labels inside enclosures, exported to commissioning spreadsheets, mailed to subcontractors, photographed during handover, and left on the box in the plant room.

Note also what an install code does NOT protect. It secures the delivery of the network key. Once delivered, the network key is shared by the whole mesh exactly as before, so one compromised device still reads and forges everything.

How to assess it

# derive the link key a coordinator would store for a printed code
python3 - <<'PY'
from zigpy.util import convert_install_code
print(convert_install_code(bytes.fromhex("...")).hex())
PY

Feed the result to Wireshark as a key in the Zigbee key table and it decrypts the Transport Key command, learns the network key from it, and opens the whole capture. If the derivation is wrong the message integrity code simply does not verify, which makes the whole chain self-checking: you never have to wonder whether a candidate key is right.

Pitfalls

  • The printed code and the link key are different things. Pasting the printed code into a key table decrypts nothing.
  • The checksum is part of what is printed. Whether it is part of what is hashed is a separate question, and getting it wrong yields a key that fails silently.
  • A device already joined keeps working with a lost label, which is why damaged labels stay in service for years.
  • An install code is per device. Recovering one gives you that device's join, not the neighbouring PAN's.

Further reading