Skip to content

Commit

Permalink
[nrfconnect] Added auto-generation of certificates at building time. (#…
Browse files Browse the repository at this point in the history
…20603) (#20725)

Added a new feature to the factory data support for the nRF Connect platform.
Users can use -DCONFIG_CHIP_FACTORY_DATA_GENERATE_CERTS to generate CD, DAC,
and PAI certificates during the building process.

This solution uses the chip-cert executable to generate new certificates and
puts ready certs into a factory data set.

Co-authored-by: Arkadiusz Bałys <arkadiusz.balys@nordicsemi.no>
  • Loading branch information
woody-apple and ArekBalysNordic authored Jul 14, 2022
1 parent 8634373 commit 89d2a6a
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 24 deletions.
2 changes: 1 addition & 1 deletion config/nrfconnect/chip-module/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ config CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE
factory data.

# Use default certificates without generating or providing them
config CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS
config CHIP_FACTORY_DATA_USE_DEFAULT_CERTS
bool "Use default certificates located in Matter repository"
default y
help
Expand Down
20 changes: 7 additions & 13 deletions config/nrfconnect/chip-module/generate_factory_data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ if(NOT CONFIG_CHIP_DEVICE_GENERATE_ROTATING_DEVICE_UID)
else()
string(APPEND script_args "--rd_uid \"${CONFIG_CHIP_DEVICE_ROTATING_DEVICE_UID}\"\n")
endif()
else()
string(APPEND script_args "--generate_rd_uid\n")
endif()

# for development purpose user can use default certs instead of generating or providing them
if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS)
if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS)
# convert decimal PID to its hexadecimal representation to find out certification files in repository
math(EXPR LOCAL_PID "${CONFIG_CHIP_DEVICE_PRODUCT_ID}" OUTPUT_FORMAT HEXADECIMAL)
string(SUBSTRING ${LOCAL_PID} 2 -1 raw_pid)
Expand All @@ -68,14 +70,9 @@ if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS)
string(APPEND script_args "--dac_key \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid}-Key.der\"\n")
string(APPEND script_args "--pai_cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-PAI-noPID-Cert.der\"\n")
else()
# try to generate a new DAC and PAI certs and DAC key
# request script to generate a new certificates
# by adding an argument to script_args
find_program(chip-cert NAMES chip-cert)
if(NOT chip-cert)
message(FATAL_ERROR "Could not find chip_cert_path executable in PATH")
endif()
string(APPEND script_args "--chip_cert_path ${chip-cert}\n")
find_program(chip_cert_exe NAMES chip-cert REQUIRED)
string(APPEND script_args "--gen_cd\n")
string(APPEND script_args "--chip_cert_path ${chip_cert_exe}\n")
endif()

