Chiffrage d'une trame ZigBee avec AES-128-CCM*

Cette page détaille comment crypter une trame ZigBee avec l'algorithme AES-128-CCM*. Le chiffrage ZigBee est réalisé en trois étapes :

La trame utilisée dans cet exemple est une trame interceptée sur un véritable réseau ZigBee détaillée sur cette page : Autopsie d'une trame ZigBee.

L'algorithme AES-128-CCM* est détaillé dans la section 4.3.1.1 et dans l'annexe A de la spécification ZigBee

Bibliothèques et fonctions

Dans la suite, nous utiliserons la bibliothèque de cryptographie et les fonctions suivantes :

!pip install pycrypto

# Python Cryptography Toolkit for AES encryption
from Crypto.Cipher import AES

# Print aray of bytes in hexadecimal
def printhex(x, sep = ' '):
  str = ''
  for b in x:  
    byte = hex(b)[2:]
    if (len(byte)<2)  :
      str += '0' + byte + sep
    else:
      str += hex(b)[2:] + sep 
  print (str[:-1].upper())

# 16 bits padding (with 0x00)
def pad(x):
  n=(16-len(x)%16)%16
  return x + bytes([0x00]*n)

Clé réseau

D'après l'annexe A.2 de la spécification ZigBee:

A bit string Key of length keylen bits to be used as the key. Each entity shall have evidence that access to this key is restricted to the entity itself and its intended key-sharing group member(s).

Dans cet exemple, la clé de chiffrage a été préalablement interceptée sur le réseau grâce au sniffer ZigBee :

Network key: AD:8E:BB:C4:F9:6A:E7:00:05:06:D3:FC:D1:62:7F:B8

key =   bytes([0xAD, 0x8E, 0xBB, 0xC4, 0xF9, 0x6A, 0xE7, 0x00, 0x05, 0x06, 0xD3, 0xFC, 0xD1, 0x62, 0x7F, 0xB8])
printhex (key)
AD 8E BB C4 F9 6A E7 00 05 06 D3 FC D1 62 7F B8

Paramètre L

D'après l'annexe A de la spécification ZigBee:

The length L of the message length field, in octets, shall have been chosen. Valid values for L are the integers 2, 3,..., 8 (the value L=1 is reserved).

Dans notre cas, L=2.

L = 2

