Generate random passcode and discriminator

QR Code and Manual Codes based on that too.
This commit is contained in:
Scott Shawcroft 2024-10-18 14:35:12 -07:00
parent 818c9e6a7d
commit 75d4050307
No known key found for this signature in database
5 changed files with 151 additions and 19 deletions

View file

@ -14,7 +14,7 @@ from .protocol import InteractionModelOpcode, ProtocolId, SecureProtocolOpcode
from . import session
from .device_types.utility.root_node import RootNode
__version__ = "0.1.0"
__version__ = "0.2.0"
class CircuitMatter:
@ -110,6 +110,7 @@ class CircuitMatter:
from . import pase
pase.show_qr_code(self.vendor_id, self.product_id, discriminator, passcode)
print("Manual code:", self.nonvolatile["manual_code"])
instance_name = self.random.urandom(8).hex().upper()
self.mdns_server.advertise_service(
"_matterc",

View file

@ -4,13 +4,29 @@ import binascii
import hashlib
from . import tlv
from . import pase
from .data_model import Enum8
import ecdsa
from ecdsa.curves import NIST256p
from ecdsa import der
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"
INVALID_PASSCODES = [
0,
11111111,
22222222,
33333333,
44444444,
55555555,
66666666,
77777777,
88888888,
12345678,
87654321,
]
class CertificationType(Enum8):
DEVELOPMENT_AND_TEST = 0
@ -171,6 +187,7 @@ def generate_dac(
der.encode_octet_string(hashlib.sha1(public_key).digest())
),
)
# ID of the CircuitMatter 0xFFF4 PAI
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"
extensions = der.encode_constructed(
3, der.encode_sequence(basic_constraints, key_usage, key_id, authority_key_id)
@ -201,6 +218,78 @@ def generate_dac(
return dac_cert, dac_key
def compute_verifier(passcode: int, salt: bytes, iterations: int) -> bytes:
w0, w1 = pase._pbkdf2(passcode, salt, iterations)
L = NIST256p.generator * w1
return w0.to_bytes(NIST256p.baselen, byteorder="big") + L.to_bytes("uncompressed")
# Look up tables for Verhoeff Algorithm
# From: https://en.wikipedia.org/wiki/Verhoeff_algorithm#Table-based_algorithm
D_TABLE = (
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
+ b"\x01\x02\x03\x04\x00\x06\x07\x08\x09\x05"
+ b"\x02\x03\x04\x00\x01\x07\x08\x09\x05\x06"
+ b"\x03\x04\x00\x01\x02\x08\x09\x05\x06\x07"
+ b"\x04\x00\x01\x02\x03\x09\x05\x06\x07\x08"
+ b"\x05\x09\x08\x07\x06\x00\x04\x03\x02\x01"
+ b"\x06\x05\x09\x08\x07\x01\x00\x04\x03\x02"
+ b"\x07\x06\x05\x09\x08\x02\x01\x00\x04\x03"
+ b"\x08\x07\x06\x05\x09\x03\x02\x01\x00\x04"
+ b"\x09\x08\x07\x06\x05\x04\x03\x02\x01\x00"
)
INV_TABLE = b"\x00\x04\x03\x02\x01\x05\x06\x07\x08\x09"
P_TABLE = (
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
+ b"\x01\x05\x07\x06\x02\x08\x03\x00\x09\x04"
+ b"\x05\x08\x00\x03\x07\x09\x06\x01\x04\x02"
b"\x08\x09\x01\x06\x00\x04\x03\x05\x02\x07"
+ b"\x09\x04\x05\x03\x01\x02\x06\x08\x07\x00"
+ b"\x04\x02\x08\x06\x05\x07\x03\x09\x00\x01"
+ b"\x02\x07\x09\x03\x08\x00\x06\x04\x01\x05"
+ b"\x07\x00\x04\x06\x09\x01\x03\x02\x05\x08"
)
def _bcd(buf, n):
div = 10 ** (len(buf) - 1)
for i in range(len(buf)):
buf[i] = (n // div) % 10
div //= 10
def compute_manual_code(
discriminator, passcode, vendor_id=None, product_id=None
) -> str:
vid_pid_present = 0
if vendor_id is not None and product_id is not None:
vid_pid_present = 1
digits = memoryview(bytearray(11))
digits[0] = (vid_pid_present << 2) | (discriminator >> 10)
d2_6 = ((discriminator & 0x300) << 6) | (passcode & 0x3FFF)
_bcd(digits[1:6], d2_6)
d7_10 = passcode >> 14
_bcd(digits[6:10], d7_10)
# Checksum of zero. We'll overwrite it.
digits[10] = 0
c = 0
for i, n in enumerate(reversed(digits)):
c = D_TABLE[c * 10 + P_TABLE[(i % 8) * 10 + n]]
digits[10] = INV_TABLE[c]
digits = [str(x) for x in digits]
digits.insert(4, "-")
digits.insert(8, "-")
return "".join(digits)
def generate_initial_state(vendor_id, product_id, product_name, random_source):
if vendor_id != 0xFFF4 or product_id != 0x1234:
raise ValueError("Invalid vendor_id or product_id")
@ -208,12 +297,24 @@ def generate_initial_state(vendor_id, product_id, product_name, random_source):
cd = generate_certificates(vendor_id=vendor_id, product_id=product_id)
dac_cert, dac_key = generate_dac(vendor_id, product_id, product_name, random_source)
passcode = 0
while passcode in INVALID_PASSCODES:
passcode = random_source.randbelow(99999999)
discriminator = random_source.randbelow(1 << 12) # A 12-bit random number
iteration_count = 10000
salt = random_source.urandom(32)
verifier = compute_verifier(passcode, salt, iteration_count)
# This does *NOT* follow the spec because the passcode is stored alongside the verifier.
# The spec wants the passcode stored physically on the box and package of the device in
# the setup code and QR Code. The verifier is in the device only.
initial_state = {
"discriminator": 3840,
"passcode": 67202583,
"iteration-count": 10000,
"salt": "5uCP0ITHYzI9qBEe6hfU4HfY3y7VopSk0qNvhvznhiQ=",
"verifier": "0xGqxJFBr/ViQt3lv1Yw5F0GcPBAtFFvXB+EcIIjH5cEsjkPZHDQyFWjA6Ide+2gafYnZgIy6gJBgdJOlD8htAZKe0i6nIhT/ADsBWH4CvZcl37n/ofEEECWSEBV4vy/0A==",
"discriminator": discriminator,
"passcode": passcode,
"manual_code": compute_manual_code(discriminator, passcode),
"iteration-count": iteration_count,
"salt": binascii.b2a_base64(salt, newline=False).decode("utf-8"),
"verifier": binascii.b2a_base64(verifier, newline=False).decode("utf-8"),
"devices": {
"root": {
"0x3e": {

View file

@ -118,13 +118,6 @@ def initiator_values(passcode, salt, iterations) -> tuple[bytes, bytes]:
)
def verifier_values(passcode: int, salt: bytes, iterations: int) -> tuple[bytes, bytes]:
w0, w1 = _pbkdf2(passcode, salt, iterations)
L = NIST256p.generator * w1
return w0.to_bytes(NIST256p.baselen, byteorder="big"), L.to_bytes("uncompressed")
# w0 and w1 are big-endian encoded
def Crypto_pA(w0, w1) -> bytes:
return b""
@ -252,7 +245,6 @@ def _base38_encode(buf) -> str:
for i in range(0, len(buf), 3):
value = 0
remaining = min(3, len(buf) - i)
print("remaining", remaining)
for j in range(remaining):
value |= buf[i + j] << (j * 8)
outputs = 5
@ -263,11 +255,10 @@ def _base38_encode(buf) -> str:
for j in range(outputs):
encoded.append(alphabet[value % 38])
value //= 38
print(encoded)
return "".join(encoded)
def show_qr_code(vendor_id, product_id, discriminator, passcode):
def compute_qr_code(vendor_id, product_id, discriminator, passcode) -> str:
total_bits = 3 + 16 * 2 + 2 + 8 + 12 + 27 + 4
total_bytes = total_bits // 8
buf = bytearray(total_bytes)
@ -282,10 +273,12 @@ def show_qr_code(vendor_id, product_id, discriminator, passcode):
offset = _write_bits(buf, offset, 8, discovery)
offset = _write_bits(buf, offset, 12, discriminator)
offset = _write_bits(buf, offset, 27, passcode)
print(buf.hex(" "))
encoded = _base38_encode(buf)
return _base38_encode(buf)
def show_qr_code(vendor_id, product_id, discriminator, passcode):
encoded = compute_qr_code(vendor_id, product_id, discriminator, passcode)
import qrcode
qr = qrcode.QRCode(
@ -296,4 +289,5 @@ def show_qr_code(vendor_id, product_id, discriminator, passcode):
)
qr.add_data("MT:")
qr.add_data(encoded)
print("QR code data: MT:" + encoded)
qr.print_ascii()

View file

@ -63,7 +63,6 @@ def run(replay_file=None):
if __name__ == "__main__":
import sys
print(sys.argv)
replay_file = None
if len(sys.argv) > 1:
replay_file = sys.argv[1]

37
tests/test_setup_codes.py Normal file
View file

@ -0,0 +1,37 @@
from circuitmatter import certificates, pase
def test_basic():
vendor_id = 0xFFF4
product_id = 0x1234
discriminator = 2721
passcode = 42430398
assert (
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
== "MNOA5D4V0163Z072Y00"
)
assert certificates.compute_manual_code(discriminator, passcode) == "2449-902-5895"
def test_min():
vendor_id = 0xFFF4
product_id = 0x1234
discriminator = 0
passcode = 1
assert (
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
== "MNOA55UM00ID0000000"
)
assert certificates.compute_manual_code(discriminator, passcode) == "0000-010-0007"
def test_max():
vendor_id = 0xFFF4
product_id = 0x1234
discriminator = 0xFFF
passcode = 99999998
assert (
pase.compute_qr_code(vendor_id, product_id, discriminator, passcode)
== "MNOA5N15271DQ36B420"
)
assert certificates.compute_manual_code(discriminator, passcode) == "3575-986-1036"