mfg_tool.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) 2022 Project CHIP Authors
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. import argparse
  18. import base64
  19. import binascii
  20. import csv
  21. import json
  22. import logging as logger
  23. import os
  24. import random
  25. import shutil
  26. import subprocess
  27. import sys
  28. import cbor2 as cbor
  29. import cryptography.hazmat.backends
  30. import cryptography.x509
  31. import pyqrcode
  32. from intelhex import IntelHex
  33. TOOLS = {
  34. 'spake2p': None,
  35. 'chip-cert': None,
  36. 'chip-tool': None,
  37. }
  38. INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, 55555555,
  39. 66666666, 77777777, 88888888, 99999999, 12345678, 87654321]
  40. FACTORY_DATA_VERSION = 1
  41. SERIAL_NUMBER_LEN = 32
  42. # Lengths for manual pairing codes and qrcode
  43. SHORT_MANUALCODE_LEN = 11
  44. LONG_MANUALCODE_LEN = 21
  45. QRCODE_LEN = 22
  46. ROTATING_DEVICE_ID_UNIQUE_ID_LEN = 16
  47. HEX_PREFIX = "hex:"
  48. DEV_SN_CSV_HDR = "Serial Number,\n"
  49. NVS_MEMORY = dict()
  50. def nvs_memory_append(key, value):
  51. if isinstance(value, str):
  52. NVS_MEMORY[key] = value.encode("utf-8")
  53. else:
  54. NVS_MEMORY[key] = value
  55. def nvs_memory_update(key, value):
  56. if isinstance(value, str):
  57. NVS_MEMORY.update({key: value.encode("utf-8")})
  58. else:
  59. NVS_MEMORY.update({key: value})
  60. def check_tools_exists(args):
  61. if args.spake2_path:
  62. TOOLS['spake2p'] = shutil.which(args.spake2_path)
  63. else:
  64. TOOLS['spake2p'] = shutil.which('spake2p')
  65. if TOOLS['spake2p'] is None:
  66. logger.error('spake2p not found, please specify --spake2-path argument')
  67. sys.exit(1)
  68. # if the certs and keys are not in the generated partitions or the specific dac cert and key are used,
  69. # the chip-cert is not needed.
  70. if args.paa or (args.pai and (args.dac_cert is None and args.dac_key is None)):
  71. if args.chip_cert_path:
  72. TOOLS['chip-cert'] = shutil.which(args.chip_cert_path)
  73. else:
  74. TOOLS['chip-cert'] = shutil.which('chip-cert')
  75. if TOOLS['chip-cert'] is None:
  76. logger.error('chip-cert not found, please specify --chip-cert-path argument')
  77. sys.exit(1)
  78. if args.chip_tool_path:
  79. TOOLS['chip-tool'] = shutil.which(args.chip_tool_path)
  80. else:
  81. TOOLS['chip-tool'] = shutil.which('chip-tool')
  82. if TOOLS['chip-tool'] is None:
  83. logger.error('chip-tool not found, please specify --chip-tool-path argument')
  84. sys.exit(1)
  85. logger.debug('Using following tools:')
  86. logger.debug('spake2p: {}'.format(TOOLS['spake2p']))
  87. logger.debug('chip-cert: {}'.format(TOOLS['chip-cert']))
  88. logger.debug('chip-tool: {}'.format(TOOLS['chip-tool']))
  89. def execute_cmd(cmd):
  90. logger.debug('Executing Command: {}'.format(cmd))
  91. status = subprocess.run(cmd, capture_output=True)
  92. try:
  93. status.check_returncode()
  94. except subprocess.CalledProcessError as e:
  95. if status.stderr:
  96. logger.error('[stderr]: {}'.format(status.stderr.decode('utf-8').strip()))
  97. logger.error('Command failed with error: {}'.format(e))
  98. sys.exit(1)
  99. def check_str_range(s, min_len, max_len, name):
  100. if s and ((len(s) < min_len) or (len(s) > max_len)):
  101. logger.error('%s must be between %d and %d characters', name, min_len, max_len)
  102. sys.exit(1)
  103. def check_int_range(value, min_value, max_value, name):
  104. if value and ((value < min_value) or (value > max_value)):
  105. logger.error('%s is out of range, should be in range [%d, %d]', name, min_value, max_value)
  106. sys.exit(1)
  107. def vid_pid_str(vid, pid):
  108. return '_'.join([hex(vid)[2:], hex(pid)[2:]])
  109. def read_der_file(path: str):
  110. logger.debug("Reading der file {}...", path)
  111. try:
  112. with open(path, 'rb') as f:
  113. data = f.read()
  114. return data
  115. except IOError as e:
  116. logger.error(e)
  117. raise e
  118. def read_key_bin_file(path: str):
  119. try:
  120. with open(path, 'rb') as file:
  121. key_data = file.read()
  122. return key_data
  123. except IOError or ValueError:
  124. return None
  125. def setup_out_dir(out_dir_top, args, serial: str):
  126. out_dir = os.sep.join([out_dir_top, vid_pid_str(args.vendor_id, args.product_id)])
  127. if args.in_tree:
  128. out_dir = out_dir_top
  129. os.makedirs(out_dir, exist_ok=True)
  130. dirs = {
  131. 'output': os.sep.join([out_dir, serial]),
  132. 'internal': os.sep.join([out_dir, serial, 'internal']),
  133. }
  134. if args.in_tree:
  135. dirs['output'] = out_dir
  136. dirs['internal'] = os.sep.join([out_dir, 'internal'])
  137. os.makedirs(dirs['output'], exist_ok=True)
  138. os.makedirs(dirs['internal'], exist_ok=True)
  139. return dirs
  140. def convert_x509_cert_from_pem_to_der(pem_file, out_der_file):
  141. with open(pem_file, 'rb') as f:
  142. pem_data = f.read()
  143. pem_cert = cryptography.x509.load_pem_x509_certificate(pem_data, cryptography.hazmat.backends.default_backend())
  144. der_cert = pem_cert.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.DER)
  145. with open(out_der_file, 'wb') as f:
  146. f.write(der_cert)
  147. def generate_passcode(args, out_dirs):
  148. salt_len_max = 32
  149. cmd = [
  150. TOOLS['spake2p'], 'gen-verifier',
  151. '--iteration-count', str(args.spake2_it),
  152. '--salt-len', str(salt_len_max),
  153. '--out', os.sep.join([out_dirs['output'], 'pin.csv'])
  154. ]
  155. # If passcode is provided, use it
  156. if (args.passcode):
  157. cmd.extend(['--pin-code', str(args.passcode)])
  158. execute_cmd(cmd)
  159. def generate_discriminator(args, out_dirs):
  160. # If discriminator is provided, use it
  161. if args.discriminator:
  162. disc = args.discriminator
  163. else:
  164. disc = random.randint(0x0000, 0x0FFF)
  165. # Append discriminator to each line of the passcode file
  166. with open(os.sep.join([out_dirs['output'], 'pin.csv']), 'r') as fd:
  167. lines = fd.readlines()
  168. lines[0] = ','.join([lines[0].strip(), 'Discriminator'])
  169. for i in range(1, len(lines)):
  170. lines[i] = ','.join([lines[i].strip(), str(disc)])
  171. with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'w') as fd:
  172. fd.write('\n'.join(lines) + '\n')
  173. os.remove(os.sep.join([out_dirs['output'], 'pin.csv']))
  174. def generate_pai_certs(args, ca_key, ca_cert, out_key, out_cert):
  175. cmd = [
  176. TOOLS['chip-cert'], 'gen-att-cert',
  177. '--type', 'i',
  178. '--subject-cn', '"{} PAI {}"'.format(args.cn_prefix, '00'),
  179. '--out-key', out_key,
  180. '--out', out_cert,
  181. ]
  182. if args.lifetime:
  183. cmd.extend(['--lifetime', str(args.lifetime)])
  184. if args.valid_from:
  185. cmd.extend(['--valid-from', str(args.valid_from)])
  186. cmd.extend([
  187. '--subject-vid', hex(args.vendor_id)[2:],
  188. '--subject-pid', hex(args.product_id)[2:],
  189. '--ca-key', ca_key,
  190. '--ca-cert', ca_cert,
  191. ])
  192. execute_cmd(cmd)
  193. logger.info('Generated PAI certificate: {}'.format(out_cert))
  194. logger.info('Generated PAI private key: {}'.format(out_key))
  195. def setup_root_certificates(args, dirs):
  196. pai_cert = {
  197. 'cert_pem': None,
  198. 'cert_der': None,
  199. 'key_pem': None,
  200. }
  201. # If PAA is passed as input, then generate PAI certificate
  202. if args.paa:
  203. # output file names
  204. pai_cert['cert_pem'] = os.sep.join([dirs['internal'], 'pai_cert.pem'])
  205. pai_cert['cert_der'] = os.sep.join([dirs['internal'], 'pai_cert.der'])
  206. pai_cert['key_pem'] = os.sep.join([dirs['internal'], 'pai_key.pem'])
  207. generate_pai_certs(args, args.key, args.cert, pai_cert['key_pem'], pai_cert['cert_pem'])
  208. convert_x509_cert_from_pem_to_der(pai_cert['cert_pem'], pai_cert['cert_der'])
  209. logger.info('Generated PAI certificate in DER format: {}'.format(pai_cert['cert_der']))
  210. # If PAI is passed as input, generate DACs
  211. elif args.pai:
  212. pai_cert['cert_pem'] = args.cert
  213. pai_cert['key_pem'] = args.key
  214. pai_cert['cert_der'] = os.sep.join([dirs['internal'], 'pai_cert.der'])
  215. convert_x509_cert_from_pem_to_der(pai_cert['cert_pem'], pai_cert['cert_der'])
  216. logger.info('Generated PAI certificate in DER format: {}'.format(pai_cert['cert_der']))
  217. return pai_cert
  218. # Generate the Public and Private key pair binaries
  219. def generate_keypair_bin(pem_file, out_privkey_bin, out_pubkey_bin):
  220. with open(pem_file, 'rb') as f:
  221. pem_data = f.read()
  222. key_pem = cryptography.hazmat.primitives.serialization.load_pem_private_key(pem_data, None)
  223. private_number_val = key_pem.private_numbers().private_value
  224. public_number_x = key_pem.public_key().public_numbers().x
  225. public_number_y = key_pem.public_key().public_numbers().y
  226. public_key_first_byte = 0x04
  227. with open(out_privkey_bin, 'wb') as f:
  228. f.write(private_number_val.to_bytes(32, byteorder='big'))
  229. with open(out_pubkey_bin, 'wb') as f:
  230. f.write(public_key_first_byte.to_bytes(1, byteorder='big'))
  231. f.write(public_number_x.to_bytes(32, byteorder='big'))
  232. f.write(public_number_y.to_bytes(32, byteorder='big'))
  233. def generate_dac_cert(iteration, args, out_dirs, discriminator, passcode, ca_key, ca_cert):
  234. out_key_pem = os.sep.join([out_dirs['internal'], 'DAC_key.pem'])
  235. out_cert_pem = out_key_pem.replace('key.pem', 'cert.pem')
  236. out_cert_der = out_key_pem.replace('key.pem', 'cert.der')
  237. out_private_key_bin = out_key_pem.replace('key.pem', 'private_key.bin')
  238. out_public_key_bin = out_key_pem.replace('key.pem', 'public_key.bin')
  239. cmd = [
  240. TOOLS['chip-cert'], 'gen-att-cert',
  241. '--type', 'd',
  242. '--subject-cn', '"{} DAC {}"'.format(args.cn_prefix, iteration),
  243. '--out-key', out_key_pem,
  244. '--out', out_cert_pem,
  245. ]
  246. if args.lifetime:
  247. cmd.extend(['--lifetime', str(args.lifetime)])
  248. if args.valid_from:
  249. cmd.extend(['--valid-from', str(args.valid_from)])
  250. cmd.extend(['--subject-vid', hex(args.vendor_id)[2:],
  251. '--subject-pid', hex(args.product_id)[2:],
  252. '--ca-key', ca_key,
  253. '--ca-cert', ca_cert,
  254. ])
  255. execute_cmd(cmd)
  256. logger.info('Generated DAC certificate: {}'.format(out_cert_pem))
  257. logger.info('Generated DAC private key: {}'.format(out_key_pem))
  258. convert_x509_cert_from_pem_to_der(out_cert_pem, out_cert_der)
  259. logger.info('Generated DAC certificate in DER format: {}'.format(out_cert_der))
  260. generate_keypair_bin(out_key_pem, out_private_key_bin, out_public_key_bin)
  261. logger.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin))
  262. logger.info('Generated DAC public key in binary format: {}'.format(out_public_key_bin))
  263. return out_cert_der, out_private_key_bin, out_public_key_bin
  264. def use_dac_cert_from_args(args, out_dirs):
  265. logger.info('Using DAC from command line arguments...')
  266. logger.info('DAC Certificate: {}'.format(args.dac_cert))
  267. logger.info('DAC Private Key: {}'.format(args.dac_key))
  268. # There should be only one UUID in the UUIDs list if DAC is specified
  269. out_cert_der = os.sep.join([out_dirs['internal'], 'DAC_cert.der'])
  270. out_private_key_bin = out_cert_der.replace('cert.der', 'private_key.bin')
  271. out_public_key_bin = out_cert_der.replace('cert.der', 'public_key.bin')
  272. convert_x509_cert_from_pem_to_der(args.dac_cert, out_cert_der)
  273. logger.info('Generated DAC certificate in DER format: {}'.format(out_cert_der))
  274. generate_keypair_bin(args.dac_key, out_private_key_bin, out_public_key_bin)
  275. logger.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin))
  276. logger.info('Generated DAC public key in binary format: {}'.format(out_public_key_bin))
  277. return out_cert_der, out_private_key_bin, out_public_key_bin
  278. def get_manualcode_args(vid, pid, flow, discriminator, passcode):
  279. payload_args = list()
  280. payload_args.append('--discriminator')
  281. payload_args.append(str(discriminator))
  282. payload_args.append('--setup-pin-code')
  283. payload_args.append(str(passcode))
  284. payload_args.append('--version')
  285. payload_args.append('0')
  286. payload_args.append('--vendor-id')
  287. payload_args.append(str(vid))
  288. payload_args.append('--product-id')
  289. payload_args.append(str(pid))
  290. payload_args.append('--commissioning-mode')
  291. payload_args.append(str(flow))
  292. return payload_args
  293. def get_qrcode_args(vid, pid, flow, discriminator, passcode, disc_mode):
  294. payload_args = get_manualcode_args(vid, pid, flow, discriminator, passcode)
  295. payload_args.append('--rendezvous')
  296. payload_args.append(str(1 << disc_mode))
  297. return payload_args
  298. def get_chip_qrcode(chip_tool, vid, pid, flow, discriminator, passcode, disc_mode):
  299. payload_args = get_qrcode_args(vid, pid, flow, discriminator, passcode, disc_mode)
  300. cmd_args = [chip_tool, 'payload', 'generate-qrcode']
  301. cmd_args.extend(payload_args)
  302. data = subprocess.check_output(cmd_args)
  303. # Command output is as below:
  304. # \x1b[0;32m[1655386003372] [23483:7823617] CHIP: [TOO] QR Code: MT:Y.K90-WB010E7648G00\x1b[0m
  305. return data.decode('utf-8').split('QR Code: ')[1][:QRCODE_LEN]
  306. def get_chip_manualcode(chip_tool, vid, pid, flow, discriminator, passcode):
  307. payload_args = get_manualcode_args(vid, pid, flow, discriminator, passcode)
  308. cmd_args = [chip_tool, 'payload', 'generate-manualcode']
  309. cmd_args.extend(payload_args)
  310. data = subprocess.check_output(cmd_args)
  311. # Command output is as below:
  312. # \x1b[0;32m[1655386909774] [24424:7837894] CHIP: [TOO] Manual Code: 749721123365521327689\x1b[0m\n
  313. # OR
  314. # \x1b[0;32m[1655386926028] [24458:7838229] CHIP: [TOO] Manual Code: 34972112338\x1b[0m\n
  315. # Length of manual code depends on the commissioning flow:
  316. # For standard commissioning flow it is 11 digits
  317. # For User-intent and custom commissioning flow it is 21 digits
  318. manual_code_len = LONG_MANUALCODE_LEN if flow else SHORT_MANUALCODE_LEN
  319. return data.decode('utf-8').split('Manual Code: ')[1][:manual_code_len]
  320. def generate_onboarding_data(args, out_dirs, discriminator, passcode):
  321. chip_manualcode = get_chip_manualcode(TOOLS['chip-tool'], args.vendor_id, args.product_id,
  322. args.commissioning_flow, discriminator, passcode)
  323. chip_qrcode = get_chip_qrcode(TOOLS['chip-tool'], args.vendor_id, args.product_id,
  324. args.commissioning_flow, discriminator, passcode, args.discovery_mode)
  325. logger.info('Generated QR code: ' + chip_qrcode)
  326. logger.info('Generated manual code: ' + chip_manualcode)
  327. csv_data = 'qrcode,manualcode,discriminator,passcode\n'
  328. csv_data += chip_qrcode + ',' + chip_manualcode + ',' + str(discriminator) + ',' + str(passcode) + '\n'
  329. onboarding_data_file = os.sep.join([out_dirs['output'], 'onb_codes.csv'])
  330. with open(onboarding_data_file, 'w') as f:
  331. f.write(csv_data)
  332. # Create QR code image as mentioned in the spec
  333. qrcode_file = os.sep.join([out_dirs['output'], 'qrcode.png'])
  334. chip_qr = pyqrcode.create(chip_qrcode, version=2, error='M')
  335. chip_qr.png(qrcode_file, scale=6)
  336. logger.info('Generated onboarding data and QR Code')
  337. # This function generates the DACs, picks the commissionable data from the already present csv file,
  338. # and generates the onboarding payloads, and writes everything to the master csv
  339. def write_device_unique_data(args, out_dirs, pai_cert):
  340. with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'r') as csvf:
  341. pin_disc_dict = csv.DictReader(csvf)
  342. row = pin_disc_dict.__next__()
  343. nvs_memory_append('discriminator', int(row['Discriminator']))
  344. nvs_memory_append('spake2_it', int(row['Iteration Count']))
  345. nvs_memory_append('spake2_salt', base64.b64decode(row['Salt']))
  346. nvs_memory_append('spake2_verifier', base64.b64decode(row['Verifier']))
  347. nvs_memory_append('passcode', int(row['PIN Code']))
  348. if args.paa or args.pai:
  349. if args.dac_key is not None and args.dac_cert is not None:
  350. dacs = use_dac_cert_from_args(args, out_dirs)
  351. else:
  352. dacs = generate_dac_cert(int(row['Index']), args, out_dirs, int(row['Discriminator']),
  353. int(row['PIN Code']), pai_cert['key_pem'], pai_cert['cert_pem'])
  354. nvs_memory_append('dac_cert', read_der_file(dacs[0]))
  355. nvs_memory_append('dac_key', read_key_bin_file(dacs[1]))
  356. nvs_memory_append('pai_cert', read_der_file(pai_cert['cert_der']))
  357. nvs_memory_append('cert_dclrn', read_der_file(args.cert_dclrn))
  358. if (args.enable_rotating_device_id is True) and (args.rd_id_uid is None):
  359. nvs_memory_update('rd_uid', os.urandom(ROTATING_DEVICE_ID_UNIQUE_ID_LEN))
  360. # Generate onboarding data
  361. generate_onboarding_data(args, out_dirs, int(row['Discriminator']), int(row['PIN Code']))
  362. return dacs
  363. def generate_partition(args, out_dirs):
  364. logger.info('Generating partition image: offset: 0x{:X} size: 0x{:X}'.format(args.offset, args.size))
  365. cbor_data = cbor.dumps(NVS_MEMORY)
  366. # Create hex file
  367. if len(cbor_data) > args.size:
  368. raise ValueError("generated CBOR file exceeds declared maximum partition size! {} > {}".format(len(cbor_data), args.size))
  369. ih = IntelHex()
  370. ih.putsz(args.offset, cbor_data)
  371. ih.write_hex_file(os.sep.join([out_dirs['output'], 'factory_data.hex']), True)
  372. ih.tobinfile(os.sep.join([out_dirs['output'], 'factory_data.bin']))
  373. def generate_json_summary(args, out_dirs, pai_certs, dacs_cert, serial_num: str):
  374. json_dict = dict()
  375. json_dict['serial_num'] = serial_num
  376. for key, nvs_value in NVS_MEMORY.items():
  377. if (not isinstance(nvs_value, bytes) and not isinstance(nvs_value, bytearray)):
  378. json_dict[key] = nvs_value
  379. with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'r') as csvf:
  380. pin_disc_dict = csv.DictReader(csvf)
  381. row = pin_disc_dict.__next__()
  382. json_dict['passcode'] = row['PIN Code']
  383. json_dict['spake2_salt'] = row['Salt']
  384. json_dict['spake2_verifier'] = row['Verifier']
  385. with open(os.sep.join([out_dirs['output'], 'onb_codes.csv']), 'r') as csvf:
  386. pin_disc_dict = csv.DictReader(csvf)
  387. row = pin_disc_dict.__next__()
  388. for key, value in row.items():
  389. json_dict[key] = value
  390. for key, value in pai_certs.items():
  391. json_dict[key] = value
  392. if dacs_cert is not None:
  393. json_dict['dac_cert'] = dacs_cert[0]
  394. json_dict['dac_priv_key'] = dacs_cert[1]
  395. json_dict['dac_pub_key'] = dacs_cert[2]
  396. json_dict['cert_dclrn'] = args.cert_dclrn
  397. # Format vid & pid as hex
  398. json_dict['vendor_id'] = hex(json_dict['vendor_id'])
  399. json_dict['product_id'] = hex(json_dict['product_id'])
  400. with open(os.sep.join([out_dirs['output'], 'summary.json']), 'w') as json_file:
  401. json.dump(json_dict, json_file, indent=4)
  402. def add_additional_kv(args, serial_num):
  403. # Device instance information
  404. if args.vendor_id is not None:
  405. nvs_memory_append('vendor_id', args.vendor_id)
  406. if args.vendor_name is not None:
  407. nvs_memory_append('vendor_name', args.vendor_name)
  408. if args.product_id is not None:
  409. nvs_memory_append('product_id', args.product_id)
  410. if args.product_name is not None:
  411. nvs_memory_append('product_name', args.product_name)
  412. if args.hw_ver is not None:
  413. nvs_memory_append('hw_ver', args.hw_ver)
  414. if args.hw_ver_str is not None:
  415. nvs_memory_append('hw_ver_str', args.hw_ver_str)
  416. if args.mfg_date is not None:
  417. nvs_memory_append('date', args.mfg_date)
  418. if args.enable_rotating_device_id:
  419. nvs_memory_append('rd_uid', args.rd_id_uid)
  420. # Add the serial-num
  421. nvs_memory_append('sn', serial_num)
  422. nvs_memory_append('version', FACTORY_DATA_VERSION)
  423. if args.enable_key:
  424. nvs_memory_append('enable_key', args.enable_key)
  425. # Keys from basic clusters
  426. if args.product_label is not None:
  427. nvs_memory_append('product_label', args.product_label)
  428. if args.product_url is not None:
  429. nvs_memory_append('product_url', args.product_url)
  430. if args.part_number is not None:
  431. nvs_memory_append('part_number', args.part_number)
  432. def get_and_validate_args():
  433. def allow_any_int(i): return int(i, 0)
  434. def base64_str(s): return base64.b64decode(s)
  435. parser = argparse.ArgumentParser(description='Manufacuring partition generator tool',
  436. formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=50))
  437. # General options
  438. general_args = parser.add_argument_group('General options')
  439. general_args.add_argument('-n', '--count', type=allow_any_int, default=1,
  440. help='The number of manufacturing partition binaries to generate. Default is 1.')
  441. general_args.add_argument("--output", type=str, required=False, default="out",
  442. help="[string] Output path where generated data will be stored.")
  443. general_args.add_argument("--spake2-path", type=str, required=False,
  444. help="[string] Provide Spake2+ tool path")
  445. general_args.add_argument("--chip-tool-path", type=str, required=False,
  446. help="[string] Provide chip-tool path")
  447. general_args.add_argument("--chip-cert-path", type=str, required=False,
  448. help="[string] Provide chip-cert path")
  449. general_args.add_argument("--overwrite", action="store_true", default=False,
  450. help="If output directory exist this argument allows to generate new factory data and overwrite it.")
  451. general_args.add_argument("--in-tree", action="store_true", default=False,
  452. help="Use it only when building factory data from Matter source code.")
  453. general_args.add_argument("--enable-key", type=str,
  454. help="[hex string] [128-bit hex-encoded] The Enable Key is a 128-bit value that triggers manufacturer-specific action while invoking the TestEventTrigger Command."
  455. "This value is used during Certification Tests, and should not be present on production devices.")
  456. # Commissioning options
  457. commissioning_args = parser.add_argument_group('Commisioning options')
  458. commissioning_args.add_argument('--passcode', type=allow_any_int,
  459. help='The passcode for pairing. Randomly generated if not specified.')
  460. commissioning_args.add_argument("--spake2-it", type=allow_any_int, default=1000,
  461. help="[int] Provide Spake2+ iteration count.")
  462. commissioning_args.add_argument('--discriminator', type=allow_any_int,
  463. help='The discriminator for pairing. Randomly generated if not specified.')
  464. commissioning_args.add_argument('-cf', '--commissioning-flow', type=allow_any_int, default=0,
  465. help='Device commissioning flow, 0:Standard, 1:User-Intent, 2:Custom. \
  466. Default is 0.', choices=[0, 1, 2])
  467. commissioning_args.add_argument('-dm', '--discovery-mode', type=allow_any_int, default=1,
  468. help='Commissionable device discovery networking technology. \
  469. 0:WiFi-SoftAP, 1:BLE, 2:On-network. Default is BLE.', choices=[0, 1, 2])
  470. # Device insrance information
  471. dev_inst_args = parser.add_argument_group('Device instance information options')
  472. dev_inst_args.add_argument('-v', '--vendor-id', type=allow_any_int, required=False, help='Vendor id')
  473. dev_inst_args.add_argument('--vendor-name', type=str, required=False, help='Vendor name')
  474. dev_inst_args.add_argument('-p', '--product-id', type=allow_any_int, required=False, help='Product id')
  475. dev_inst_args.add_argument('--product-name', type=str, required=False, help='Product name')
  476. dev_inst_args.add_argument('--hw-ver', type=allow_any_int, required=False, help='Hardware version')
  477. dev_inst_args.add_argument('--hw-ver-str', type=str, required=False, help='Hardware version string')
  478. dev_inst_args.add_argument('--mfg-date', type=str, required=False, help='Manufacturing date in format YYYY-MM-DD')
  479. dev_inst_args.add_argument('--serial-num', type=str, required=False, help='Serial number in hex format')
  480. dev_inst_args.add_argument('--enable-rotating-device-id', action='store_true',
  481. help='Enable Rotating device id in the generated binaries')
  482. dev_inst_args.add_argument('--rd-id-uid', type=str, required=False,
  483. help='128-bit unique identifier for generating rotating device identifier, provide 32-byte hex string, e.g. "1234567890abcdef1234567890abcdef"')
  484. dac_args = parser.add_argument_group('Device attestation credential options')
  485. # If DAC is present then PAI key is not required, so it is marked as not required here
  486. # but, if DAC is not present then PAI key is required and that case is validated in validate_args()
  487. dac_args.add_argument('-c', '--cert', type=str, required=False, help='The input certificate file in PEM format.')
  488. dac_args.add_argument('-k', '--key', type=str, required=False, help='The input key file in PEM format.')
  489. dac_args.add_argument('-cd', '--cert-dclrn', type=str, required=True, help='The certificate declaration file in DER format.')
  490. dac_args.add_argument('--dac-cert', type=str, help='The input DAC certificate file in PEM format.')
  491. dac_args.add_argument('--dac-key', type=str, help='The input DAC private key file in PEM format.')
  492. dac_args.add_argument('-cn', '--cn-prefix', type=str, default='Telink',
  493. help='The common name prefix of the subject of the generated certificate.')
  494. dac_args.add_argument('-lt', '--lifetime', default=4294967295, type=allow_any_int,
  495. help='Lifetime of the generated certificate. Default is 4294967295 if not specified, \
  496. this indicate that certificate does not have well defined expiration date.')
  497. dac_args.add_argument('-vf', '--valid-from', type=str,
  498. help='The start date for the certificate validity period in format <YYYY>-<MM>-<DD> [ <HH>:<MM>:<SS> ]. \
  499. Default is current date.')
  500. input_cert_group = dac_args.add_mutually_exclusive_group(required=False)
  501. input_cert_group.add_argument('--paa', action='store_true', help='Use input certificate as PAA certificate.')
  502. input_cert_group.add_argument('--pai', action='store_true', help='Use input certificate as PAI certificate.')
  503. basic_args = parser.add_argument_group('Few more Basic clusters options')
  504. basic_args.add_argument('--product-label', type=str, required=False, help='Product label')
  505. basic_args.add_argument('--product-url', type=str, required=False, help='Product URL')
  506. basic_args.add_argument('--part_number', type=str, required=False, help='Provide human-readable product number')
  507. part_gen_args = parser.add_argument_group('Partition generator options')
  508. part_gen_args.add_argument('--offset', type=allow_any_int, default=0x107000,
  509. help='Partition offset - an address in devices NVM memory, where factory data will be stored')
  510. part_gen_args.add_argument('--size', type=allow_any_int, default=0x1000,
  511. help='The maximum partition size')
  512. args = parser.parse_args()
  513. # Validate in-tree parameter
  514. if args.count > 1 and args.in_tree:
  515. logger.error('Option --in-tree can not be use together with --count > 1')
  516. sys.exit(1)
  517. # Validate discriminator and passcode
  518. check_int_range(args.discriminator, 0x0000, 0x0FFF, 'Discriminator')
  519. if args.passcode is not None:
  520. if ((args.passcode < 0x0000001 and args.passcode > 0x5F5E0FE) or (args.passcode in INVALID_PASSCODES)):
  521. logger.error('Invalid passcode' + str(args.passcode))
  522. sys.exit(1)
  523. # Validate the device instance information
  524. check_int_range(args.product_id, 0x0000, 0xFFFF, 'Product id')
  525. check_int_range(args.vendor_id, 0x0000, 0xFFFF, 'Vendor id')
  526. check_int_range(args.hw_ver, 0x0000, 0xFFFF, 'Hardware version')
  527. check_int_range(args.spake2_it, 1, 10000, 'Spake2+ iteration count')
  528. check_str_range(args.serial_num, 1, SERIAL_NUMBER_LEN, 'Serial number')
  529. check_str_range(args.vendor_name, 1, 32, 'Vendor name')
  530. check_str_range(args.product_name, 1, 32, 'Product name')
  531. check_str_range(args.hw_ver_str, 1, 64, 'Hardware version string')
  532. check_str_range(args.mfg_date, 8, 16, 'Manufacturing date')
  533. check_str_range(args.rd_id_uid, 16, 32, 'Rotating device Unique id')
  534. # Validates the attestation related arguments
  535. # DAC key and DAC cert both should be present or none
  536. if (args.dac_key is not None) != (args.dac_cert is not None):
  537. logger.error("dac_key and dac_cert should be both present or none")
  538. sys.exit(1)
  539. else:
  540. # Make sure PAI certificate is present if DAC is present
  541. if (args.dac_key is not None) and (args.pai is False):
  542. logger.error('Please provide PAI certificate along with DAC certificate and DAC key')
  543. sys.exit(1)
  544. # Validate the input certificate type, if DAC is not present
  545. if args.dac_key is None and args.dac_cert is None:
  546. if args.paa:
  547. logger.info('Input Root certificate type PAA')
  548. elif args.pai:
  549. logger.info('Input Root certificate type PAI')
  550. else:
  551. logger.info('Do not include the device attestation certificates and keys in partition binaries')
  552. # Check if Key and certificate are present
  553. if (args.paa or args.pai) and (args.key is None or args.cert is None):
  554. logger.error('CA key and certificate are required to generate DAC key and certificate')
  555. sys.exit(1)
  556. check_str_range(args.product_label, 1, 64, 'Product Label')
  557. check_str_range(args.product_url, 1, 256, 'Product URL')
  558. check_str_range(args.part_number, 1, 32, 'Part Number')
  559. return args
  560. def main():
  561. logger.basicConfig(format='[%(asctime)s] [%(levelname)7s] - %(message)s', level=logger.INFO)
  562. args = get_and_validate_args()
  563. check_tools_exists(args)
  564. if os.path.exists(args.output):
  565. if args.overwrite:
  566. logger.info("Output directory already exists. All data will be overwritten.")
  567. shutil.rmtree(args.output)
  568. else:
  569. logger.error("Output directory exists! Please use different or remove existing.")
  570. exit(1)
  571. # If serial number is not passed, then generate one
  572. if args.serial_num is None:
  573. serial_num_int = int(binascii.b2a_hex(os.urandom(SERIAL_NUMBER_LEN)), 16)
  574. logger.info("Serial number not provided. Using generated one: {}".format(hex(serial_num_int)))
  575. else:
  576. serial_num_int = int(args.serial_num, 16)
  577. out_dir_top = os.path.realpath(args.output)
  578. os.makedirs(out_dir_top, exist_ok=True)
  579. dev_sn_file = open(os.sep.join([out_dir_top, "device_sn.csv"]), "w")
  580. dev_sn_file.write(DEV_SN_CSV_HDR)
  581. for i in range(args.count):
  582. pai_cert = {}
  583. serial_num_str = format(serial_num_int + i, 'x')
  584. logger.info("Generating for {}".format(serial_num_str))
  585. dev_sn_file.write(serial_num_str + '\n')
  586. out_dirs = setup_out_dir(out_dir_top, args, serial_num_str)
  587. add_additional_kv(args, serial_num_str)
  588. generate_passcode(args, out_dirs)
  589. generate_discriminator(args, out_dirs)
  590. if args.paa or args.pai:
  591. pai_cert = setup_root_certificates(args, out_dirs)
  592. dacs_cert = write_device_unique_data(args, out_dirs, pai_cert)
  593. generate_partition(args, out_dirs)
  594. generate_json_summary(args, out_dirs, pai_cert, dacs_cert, serial_num_str)
  595. dev_sn_file.close()
  596. if __name__ == "__main__":
  597. main()