Skip to content

Commit 75d4050

Browse files
committed
Generate random passcode and discriminator
QR Code and Manual Codes based on that too.
1 parent 818c9e6 commit 75d4050

File tree

5 files changed

+151
-19
lines changed

5 files changed

+151
-19
lines changed

circuitmatter/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from . import session
1515
from .device_types.utility.root_node import RootNode
1616

17-
__version__ = "0.1.0"
17+
__version__ = "0.2.0"
1818

1919

2020
class CircuitMatter:
@@ -110,6 +110,7 @@ def start_commissioning(self):
110110
from . import pase
111111

112112
pase.show_qr_code(self.vendor_id, self.product_id, discriminator, passcode)
113+
print("Manual code:", self.nonvolatile["manual_code"])
113114
instance_name = self.random.urandom(8).hex().upper()
114115
self.mdns_server.advertise_service(
115116
"_matterc",

circuitmatter/certificates.py

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@
44
import hashlib
55

66
from . import tlv
7+
from . import pase
78
from .data_model import Enum8
89

910
import ecdsa
11+
from ecdsa.curves import NIST256p
1012
from ecdsa import der
1113

1214
PAI_KEY_DER = b"\x30\x77\x02\x01\x01\x04\x20\xbb\x76\xa5\x80\x5f\x97\x26\x49\xaf\x1e\x8a\x87\xdc\x45\x57\xe6\x2c\x09\x00\xe5\x07\x09\xe8\x5c\x79\xc6\x44\xdf\x78\x90\xe5\x96\xa0\x0a\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\xa1\x44\x03\x42\x00\x04\x37\x5d\x2b\xc8\xc6\x15\x27\x5b\xfd\x84\x8b\x52\xfe\x21\x96\xe2\xa1\x4e\xf3\xcc\x91\xae\xf0\x5d\xff\x85\x1c\xbc\x19\xb1\xa9\x35\x45\x8c\xfe\x04\xaa\x42\x4e\x01\x6d\xe3\xd6\x74\xdc\x5b\x73\x29\xbd\x77\x57\xfd\xdb\x32\x38\xd6\x26\x73\x62\x9b\x3c\x79\x08\x45"
1315

16+
INVALID_PASSCODES = [
17+
0,
18+
11111111,
19+
22222222,
20+
33333333,
21+
44444444,
22+
55555555,
23+
66666666,
24+
77777777,
25+
88888888,
26+
12345678,
27+
87654321,
28+
]
29+
1430

1531
class CertificationType(Enum8):
1632
DEVELOPMENT_AND_TEST = 0
@@ -171,6 +187,7 @@ def generate_dac(
171187
der.encode_octet_string(hashlib.sha1(public_key).digest())
172188
),
173189
)
190+
# ID of the CircuitMatter 0xFFF4 PAI
174191
authority_key_id = b"\x30\x1f\x06\x03\x55\x1d\x23\x04\x18\x30\x16\x80\x14\x07\xf8\x38\x0a\x5f\x01\x36\xfc\xe2\x36\xbd\x45\xf2\x88\xff\x22\xdc\xa6\xf4\xa7"
175192
extensions = der.encode_constructed(
176193
3, der.encode_sequence(basic_constraints, key_usage, key_id, authority_key_id)
@@ -201,19 +218,103 @@ def generate_dac(
201218
return dac_cert, dac_key
202219

203220

221+
def compute_verifier(passcode: int, salt: bytes, iterations: int) -> bytes:
222+
w0, w1 = pase._pbkdf2(passcode, salt, iterations)
223+
L = NIST256p.generator * w1
224+
225+
return w0.to_bytes(NIST256p.baselen, byteorder="big") + L.to_bytes("uncompressed")
226+
227+
228+
# Look up tables for Verhoeff Algorithm
229+
# From: https://en.wikipedia.org/wiki/Verhoeff_algorithm#Table-based_algorithm
230+
D_TABLE = (
231+
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
232+
+ b"\x01\x02\x03\x04\x00\x06\x07\x08\x09\x05"
233+
+ b"\x02\x03\x04\x00\x01\x07\x08\x09\x05\x06"
234+
+ b"\x03\x04\x00\x01\x02\x08\x09\x05\x06\x07"
235+
+ b"\x04\x00\x01\x02\x03\x09\x05\x06\x07\x08"
236+
+ b"\x05\x09\x08\x07\x06\x00\x04\x03\x02\x01"
237+
+ b"\x06\x05\x09\x08\x07\x01\x00\x04\x03\x02"
238+
+ b"\x07\x06\x05\x09\x08\x02\x01\x00\x04\x03"
239+
+ b"\x08\x07\x06\x05\x09\x03\x02\x01\x00\x04"
240+
+ b"\x09\x08\x07\x06\x05\x04\x03\x02\x01\x00"
241+
)
242+
243+
INV_TABLE = b"\x00\x04\x03\x02\x01\x05\x06\x07\x08\x09"
244+
245+
P_TABLE = (
246+
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
247+
+ b"\x01\x05\x07\x06\x02\x08\x03\x00\x09\x04"
248+
+ b"\x05\x08\x00\x03\x07\x09\x06\x01\x04\x02"
249+
b"\x08\x09\x01\x06\x00\x04\x03\x05\x02\x07"
250+
+ b"\x09\x04\x05\x03\x01\x02\x06\x08\x07\x00"
251+
+ b"\x04\x02\x08\x06\x05\x07\x03\x09\x00\x01"
252+
+ b"\x02\x07\x09\x03\x08\x00\x06\x04\x01\x05"
253+
+ b"\x07\x00\x04\x06\x09\x01\x03\x02\x05\x08"
254+
)
255+
256+
257+
def _bcd(buf, n):
258+
div = 10 ** (len(buf) - 1)
259+
for i in range(len(buf)):
260+
buf[i] = (n // div) % 10
261+
div //= 10
262+
263+
264+
def compute_manual_code(
265+
discriminator, passcode, vendor_id=None, product_id=None
266+
) -> str:
267+
vid_pid_present = 0
268+
if vendor_id is not None and product_id is not None:
269+
vid_pid_present = 1
270+
271+
digits = memoryview(bytearray(11))
272+
digits[0] = (vid_pid_present << 2) | (discriminator >> 10)
273+
d2_6 = ((discriminator & 0x300) << 6) | (passcode & 0x3FFF)
274+
_bcd(digits[1:6], d2_6)
275+
d7_10 = passcode >> 14
276+
_bcd(digits[6:10], d7_10)
277+
278+
# Checksum of zero. We'll overwrite it.
279+
digits[10] = 0
280+
281+
c = 0
282+
for i, n in enumerate(reversed(digits)):
283+
c = D_TABLE[c * 10 + P_TABLE[(i % 8) * 10 + n]]
284+
285+
digits[10] = INV_TABLE[c]
286+
digits = [str(x) for x in digits]
287+
digits.insert(4, "-")
288+
digits.insert(8, "-")
289+
290+
return "".join(digits)
291+
292+
204293
def generate_initial_state(vendor_id, product_id, product_name, random_source):
205294
if vendor_id != 0xFFF4 or product_id != 0x1234:
206295
raise ValueError("Invalid vendor_id or product_id")
207296

208297
cd = generate_certificates(vendor_id=vendor_id, product_id=product_id)
209298

210299
dac_cert, dac_key = generate_dac(vendor_id, product_id, product_name, random_source)
300+
301+
passcode = 0
302+
while passcode in INVALID_PASSCODES:
303+
passcode = random_source.randbelow(99999999)
304+
discriminator = random_source.randbelow(1 << 12) # A 12-bit random number
305+
iteration_count = 10000
306+
salt = random_source.urandom(32)
307+
verifier = compute_verifier(passcode, salt, iteration_count)
308+
# This does *NOT* follow the spec because the passcode is stored alongside the verifier.
309+
# The spec wants the passcode stored physically on the box and package of the device in
310+
# the setup code and QR Code. The verifier is in the device only.
211311
initial_state = {
212-
"discriminator": 3840,
213-
"passcode": 67202583,
214-
"iteration-count": 10000,
215-
"salt": "5uCP0ITHYzI9qBEe6hfU4HfY3y7VopSk0qNvhvznhiQ=",
216-
"verifier": "0xGqxJFBr/ViQt3lv1Yw5F0GcPBAtFFvXB+EcIIjH5cEsjkPZHDQyFWjA6Ide+2gafYnZgIy6gJBgdJOlD8htAZKe0i6nIhT/ADsBWH4CvZcl37n/ofEEECWSEBV4vy/0A==",
312+
"discriminator": discriminator,
313+
"passcode": passcode,
314+
"manual_code": compute_manual_code(discriminator, passcode),
315+
"iteration-count": iteration_count,
316+
"salt": binascii.b2a_base64(salt, newline=False).decode("utf-8"),
317+
"verifier": binascii.b2a_base64(verifier, newline=False).decode("utf-8"),
217318
"devices": {
218319
"root": {
219320
"0x3e": {

circuitmatter/pase.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,6 @@ def initiator_values(passcode, salt, iterations) -> tuple[bytes, bytes]:
118118
)
119119

120120

121-
def verifier_values(passcode: int, salt: bytes, iterations: int) -> tuple[bytes, bytes]:
122-
w0, w1 = _pbkdf2(passcode, salt, iterations)
123-
L = NIST256p.generator * w1
124-
125-
return w0.to_bytes(NIST256p.baselen, byteorder="big"), L.to_bytes("uncompressed")
126-
127-
128121
# w0 and w1 are big-endian encoded
129122
def Crypto_pA(w0, w1) -> bytes:
130123
return b""
@@ -252,7 +245,6 @@ def _base38_encode(buf) -> str:
252245
for i in range(0, len(buf), 3):
253246
value = 0
254247
remaining = min(3, len(buf) - i)
255-
print("remaining", remaining)
256248
for j in range(remaining):
257249
value |= buf[i + j] << (j * 8)
258250
outputs = 5
@@ -263,11 +255,10 @@ def _base38_encode(buf) -> str:
263255
for j in range(outputs):
264256
encoded.append(alphabet[value % 38])
265257
value //= 38
266-
print(encoded)
267258
return "".join(encoded)
268259

269260

270-
def show_qr_code(vendor_id, product_id, discriminator, passcode):
261+
def compute_qr_code(vendor_id, product_id, discriminator, passcode) -> str:
271262
total_bits = 3 + 16 * 2 + 2 + 8 + 12 + 27 + 4
272263
total_bytes = total_bits // 8
273264
buf = bytearray(total_bytes)
@@ -282,10 +273,12 @@ def show_qr_code(vendor_id, product_id, discriminator, passcode):
282273
offset = _write_bits(buf, offset, 8, discovery)
283274
offset = _write_bits(buf, offset, 12, discriminator)
284275
offset = _write_bits(buf, offset, 27, passcode)
285-
print(buf.hex(" "))
286276

287-
encoded = _base38_encode(buf)
277+
return _base38_encode(buf)
278+
288279

280+
def show_qr_code(vendor_id, product_id, discriminator, passcode):
281+
encoded = compute_qr_code(vendor_id, product_id, discriminator, passcode)
289282
import qrcode
290283

291284
qr = qrcode.QRCode(
@@ -296,4 +289,5 @@ def show_qr_code(vendor_id, product_id, discriminator, passcode):
296289
)
297290
qr.add_data("MT:")
298291
qr.add_data(encoded)
292+
print("QR code data: MT:" + encoded)
299293
qr.print_ascii()

examples/replay.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def run(replay_file=None):
6363
if __name__ == "__main__":
6464
import sys
6565

66-
print(sys.argv)
6766
replay_file = None
6867
if len(sys.argv) > 1:
6968
replay_file = sys.argv[1]

tests/test_setup_codes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from circuitmatter import certificates, pase
2+
3+
4+
def test_basic():
5+
vendor_id = 0xFFF4
6+
product_id = 0x1234
7+
discriminator = 2721
8+
passcode = 42430398
9+
assert (
10+
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
11+
== "MNOA5D4V0163Z072Y00"
12+
)
13+
assert certificates.compute_manual_code(discriminator, passcode) == "2449-902-5895"
14+
15+
16+
def test_min():
17+
vendor_id = 0xFFF4
18+
product_id = 0x1234
19+
discriminator = 0
20+
passcode = 1
21+
assert (
22+
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
23+
== "MNOA55UM00ID0000000"
24+
)
25+
assert certificates.compute_manual_code(discriminator, passcode) == "0000-010-0007"
26+
27+
28+
def test_max():
29+
vendor_id = 0xFFF4
30+
product_id = 0x1234
31+
discriminator = 0xFFF
32+
passcode = 99999998
33+
assert (
34+
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
35+
== "MNOA5N15271DQ36B420"
36+
)
37+
assert certificates.compute_manual_code(discriminator, passcode) == "3575-986-1036"

0 commit comments

Comments
 (0)