Skip to content

Commit 818c9e6

Browse files
committed
Generate DAC ourselves from our own (test) PAI
Now generate and store keys in the json device state. Our PAI for vendor ID 0xfff4 is hard coded. Other vendor ids require externally generated certs. Fixes #19
1 parent 64d8fbd commit 818c9e6

13 files changed

+404
-420
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,4 @@ cython_debug/
164164
# CircuitMatter replay logs
165165
test_data/recorded_packets-*-*.jsonl
166166
test_data/device_state-*-*.json
167-
certification_declaration.der
167+
*-device-state.json

CircuitMatter-PAI-Cert.der

430 Bytes
Binary file not shown.

CircuitMatter-PAI-Key.der

121 Bytes
Binary file not shown.

circuitmatter/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import binascii
44
import hashlib
5+
import json
6+
import pathlib
57
import time
68

79
from . import case
@@ -22,8 +24,9 @@ def __init__(
2224
mdns_server=None,
2325
random_source=None,
2426
state_filename="matter-device-state.json",
25-
vendor_id=0xFFF1,
26-
product_id=0x8000,
27+
vendor_id=0xFFF4,
28+
product_id=0x1234,
29+
product_name="CircuitMatter Device",
2730
):
2831
if socketpool is None:
2932
import socket
@@ -43,6 +46,16 @@ def __init__(
4346
random_source = random
4447
self.random = random_source
4548

49+
state_file = pathlib.Path(state_filename)
50+
if not state_file.exists():
51+
from circuitmatter import certificates
52+
53+
initial_state = certificates.generate_initial_state(
54+
vendor_id, product_id, product_name, random_source
55+
)
56+
with open(state_filename, "w") as f:
57+
json.dump(initial_state, f, indent=1)
58+
4659
self.nonvolatile = nonvolatile.PersistentDictionary(state_filename)
4760

4861
for key in ["discriminator", "salt", "iteration-count", "verifier"]:

circuitmatter/certificates.py

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# This file should only be needed when generating certificates.
22

3+
import binascii
34
import hashlib
45

56
from . import tlv
@@ -8,7 +9,7 @@
89
import ecdsa
910
from ecdsa import der
1011

11-
import pathlib
12+
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"
1213

1314

1415
class CertificationType(Enum8):
@@ -41,39 +42,39 @@ def encode_set(*encoded_pieces):
4142
return b"\x31" + der.encode_length(total_len) + b"".join(encoded_pieces)
4243

4344

45+
def encode_utf8_string(s):
46+
encoded = s.encode("utf-8")
47+
return b"\x0c" + der.encode_length(len(encoded)) + encoded
48+
49+
4450
def generate_certificates(
4551
vendor_id=0xFFF1, product_id=0x8000, device_type=22, prefix=None
4652
):
4753
declaration = CertificationDeclaration()
4854
declaration.format_version = 1 # Always 1
4955
declaration.vendor_id = vendor_id
5056
declaration.product_id_array = [product_id]
51-
declaration.device_type_id = 0x1234 # device_type
52-
declaration.certificate_id = "ZIG20141ZB330001-24" # "CSA00000SWC00000-00"
57+
declaration.device_type_id = device_type
58+
declaration.certificate_id = "CSA00000SWC00000-00"
5359
declaration.security_level = 0 # Always 0
5460
declaration.security_information = 0 # Always 0
55-
declaration.version_number = 0x2694 # 1 # Always 1
61+
declaration.version_number = 1 # Always 1
5662
declaration.certification_type = CertificationType.DEVELOPMENT_AND_TEST
5763
declaration = declaration.encode()
5864

59-
for i in range(0, len(declaration), 16):
60-
print(f"{i:08x}", declaration[i : i + 16].hex(" "))
61-
6265
# From: https://github.com/project-chip/matter.js/blob/main/packages/protocol/src/certificate/CertificationDeclarationManager.ts
6366
# NIST256p is the same as secp256r1
6467
private_key = ecdsa.keys.SigningKey.from_string(
6568
b"\xae\xf3\x48\x41\x16\xe9\x48\x1e\xc5\x7b\xe0\x47\x2d\xf4\x1b\xf4\x99\x06\x4e\x50\x24\xad\x86\x9e\xca\x5e\x88\x98\x02\xd4\x80\x75",
6669
curve=ecdsa.curves.NIST256p,
6770
hashfunc=hashlib.sha256,
6871
)
69-
print(private_key.to_string().hex().upper())
7072
subject_key_identifier = b"\x62\xfa\x82\x33\x59\xac\xfa\xa9\x96\x3e\x1c\xfa\x14\x0a\xdd\xf5\x04\xf3\x71\x60"
7173
signature = private_key.sign_deterministic(
7274
declaration,
7375
hashfunc=hashlib.sha256,
7476
sigencode=ecdsa.util.sigencode_der_canonize,
7577
)
76-
print("signature", signature.hex(" "))
7778

7879
certification_declaration = []
7980
# version
@@ -120,9 +121,118 @@ def generate_certificates(
120121
return cms_signed
121122

122123

124+
def generate_dac(
125+
vendor_id, product_id, product_name, random_source
126+
) -> tuple[bytes, bytes]:
127+
dac_key = ecdsa.keys.SigningKey.generate(
128+
curve=ecdsa.NIST256p, hashfunc=hashlib.sha256, entropy=random_source.urandom
129+
)
130+
131+
version = der.encode_constructed(0, der.encode_integer(2))
132+
serial_number = der.encode_integer(1)
133+
signature_algorithm = der.encode_sequence(der.encode_oid(1, 2, 840, 10045, 4, 3, 2))
134+
# CircuitMatter PAI for vendor ID 0xfff4
135+
issuer = b"\x30\x32\x31\x1a\x30\x18\x06\x03\x55\x04\x03\x0c\x11\x43\x69\x72\x63\x75\x69\x74\x4d\x61\x74\x74\x65\x72\x20\x50\x41\x49\x31\x14\x30\x12\x06\x0a\x2b\x06\x01\x04\x01\x82\xa2\x7c\x02\x01\x0c\x04\x46\x46\x46\x34"
136+
137+
# Starting 10/17/2024 and never expiring
138+
validity = b"\x30\x20\x17\x0d\x32\x34\x31\x30\x31\x37\x30\x30\x30\x30\x30\x30\x5a\x18\x0f\x39\x39\x39\x39\x31\x32\x33\x31\x32\x33\x35\x39\x35\x39\x5a"
139+
140+
common_name = encode_set(
141+
der.encode_sequence(
142+
der.encode_oid(2, 5, 4, 3), encode_utf8_string(product_name)
143+
)
144+
)
145+
encoded_vendor_id = encode_set(
146+
der.encode_sequence(
147+
der.encode_oid(1, 3, 6, 1, 4, 1, 37244, 2, 1),
148+
encode_utf8_string(f"{vendor_id:04X}"),
149+
)
150+
)
151+
encoded_product_id = encode_set(
152+
der.encode_sequence(
153+
der.encode_oid(1, 3, 6, 1, 4, 1, 37244, 2, 2),
154+
encode_utf8_string(f"{product_id:04X}"),
155+
)
156+
)
157+
subject = der.encode_sequence(common_name, encoded_vendor_id, encoded_product_id)
158+
159+
algorithm_id = b"\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"
160+
161+
public_key = dac_key.verifying_key.to_string(encoding="uncompressed")
162+
public_key_info = der.encode_sequence(
163+
algorithm_id, der.encode_bitstring(public_key, unused=0)
164+
)
165+
166+
basic_constraints = b"\x30\x0c\x06\x03\x55\x1d\x13\x01\x01\xff\x04\x02\x30\x00"
167+
key_usage = b"\x30\x0e\x06\x03\x55\x1d\x0f\x01\x01\xff\x04\x04\x03\x02\x07\x80"
168+
key_id = der.encode_sequence(
169+
der.encode_oid(2, 5, 29, 14),
170+
der.encode_octet_string(
171+
der.encode_octet_string(hashlib.sha1(public_key).digest())
172+
),
173+
)
174+
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"
175+
extensions = der.encode_constructed(
176+
3, der.encode_sequence(basic_constraints, key_usage, key_id, authority_key_id)
177+
)
178+
179+
certificate = der.encode_sequence(
180+
version,
181+
serial_number,
182+
signature_algorithm,
183+
issuer,
184+
validity,
185+
subject,
186+
public_key_info,
187+
extensions,
188+
)
189+
190+
pai_key = ecdsa.keys.SigningKey.from_der(PAI_KEY_DER, hashfunc=hashlib.sha256)
191+
signature = pai_key.sign_deterministic(
192+
certificate,
193+
hashfunc=hashlib.sha256,
194+
sigencode=ecdsa.util.sigencode_der_canonize,
195+
)
196+
197+
dac_cert = der.encode_sequence(
198+
certificate, signature_algorithm, der.encode_bitstring(signature, unused=0)
199+
)
200+
dac_key = dac_key.to_der()
201+
return dac_cert, dac_key
202+
203+
204+
def generate_initial_state(vendor_id, product_id, product_name, random_source):
205+
if vendor_id != 0xFFF4 or product_id != 0x1234:
206+
raise ValueError("Invalid vendor_id or product_id")
207+
208+
cd = generate_certificates(vendor_id=vendor_id, product_id=product_id)
209+
210+
dac_cert, dac_key = generate_dac(vendor_id, product_id, product_name, random_source)
211+
initial_state = {
212+
"discriminator": 3840,
213+
"passcode": 67202583,
214+
"iteration-count": 10000,
215+
"salt": "5uCP0ITHYzI9qBEe6hfU4HfY3y7VopSk0qNvhvznhiQ=",
216+
"verifier": "0xGqxJFBr/ViQt3lv1Yw5F0GcPBAtFFvXB+EcIIjH5cEsjkPZHDQyFWjA6Ide+2gafYnZgIy6gJBgdJOlD8htAZKe0i6nIhT/ADsBWH4CvZcl37n/ofEEECWSEBV4vy/0A==",
217+
"devices": {
218+
"root": {
219+
"0x3e": {
220+
"cd": binascii.b2a_base64(cd, newline=False).decode("utf-8"),
221+
"dac_cert": binascii.b2a_base64(dac_cert, newline=False).decode(
222+
"utf-8"
223+
),
224+
"dac_key": binascii.b2a_base64(dac_key, newline=False).decode(
225+
"utf-8"
226+
),
227+
}
228+
},
229+
},
230+
}
231+
return initial_state
232+
233+
123234
if __name__ == "__main__":
124-
cd = generate_certificates()
125-
pathlib.Path("certification_declaration.der").write_bytes(cd)
126-
for i in range(0, len(cd), 16):
127-
print(f"{i:08x}", cd[i : i + 16].hex(" "))
128-
print(cd.hex(" "))
235+
from circuitmatter.utility import random
236+
237+
initial_state = generate_initial_state(0xFFF4, 0x1234, "CircuitMatter", random)
238+
print(initial_state)

circuitmatter/device_types/utility/root_node.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import ecdsa
33
from ecdsa import der
44
import hashlib
5-
import pathlib
65
import struct
76
import time
87

@@ -35,17 +34,7 @@
3534

3635
from .. import simple_device
3736

38-
TEST_CERTS = pathlib.Path(
39-
"../esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/"
40-
)
41-
TEST_PAI_CERT_DER = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.der"
42-
TEST_PAI_CERT_PEM = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.pem"
43-
TEST_DAC_CERT_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.der"
44-
TEST_DAC_CERT_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.pem"
45-
TEST_DAC_KEY_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.der"
46-
TEST_DAC_KEY_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.pem"
47-
48-
TEST_CD_CERT_DER = pathlib.Path("test_data/certification_declaration.der")
37+
PAI_CERT_DER = b"\x30\x82\x01\xaa\x30\x82\x01\x50\xa0\x03\x02\x01\x02\x02\x08\x5e\xf5\xaf\xd0\x13\x60\xf5\xd4\x30\x0a\x06\x08\x2a\x86\x48\xce\x3d\x04\x03\x02\x30\x1a\x31\x18\x30\x16\x06\x03\x55\x04\x03\x0c\x0f\x4d\x61\x74\x74\x65\x72\x20\x54\x65\x73\x74\x20\x50\x41\x41\x30\x20\x17\x0d\x32\x34\x31\x30\x31\x37\x30\x30\x30\x30\x30\x30\x5a\x18\x0f\x39\x39\x39\x39\x31\x32\x33\x31\x32\x33\x35\x39\x35\x39\x5a\x30\x32\x31\x1a\x30\x18\x06\x03\x55\x04\x03\x0c\x11\x43\x69\x72\x63\x75\x69\x74\x4d\x61\x74\x74\x65\x72\x20\x50\x41\x49\x31\x14\x30\x12\x06\x0a\x2b\x06\x01\x04\x01\x82\xa2\x7c\x02\x01\x0c\x04\x46\x46\x46\x34\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\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\xa3\x66\x30\x64\x30\x12\x06\x03\x55\x1d\x13\x01\x01\xff\x04\x08\x30\x06\x01\x01\xff\x02\x01\x00\x30\x0e\x06\x03\x55\x1d\x0f\x01\x01\xff\x04\x04\x03\x02\x01\x06\x30\x1d\x06\x03\x55\x1d\x0e\x04\x16\x04\x14\x07\xf8\x38\x0a\x5f\x01\x36\xfc\xe2\x36\xbd\x45\xf2\x88\xff\x22\xdc\xa6\xf4\xa7\x30\x1f\x06\x03\x55\x1d\x23\x04\x18\x30\x16\x80\x14\x78\x5c\xe7\x05\xb8\x6b\x8f\x4e\x6f\xc7\x93\xaa\x60\xcb\x43\xea\x69\x68\x82\xd5\x30\x0a\x06\x08\x2a\x86\x48\xce\x3d\x04\x03\x02\x03\x48\x00\x30\x45\x02\x21\x00\x9c\x5f\x59\x83\x5c\xc6\x51\xc2\x5c\x79\x01\x33\x25\x22\x3d\x25\x6b\xe7\x43\x98\xbc\x03\x83\x89\xf4\x55\x6d\xf7\xf7\x4a\x8a\x34\x02\x20\x5c\x14\x17\x4c\xc3\x23\x07\xff\x42\x1c\x4f\x8b\x0b\x63\xb9\x62\x52\x58\xa2\x96\xe0\x31\xfd\xce\x51\xa2\x7a\x08\x49\x2b\xc0\x38"
4938

5039

5140
class _GeneralCommissioningCluster(GeneralCommissioningCluster):
@@ -111,9 +100,7 @@ def __init__(self, group_key_manager, random_source, mdns_server, port):
111100

112101
self.group_key_manager = group_key_manager
113102

114-
self.dac_key = ecdsa.keys.SigningKey.from_der(
115-
TEST_DAC_KEY_DER.read_bytes(), hashfunc=hashlib.sha256
116-
)
103+
self.dac_key = None
117104

118105
self.new_key_for_update = False
119106
self.pending_root_cert = None
@@ -133,6 +120,10 @@ def __init__(self, group_key_manager, random_source, mdns_server, port):
133120
def restore(self, nonvolatile):
134121
super().restore(nonvolatile)
135122

123+
self.dac_key = ecdsa.keys.SigningKey.from_der(
124+
binascii.a2b_base64(nonvolatile["dac_key"]), hashfunc=hashlib.sha256
125+
)
126+
136127
if "pk" not in nonvolatile:
137128
return
138129

@@ -178,9 +169,9 @@ def certificate_chain_request(
178169
) -> NodeOperationalCredentialsCluster.CertificateChainResponse:
179170
response = NodeOperationalCredentialsCluster.CertificateChainResponse()
180171
if args.CertificateType == CertificateChainTypeEnum.PAI:
181-
response.Certificate = TEST_PAI_CERT_DER.read_bytes()
172+
response.Certificate = PAI_CERT_DER
182173
elif args.CertificateType == CertificateChainTypeEnum.DAC:
183-
response.Certificate = TEST_DAC_CERT_DER.read_bytes()
174+
response.Certificate = binascii.a2b_base64(self._nonvolatile["dac_cert"])
184175
return response
185176

186177
def attestation_request(
@@ -189,7 +180,9 @@ def attestation_request(
189180
args: NodeOperationalCredentialsCluster.AttestationRequest,
190181
) -> NodeOperationalCredentialsCluster.AttestationResponse:
191182
elements = AttestationElements()
192-
elements.certification_declaration = TEST_CD_CERT_DER.read_bytes()
183+
elements.certification_declaration = binascii.a2b_base64(
184+
self._nonvolatile["cd"]
185+
)
193186
elements.attestation_nonce = args.AttestationNonce
194187
elements.timestamp = int(time.time())
195188
elements = elements.encode()

examples/replay.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ class NeoPixel(on_off.OnOffLight):
2222

2323

2424
def run(replay_file=None):
25-
device_state = pathlib.Path("test_data/device_state.json")
26-
replay_device_state = pathlib.Path("test_data/replay_device_state.json")
25+
device_state = pathlib.Path("live-device-state.json")
2726
if replay_file:
2827
replay_lines = []
2928
with open(replay_file, "r") as f:
@@ -34,18 +33,25 @@ def run(replay_file=None):
3433
mdns_server = DummyMDNS()
3534
random_source = ReplayRandom(replay_lines)
3635
# Reset device state to before the captured run
37-
device_state.write_text(pathlib.Path(device_state_fn).read_text())
36+
if device_state_fn == "none":
37+
device_state.unlink(missing_ok=True)
38+
else:
39+
device_state.write_text(pathlib.Path(device_state_fn).read_text())
3840
else:
3941
timestamp = time.strftime("%Y%m%d-%H%M%S")
4042
record_file = open(f"test_data/recorded_packets-{timestamp}.jsonl", "w")
4143
device_state_fn = f"test_data/device_state-{timestamp}.json"
42-
record_file.write(f"{device_state_fn}\n")
44+
replay_device_state = pathlib.Path(device_state_fn)
45+
if device_state.exists():
46+
record_file.write(f"{device_state_fn}\n")
47+
# Save device state before we run so replays can use it.
48+
replay_device_state.write_text(device_state.read_text())
49+
else:
50+
# No starting state.
51+
record_file.write("none\n")
4352
socketpool = RecordingSocketPool(record_file, socket)
4453
mdns_server = Avahi()
4554
random_source = RecordingRandom(record_file, random)
46-
# Save device state before we run so replays can use it.
47-
replay_device_state = pathlib.Path(device_state_fn)
48-
replay_device_state.write_text(device_state.read_text())
4955

5056
matter = cm.CircuitMatter(socketpool, mdns_server, random_source, device_state)
5157
led = NeoPixel("neopixel1")

0 commit comments

Comments
 (0)