M (longueur du code d'intégrité)

D'après l'annexe A de la spécification ZigBee:

The length M of the authentication field, in octets, shall have been chosen. Valid values for M are the integers 0, 4, 6, 8, 10, 12, 14, and 16. (The value M=0 corresponds to disabling authenticity, since then the authentication field contains an empty string.)

M est la longueur de la trame d'intégrité (MIC) en nombre d'octets. Dans le cas présent, et d'après la section 4.5.1.1.1 de la spécification ZigBee, nous pouvons en décuire que la longueur du MIC est de 4 octets : AC:4C:76:AF.

M=4

NWK header

L'en-tête réseau (NWK header) est extraite de la trame brute :

NWK header: 48:02:00:00:8A:5C:1E:5D

NwkHeader = bytes([0x48, 0x02, 0x00, 0x00, 0x8A, 0x5C, 0x1E, 0x5D])
printhex (NwkHeader)
48 02 00 00 8A 5C 1E 5D

NWK AUX Header

D'après la section 4.5.1 de la spécification ZigBee l'en-tête réseau auxilière est contruite de la façon suivante :

The auxiliary frame header, as illustrated by Figure 4.18, shall include a security control field and a frame counter field, and may include a sender address field and key sequence number field.

Octets: 1 4 0/8 0/1
Security control Frame counter Source address Key sequence number

Vous trouverez plus de détails dans la section sur l'entête réseau auxilière.

AuxiliaryHeader = bytes([0x2D, 0xE1, 0x00, 0x00, 0x00, 0x01, 0x3C, 0xE8, 0x01, 0x00, 0x8D, 0x15, 0x00, 0x01])
printhex (AuxiliaryHeader)
2D E1 00 00 00 01 3C E8 01 00 8D 15 00 01

Nonce

D'après la section 4.5.2.2 de la spécification ZigBee, le nonce est construit de la façon suivante :

The nonce input used for the CCM encryption and authentication transformation and for the CCM decryption and authentication checking transformation consists of data explicitly included in the frame and data that both devices can independently obtain.

Octets: 8 4 1
Source address Frame counter Security control
nonce = bytes([0x01, 0x3C, 0xE8, 0x01, 0x00, 0x8D, 0x15, 0x00  ,  0xE1, 0x00, 0x00, 0x00  ,  0x2D])
print (len(nonce), 'bytes')
printhex (nonce)
13 bytes
01 3C E8 01 00 8D 15 00 E1 00 00 00 2D

a et m

D'après la section 4.3.1.1 de la spécification ZigBee, les paramètres a et m sont coinstruits de la façon suivante :

If the security level requires encryption, the octet string a shall be the string NwkHeader || AuxiliaryHeader and the octet string m shall be the string Payload.

Dans notre cas, le niveau de sécurité nécessite un chiffrage :

a = NwkHeader || AuxiliaryHeader

m = payload (Les données utiles (payload) contiennent les données envoyées par le bouton, voir la section NWK payload pour plus de détails).

# Octet string a
a = NwkHeader + AuxiliaryHeader
print ('a:')
printhex (a)

# Octet string m
print ('\nm:')
m = bytes([0x00, 0x01, 0x12, 0x00, 0x04, 0x01, 0x01, 0x62, 0x18, 0xC3, 0x0A, 0x55, 0x00, 0x21, 0x01, 0x00])
printhex(m)
a:
48 02 00 00 8A 5C 1E 5D 2D E1 00 00 00 01 3C E8 01 00 8D 15 00 01

m:
00 01 12 00 04 01 01 62 18 C3 0A 55 00 21 01 00

AddAuthData

D'après l'annexe A.2.1 de la spécification ZigBee:

  1. Form the octet string representation L(a) of the length l(a) of the octet string a. [...]
  2. Right-concatenate the octet string L(a) with the octet string a itself. Note that the resulting string contains and a encoded in a reversible manner.
  3. Form the padded message AddAuthData by right-concatenating the resulting string with the smallest non-negative number of all-zero octets such that the octet string AddAuthData has length divisible by 16.

Dans notre cas, (cas b de la spécification ZigBee), L(a) est la longueur l(a) codée sur 2 octets.

# Right-concatenate the octet string L(a) with the octet string a itself.
AddAuthData = len(a).to_bytes(2, byteorder = 'big') + a
# Form the padded message AddAuthData
AddAuthData = pad(AddAuthData)

print(len(AddAuthData), 'bytes')
printhex(AddAuthData)
32 bytes
00 16 48 02 00 00 8A 5C 1E 5D 2D E1 00 00 00 01 3C E8 01 00 8D 15 00 01 00 00 00 00 00 00 00 00

PlaintextData

D'après l'annexe A.2.1 de la spécification ZigBee, la donnée PlaintextData est construite de la façon suivante :

Form the padded message PlaintextData by right-concatenating the octet string m with the smallest non-negative number of all-zero octets such that the octet string PlaintextData has length divisible by 16.

PlaintextData = m
# Padding (not necessary here, because length(m)=16)
PlaintextData = pad(PlaintextData)

print (len(PlaintextData), 'bytes')
printhex (PlaintextData)
16 bytes
00 01 12 00 04 01 01 62 18 C3 0A 55 00 21 01 00

AuthData

D'après l'annexe A.2.1 de la spécification ZigBee le message AuthData est construit de la façon suivante :

Form the message AuthData consisting of the octet strings AddAuthData and PlaintextData:

AuthData = AddAuthData || PlaintextData

Notons que AuthData est une partie de la donnée d'entrée de l'algorithme EAS-CBC et peut être tronqué en section de 16 octets qui seront chacune traitée par l'algorithme AES :

AuthData = B1 || B2 || B3 || B4

AuthData = AddAuthData + PlaintextData
print (len(AuthData), 'bytes')
printhex (AuthData)
48 bytes
00 16 48 02 00 00 8A 5C 1E 5D 2D E1 00 00 00 01 3C E8 01 00 8D 15 00 01 00 00 00 00 00 00 00 00 00 01 12 00 04 01 01 62 18 C3 0A 55 00 21 01 00

Flags

Les flags sont agrégés sur un octet. Cet octet est construit d'après la section A.2.2 de la spécification ZigBee:

Form the 1-octet Flags field consisting of the 1-bit Reserved field, the 1-bit Adata field, and the 3-bit representations of the integers M and L, as follows:

Flags = Reserved || Adata || M || L

Here, the 1-bit Reserved field is reserved for future expansions and shall be set to ‘0’.

Reserved = 0b0

The 1-bit Adata field is set to ‘0’ if l(a)=0, and set to ‘1’ if l(a)>0.

Adata = 0b1 since l(a)=22

The L field is the 3-bit representation of the integer L-1, in most-significant-bit-first order.

Comme L=2, L-1=1 => L field = 001

The M field is the 3-bit representation of the integer (M-2)/2 if M>0 and of the integer 0 if M=0, in most-significant-bit-first order

Comme M=4, M-2/2=1 => M field = 001

En conclusion :

Flags = 0b01001001 = 0x49

Flags = bytes([0x49]) # = ([0b01001001])
printhex (Flags)
49

B0

B0 est le premier paquet de 16 octets de l'algoritme AES. D'après la section A.2.2 de la spécification ZigBee, BO est construit de la façon suivante :

Form the 16-octet B0 field consisting of the 1-octet Flags field defined above, the 15-L octet nonce field N, and the L-octet representation of the length field l(m), as follows:

B0 = Flags || Nonce N || l(m)

B0 = Flags + nonce + len(m).to_bytes(2, byteorder = 'big')
print ('Should be 16 bytes =>', len(B0), 'bytes')
printhex (B0)
Should be 16 bytes => 16 bytes
49 01 3C E8 01 00 8D 15 00 E1 00 00 00 2D 00 10

X0

XO est le vecteur d'initialisation (IV Initialization vector). D'après la section A.2.2 de la spécification ZigBee, X0 est construit de la façon suivante :

Parse the message AuthData as B1 || B2 || ... ||Bt, where each message block Bi is a 16-octet string. The CBC-MAC value Xt+1 is defined by:

X0: = 0b0 (x128); Xi+1:= E(Key, Xi ⊕ Bi) for i=0, ... , t.

X0 est donc initialisé avec :

X0 = 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00

X0 = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
printhex(X0)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Authentication tag T (MIC)

Dans cette section, nous allons générer le code d'intégrité (MIC).

D'après la section A.2.2 de la spécification ZigBee, l'algorithme utilisé pour le cryptage est AES-128-CBC. Plutôt que de chiffrer les groupes de 16 octets un par un (B0, B1, B2 ...), nous allons utiliser la biblithèque Python Cryptography Toolkit pour chiffrer d'un seul coup les données avec les paramètres suivants :

Le MIC généré est AC 4C 76 AF qui correspond bien à la donnée NWK MIC de notre trame.

cipher = AES.new(key, AES.MODE_CBC, X0)
X1 = cipher.encrypt(B0 + AuthData)
print ('MIC = ')
printhex (X1[-16:-12])
MIC = 
AC 4C 76 AF

Encryption transformation

La dernière partie du processus de chiffrage s'appelle "encryption transformation" et est expliquée dans la Section A.2.3 de la spécification ZigBee.

La première étape consiste à construire un octet contenant les Flags:

Form the 1-octet Flags field consisting of two 1-bit Reserved fields, and the 3- bit representations of the integers 0 and L, as follows:

Flags = Reserved || Reserved || 0 || L

Here, the two 1-bit Reserved fields are reserved for future expansions and shall be set to ‘0’. The L field is the 3-bit representation of the integer L-1, in most-significant- bit-first order. The ‘0’ field is the 3-bit representation of the integer 0, in most-significant-bit-first order.

Dans notre cas, L=2 => L-1 = 1

Flags = bytes ([0b00000001])
printhex (Flags)
01

Ai

D'après la section A.2.3 de la spécification ZigBee, les groupes de 16 octets (Ai) snt construits de la façon suivante :

Define the 16-octet Ai field consisting of the 1-octet Flags field defined above, the 15-L octet nonce field N, and the L-octet representation of the integer i, as follows:

Ai = Flags || Nonce N || Counter i, for i=0, 1, 2, …

Note that this definition ensures that all the Ai fields are distinct from the B0 fields that are actually used, as those have a Flags field with a non-zero encoding of M in the positions where all Ai fields have an all-zero encoding of the integer 0 (see section A.2.2, step 1).

Comme PlaintextData est un message de 16 octets de long, nous n'aurons besoin que de A0 et A1.

A0 = Flags + nonce + bytes([0x00, 0x00])
A1 = Flags + nonce + bytes([0x00, 0x01])
printhex (A0)
printhex (A1) 
01 01 3C E8 01 00 8D 15 00 E1 00 00 00 2D 00 00
01 01 3C E8 01 00 8D 15 00 E1 00 00 00 2D 00 01

AES CTR

L'algorithme utilisé pour chiffre les données utiles est AES-CTR. Ici, nous allons utiliser la bibliothèque de chiffrage Python Cryptography Toolkit qui supporte l'algorithme AES-CTR. Comme PlaintextData est codé su 16 bits, nous allons tricher we cheat en fournissant toujours A1 dans le compteur CTR.

D'après la trame brute cryptée captée sur le réseau, la donnée utile cryptée devrait être :

Expected encrypted payload: EA 59 DE 1F 96 0E EA 8A EE 18 5A 11 89 30 96 41

def counter():  
  return A1
cipher = AES.new(key, AES.MODE_CTR, counter = counter)
Ciphertext = cipher.encrypt(PlaintextData)
printhex (Ciphertext)
EA 59 DE 1F 96 0E EA 8A EE 18 5A 11 89 30 96 41

Tag d'authentification chiffré U

La dernière partie du chiffrage est le tag U (ou message chiffré d'intégrité):

D'après la section A.2.3 de la spécification ZigBee, le tage d'authentification est construit suivant la description suivante :

Define the 16-octet encryption block S0 by:

S0:= E(Key, A0)

The encrypted authentication tag U is the result of XOR-ing the string consisting of the leftmost M octets of S0 and the authentication tag T.

D'après la trame brute chiffrée interceptée sur le réseau, le tag U chifré devrait être :

Expected encrypted tag U: 4E 05 A2 43

# Encryption: S0:= E(Key, A0)
cipher = AES.new(key)
S0 = cipher.encrypt(A0)

# Perform S0[0:4] XOR T
U = bytes(a ^ b for (a, b) in zip(S0[0:4], T))
printhex (U)
4E 05 A2 43

Téléchargement


Dernière mise à jour : 17/11/2022