# add Password-Authenticated Key Exchange parameters
Expand All @@ -88,10 +85,7 @@ string(APPEND script_args "--passcode ${CONFIG_CHIP_DEVICE_SPAKE2_PASSCODE}\n")
if(CONFIG_CHIP_FACTORY_DATA_GENERATE_SPAKE2_VERIFIER)
# request script to generate a new spake2_verifier
# by adding an argument to script_args
find_program(spake_exe NAMES spake2p)
if(NOT spake_exe)
message(FATAL_ERROR "Could not find spake2p executable in PATH")
endif()
find_program(spake_exe NAMES spake2p REQUIRED)
string(APPEND script_args "--spake2p_path ${spake_exe}\n")
else()
# Spake2 verifier should be provided using kConfig
Expand Down
49 changes: 48 additions & 1 deletion docs/guides/nrfconnect_factory_data_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,29 @@ $ python scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py -h
--passcode <pass_code> --spake2p_path <path to spake2p executable>
```

> Note: To generate new SPAKE2+ verifier you need `spake2p` executable. See
> the note at the end of this section to learn how to get it.
- Manual:

```
--spake2_verifier <verifier>
```

d. Add paths to `.der` files that contain PAI and DAC certificates and the
DAC private key (replace the respective variables with the file names):
DAC private key (replace the respective variables with the file names) using
one of the following methods:

- Automatic:

```
--chip_cert_path <path to chip-cert executable>
```

> Note: To generate new certificates, you need the `chip-cert` executable.
> See the note at the end of this section to learn how to get it.
- Manual:

```
--dac_cert <path to DAC certificate>.der --dac_key <path to DAC key>.der --pai_cert <path to PAI certificate>.der
Expand Down Expand Up @@ -328,6 +343,17 @@ If the script finishes successfully, go to the location you provided with the
> 3. Add the `connectedhomeip/src/tools/spake2p/out/spake2p` path as an
> argument of `--spake2p_path` for the Python script.
> Note: Generating new certificates is optional if default vendor and product
> IDs are used and requires providing a path to the `chip-cert` executable. To
> get it, complete the following steps:
>
> 1. Navigate to the `connectedhomeip` root directory.
> 2. In a terminal, run the command:
> `cd src/tools/chip-cert && gn gen out && ninja -C out chip-cert` to build
> the executable.
> 3. Add the `connectedhomeip/src/tools/chip-cert/out/chip-cert` path as an
> argument of `--chip_cert_path` for the Python script.
> Note: By default, overwriting the existing JSON file is disabled. This means
> that you cannot create a new JSON file with the same name in the exact
> location as an existing file. To allow overwriting, add the `--overwrite`
Expand Down Expand Up @@ -625,6 +651,27 @@ $ west build -b nrf52840dk_nrf52840 -- \
-DCONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE=y
```

You can also build an example with auto-generation of new CD, DAC and PAI
certificates. The newly generated certificates will be added to factory data set
automatically. To generate new certificates disable using default certificates
by building an example with the additional option
`-DCHIP_FACTORY_DATA_USE_DEFAULT_CERTS=n`:

```
$ west build -b nrf52840dk_nrf52840 -- \
-DCONFIG_CHIP_FACTORY_DATA=y \
-DCONFIG_CHIP_FACTORY_DATA_BUILD=y \
-DCONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE=y \
-DCONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS=n
```

> Note: To generate new certificates using the nRF Connect platform build
> system, you need the `chip-cert` executable in your system variable PATH. To
> learn how to get `chip-cert`, go to the note at the end of
> [creating the factory data partition with the second script](#creating-the-factory-data-partition-with-the-second-script)
> section, and then add the newly built executable to the system variable PATH.
> The Cmake build system will find this executable automatically.
After that, use the following command from the example's directory to write
firmware and newly generated factory data at the same time:

Expand Down
146 changes: 137 additions & 9 deletions scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#

from os.path import exists
import os
import sys
import json
import jsonschema
Expand All @@ -24,13 +25,15 @@
import subprocess
import logging as log
import base64
from collections import namedtuple
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_der_private_key

# A user can not change the factory data version and must be coherent with
# the factory data version set in the nRF Connect platform Kconfig file (CHIP_FACTORY_DATA_VERSION).
FACTORY_DATA_VERSION = 1

MATTER_ROOT = os.path.dirname(os.path.realpath(__file__))[:-len("/scripts/tools/nrfconnect")]
HEX_PREFIX = "hex:"
PUB_KEY_PREFIX = b'\x04'
INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444,
Expand Down Expand Up @@ -61,6 +64,112 @@ def get_raw_private_key_der(der_file: str, password: str):
return None


def gen_test_certs(chip_cert_exe: str,
output: str,
vendor_id: int,
product_id: int,
device_name: str,
generate_cd: bool = False,
paa_cert_path: str = None,
paa_key_path: str = None):
"""
Generate Matter certificates according to given Vendor ID and Product ID using the chip-cert executable.
To use own Product Attestation Authority certificate provide paa_cert_path and paa_key_path arguments.
Without providing these arguments a PAA certificate will be get from /credentials/test/attestation directory
in the Matter repository.
Args:
chip_cert_exe (str): path to chip-cert executable
output (str): output path to store a newly generated certificates (CD, DAC, PAI)
vendor_id (int): an identification number specific to Vendor
product_id (int): an identification number specific to Product
device_name (str): human-readable device name
generate_cd (bool, optional): Generate Certificate Declaration and store it in thee output directory. Defaults to False.
paa_cert_path (str, optional): provide PAA certification path. Defaults to None - a path will be set to /credentials/test/attestation directory.
paa_key_path (str, optional): provide PAA key path. Defaults to None - a path will be set to /credentials/test/attestation directory.
Returns:
dictionary: ["PAI_CERT": (str)<path to PAI cert .der file>,
"DAC_CERT": (str)<path to DAC cert .der file>,
"DAC_KEY": (str)<path to DAC key .der file>]
"""

CD_PATH = MATTER_ROOT + "/credentials/test/certification-declaration/Chip-Test-CD-Signing-Cert.pem"
CD_KEY_PATH = MATTER_ROOT + "/credentials/test/certification-declaration/Chip-Test-CD-Signing-Key.pem"
PAA_PATH = paa_cert_path if paa_cert_path != None else MATTER_ROOT + "/credentials/test/attestation/Chip-Test-PAA-NoVID-Cert.pem"
PAA_KEY_PATH = paa_key_path if paa_key_path != None else MATTER_ROOT + "/credentials/test/attestation/Chip-Test-PAA-NoVID-Key.pem"

attestation_certs = namedtuple("attestation_certs", ["dac_cert", "dac_key", "pai_cert"])

log.info("Generating new certificates using chip-cert...")

if generate_cd:
# generate Certification Declaration
cmd = [chip_cert_exe, "gen-cd",
"--key", CD_KEY_PATH,
"--cert", CD_PATH,
"--out", output + "/CD.der",
"--format-version", str(1),
"--vendor-id", hex(vendor_id),
"--product-id", hex(product_id),
"--device-type-id", "0xA",
"--certificate-id", "ZIG20142ZB330003-24",
"--security-level", str(0),
"--security-info", str(0),
"--certification-type", str(0),
"--version-number", "0x2694",
]
subprocess.run(cmd)

new_certificates = {"PAI_CERT": output + "/PAI_cert",
"PAI_KEY": output + "/PAI_key",
"DAC_CERT": output + "/DAC_cert",
"DAC_KEY": output + "/DAC_key"
}

# generate PAI
cmd = [chip_cert_exe, "gen-att-cert",
"-t", "i",
"-c", device_name,
"-V", hex(vendor_id),
"-C", PAA_PATH,
"-K", PAA_KEY_PATH,
"-o", new_certificates["PAI_CERT"] + ".pem",
"-O", new_certificates["PAI_KEY"] + ".pem",
"-l", str(10000),
]
subprocess.run(cmd)

# generate DAC
cmd = [chip_cert_exe, "gen-att-cert",
"-t", "d",
"-c", device_name,
"-V", hex(vendor_id),
"-P", hex(product_id),
"-C", new_certificates["PAI_CERT"] + ".pem",
"-K", new_certificates["PAI_KEY"] + ".pem",
"-o", new_certificates["DAC_CERT"] + ".pem",
"-O", new_certificates["DAC_KEY"] + ".pem",
"-l", str(10000),
]
subprocess.run(cmd)

# convert to .der files
for cert_k, cert_v in new_certificates.items():
action_type = "convert-cert" if cert_k.find("CERT") != -1 else "convert-key"
log.info(cert_v + ".der")
cmd = [chip_cert_exe, action_type,
cert_v + ".pem",
cert_v + ".der",
"--x509-der",
]
subprocess.run(cmd)

return attestation_certs(new_certificates["DAC_CERT"] + ".der",
new_certificates["DAC_KEY"] + ".der",
new_certificates["PAI_CERT"] + ".der")


def gen_spake2p_params(spake2p_path: str, passcode: int, it: int, salt: str) -> dict:
""" Generate spake2 params using external spake2p script
Expand Down Expand Up @@ -151,12 +260,29 @@ def generate_json(self):
spake_2_verifier = base64.b64decode(self._args.spake2_verifier)

# convert salt to bytestring to be coherent with spake2 verifier type
spake_2_salt = base64.b64decode(self._args.spake2_salt)
spake_2_salt = bytes(self._args.spake2_salt, 'utf-8')

if self._args.chip_cert_path:
certs = gen_test_certs(self._args.chip_cert_path,
self._args.output[:self._args.output.rfind("/")],
self._args.vendor_id,
self._args.product_id,
self._args.vendor_name + "_" + self._args.product_name,
self._args.gen_cd,
self._args.paa_cert,
self._args.paa_key)
dac_cert = certs.dac_cert
pai_cert = certs.pai_cert
dac_key = certs.dac_key
else:
dac_cert = self._args.dac_cert
dac_key = self._args.dac_key
pai_cert = self._args.pai_cert

# try to read DAC public and private keys
dac_priv_key = get_raw_private_key_der(self._args.dac_key, self._args.dac_key_password)
dac_priv_key = get_raw_private_key_der(dac_key, self._args.dac_key_password)
if dac_priv_key is None:
log.error("Can not read DAC keys from : {}".format(self._args.dac_key))
log.error("Can not read DAC keys from : {}".format(dac_key))
sys.exit(-1)

try:
Expand All @@ -175,9 +301,9 @@ def generate_json(self):
self._add_entry("date", self._args.date)
self._add_entry("hw_ver", self._args.hw_ver)
self._add_entry("hw_ver_str", self._args.hw_ver_str)
self._add_entry("dac_cert", self._process_der(self._args.dac_cert))
self._add_entry("dac_cert", self._process_der(dac_cert))
self._add_entry("dac_key", dac_priv_key)
self._add_entry("pai_cert", self._process_der(self._args.pai_cert))
self._add_entry("pai_cert", self._process_der(pai_cert))
if self._args.include_passcode:
self._add_entry("passcode", self._args.passcode)
self._add_entry("spake2_it", self._args.spake2_it)
Expand Down Expand Up @@ -329,17 +455,19 @@ def allow_any_int(i): return int(i, 0)
help="[ascii string] Provide Spake2 Verifier without generating it.")
optional_arguments.add_argument("--user", type=str,
help="[string] Provide additional user-specific keys in Json format: {'name_1': 'value_1', 'name_2': 'value_2', ... 'name_n', 'value_n'}.")
optional_arguments.add_argument("--gen_cd", action="store_true", default=False,
help="Generate a new Certificate Declaration in .der format according to used Vendor ID and Product ID. This certificate will not be included to the factory data.")
optional_arguments.add_argument("--paa_cert", type=str,
help="Provide a path to the Product Attestation Authority (PAA) certificate to generate the PAI certificate. Without providing it, a testing PAA stored in the Matter repository will be used.")
optional_arguments.add_argument("--paa_key", type=str,
help="Provide a path to the Product Attestation Authority (PAA) key to generate the PAI certificate. Without providing it, a testing PAA key stored in the Matter repository will be used.")
args = parser.parse_args()

if args.verbose:
log.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=log.DEBUG)
else:
log.basicConfig(format='[%(levelname)s] %(message)s', level=log.INFO)

if(args.chip_cert_path):
log.error("Generating DAC and PAI certificates is not supported yet")
return

# check if json file already exist
if(exists(args.output) and not args.overwrite):
log.error("Output file: {} already exist, to create a new one add argument '--overwrite'. By default overwriting is disabled".format(args.output))
Expand Down

0 comments on commit 89d2a6a

Please sign in to comment.