I'm currently working on an very simple ZigBee Green Power (GP) end device implementation. Currently, my problem is how to generate a correct MIC (message identification code).
I already have an AES-CCM implementation and tested it with the test vectors for ZigBee Green Power from the ZigBee Pro Specification. The problem is that the result I get isn't correct (it's not the same as in the specification).
I also tried to verify my implementation with other test vectors (non-GP ones) from the specification and the result was correct. So, my algorithm seems to be correct.
Is there a difference in calculating the GP MIC code to the normal ZigBee MIC?? Maybe someone has another implementation which is working for ZigBee GP??
Additional information:
The ZigBee specification I used is available here: https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf#
The test vectors which are working are in Annex C.3 of the above document.
The test vectors for the Green Power version are in Annex H.2. For the sake of completeness, the parameters are as follows (Annex H.2.3):
Key: 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCa, 0xCb, 0xCc, 0xCd, 0xCe, 0xCf
NWK Frame Control: 0x8C
GPD Src ID: 0x87654321
Security Frame Counter: 0x00000002
GPD Command ID: 0x02
No data Payload
The specification says that the nonce is constructed like this:
Nonce: SRC ID || SRC ID || Frame Counter || 0x05
Nonce: 0x21 0x43 0x65 0x87 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00 0x05
For the calculation an 'a' value is needed:
a = Header || Payload
Header = NWK FC || NWK Ext FC || SRC ID || Frame Counter
Header = 0x8C 0x10 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00
Payload = GPD Command ID = 0x20
a = 0x8C 0x10 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00 0x20
Finally, for the calculation of the AES-CBC algorithm the following paramters are needed:
length(a) = 0x0B
L(a) = 0x00 0x0B (big endian encoding of length(a))
AddAuthData = L(a) || a || padding
AddAuthData = 0x00 0x0B 0x8C 0x10 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00 0x20 0x00 0x00 0x00
Flags = 0x49
B0 = Flags || Nonce || padding
B0 = 0x49 0x21 0x43 0x65 0x87 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00 0x05 0x00 0x00
For generating the MIC code, the algorithm is used with the encryption key and the B0 ... Bi blocks (each 16 bytes). B0 is created using the nonce (see above). Usually AddAuthData and the message block is concatenated. But for the green power version, no message block is available as far as I know. Therefore, B1 ... Bn is created using the AddAuthData only. Am I right here??
B1 = AddAuthData
B1 = 0x00 0x0B 0x8C 0x10 0x21 0x43 0x65 0x87 0x02 0x00 0x00 0x00 0x20 0x00 0x00 0x00
The AES-CBC algorithm E is used with each Bi block. Bi is xor'd with the previous generated Xi result. The initial vector X0 is an all zero bits block with a length of 16 bytes. X_i+1 = E(Key, Xi XOR Bi) for i = 0 ... N
The result of the AES calculation is a 16 byte value. But only the 4 leftmost bytes are used. And the expected result is:
U = 0xCF 0x78 0x7E 0x72
But I get:
X2 = 02 1C 9F 9C 40 3A 27 B4 9A 31 64 EA 17 CF 69 D3
U = 0x02 0x1C 0x9F 0x9C
Here is a detailed AES-128-CCM* encryption with Python source code: ZigBee frame encryption with AES-128-CCM*
AES-CCM* is a combination of AES-CBC (authentication) and AES-CTR (encryption).
Authentication transformation runs AES-CBC over the 16B Bi
buffers. B0
is built from Nonce
and B1,B2,..,Bn
are the result of parsing AuthData
, whose definition is:
AuthData = AddAuthData || PlaintextData
Since you have no payload PlaintextData
is empty and then AuthData = AddAuthData
in your case.
Note that your B0
definition is not correct. There is no padding after the Nonce but the length of m
(i.e. the payload length). As you have no payload (l(m)=0
) the result is the same as padding here. But the correct definition of B0
is:
B0 = Flags || Nonce || l(m)
So here are the Bi
buffers (hex format without the 0x
prefix):
B0 = 49 21 43 65 87 21 43 65 87 02 00 00 00 05 00 00
B1 = 00 0B 8C 10 21 43 65 87 02 00 00 00 20 00 00 00
With the proper 128b key, the AES-CBC steps produces the following:
X0 = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
B0 = 49 21 43 65 87 21 43 65 87 02 00 00 00 05 00 00
X1 = 14 5B B8 1F DE D7 99 45 9D 9B 77 51 B7 31 A3 C1
B1 = 00 0B 8C 10 21 43 65 87 02 00 00 00 20 00 00 00
X2 = 02 1C 9F 9C 40 3A 27 B4 9A 31 64 EA 17 CF 69 D3
Summing it up, your are correct until now, but you are just not done yet! What you get is not U
but the T
authentication tag (not encrypted) : T = 02 1C 9F 9C
. To obtain U
you should run the encryption transformation (even though you have no payload to encrypt).
Encryption transformation uses AES-CTR. The counter is called Ai
and defined as:
Ai = Flags || Nonce || Counter
Flags
is pretty much the same as the one used for B0
calculation, but just keeping the 3 least significant bits. Then Flags = 0x01
and the Ai
are:
A0 = 01 21 43 65 87 21 43 65 87 02 00 00 00 05 00 00
A1 = 01 21 43 65 87 21 43 65 87 02 00 00 00 05 00 01
A2 = 01 21 43 65 87 21 43 65 87 02 00 00 00 05 00 02
...
A1,A2,..,An
are used the encrypt the payload which you are not interested in. But A0
is used to generate the U
encrypted tag. The operation is basically U = E(Key, A0) xor T
:
A0 = 01 21 43 65 87 21 43 65 87 02 00 00 00 05 00 00
E(Key,A0) = CD 64 E1 EE 37 25 CF 25 AD 84 00 F0 5C B4 9B 03
T = 02 1C 9F 9C
U = CF 78 7E 72
And here you have the expected result U = 0xCF 0x78 0x7E 0x72
.
User contributions licensed under CC BY-SA 3.0