| #!/usr/bin/python |
| |
| # |
| # Copyright (c) 2022 Project CHIP Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| # Script that was used to fetch CHIP Development Product Attestation Authority (PAA) |
| # certificates from DCL. |
| # For usage please run:. |
| # python ./credentials/fetch-paa-certs-from-dcl.py --help |
| |
| import copy |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| import click |
| import requests |
| from click_option_group import RequiredMutuallyExclusiveOptionGroup, optgroup |
| from cryptography import x509 |
| from cryptography.hazmat.primitives import serialization |
| |
| PRODUCTION_NODE_URL = "https://on.dcl.csa-iot.org:26657" |
| PRODUCTION_NODE_URL_REST = "https://on.dcl.csa-iot.org" |
| TEST_NODE_URL_REST = "https://on.test-net.dcl.csa-iot.org" |
| |
| MATTER_CERT_CA_SUBJECT = "MFIxDDAKBgNVBAoMA0NTQTEsMCoGA1UEAwwjTWF0dGVyIENlcnRpZmljYXRpb24gYW5kIFRlc3RpbmcgQ0ExFDASBgorBgEEAYKifAIBDARDNUEw" |
| MATTER_CERT_CA_SUBJECT_KEY_ID = "97:E4:69:D0:C5:04:14:C2:6F:C7:01:F7:7E:94:77:39:09:8D:F6:A5" |
| |
| |
| def parse_paa_root_certs(cmdpipe, paa_list): |
| """ |
| example output of a query to all x509 root certs in DCL: |
| |
| certs: |
| - subject: MCExHzAdBgNVBAMMFk1hdHRlciBEZXZlbG9wbWVudCBQQUE= |
| subjectKeyId: FA:92:CF:09:5E:FA:42:E1:14:30:65:16:32:FE:FE:1B:2C:77:A7:C8 |
| - subject: MDAxGDAWBgNVBAMMD01hdHRlciBUZXN0IFBBQTEUMBIGCisGAQQBgqJ8AgEMBDEyNUQ= |
| subjectKeyId: E2:90:8D:36:9C:3C:A3:C1:13:BB:09:E2:4D:C1:CC:C5:A6:66:91:D4 |
| - subject: MEsxCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZHb29nbGUxFTATBgNVBAMMDE1hdHRlciBQQUEgMTEUMBIGCisGAQQBgqJ8AgEMBDYwMDY= |
| subjectKeyId: B0:00:56:81:B8:88:62:89:62:80:E1:21:18:A1:A8:BE:09:DE:93:21 |
| - subject: MFUxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEtMCsGA1UEAxMkRGlnaUNlcnQgVEVTVCBSb290IENBIGZvciBNQVRURVIgUEtJ |
| subjectKeyId: C0:E0:64:15:00:EC:67:E2:7C:AF:7C:6E:2D:49:94:C7:73:DE:B7:BA |
| - subject: MDAxLjAsBgNVBAMMJU5vbiBQcm9kdWN0aW9uIE9OTFkgLSBYRk4gUEFBIENsYXNzIDM= |
| subjectKeyId: F8:99:A9:D5:AD:71:71:E4:C3:81:7F:14:10:7F:78:F0:D9:F7:62:E9 |
| - subject: MEIxGDAWBgNVBAMMD01hdHRlciBUZXN0IFBBQTEQMA4GA1UECgwHU2Ftc3VuZzEUMBIGCisGAQQBgqJ8AgEMBDEwRTE= |
| subjectKeyId: CF:9E:0A:16:78:8B:40:30:EC:DD:AB:34:B9:C2:EC:7B:E5:34:55:C0 |
| |
| Brief: |
| This method will search for the first line that contains ': ' char sequence. |
| From there, it assumes every 2 lines contain subject and subject key id info of |
| a valid PAA root certificate. |
| The paa_list parameter will contain a list of all valid PAA Root certificates |
| from DCL. |
| """ |
| |
| result = {} |
| |
| while True: |
| line = cmdpipe.stdout.readline() |
| if not line: |
| break |
| else: |
| if b': ' in line: |
| key, value = line.split(b': ') |
| result[key.strip(b' -').decode("utf-8") |
| ] = value.strip().decode("utf-8") |
| parse_paa_root_certs.counter += 1 |
| if parse_paa_root_certs.counter % 2 == 0: |
| paa_list.append(copy.deepcopy(result)) |
| |
| |
| def write_cert(certificate, subject): |
| filename = 'dcld_mirror_' + \ |
| re.sub('[^a-zA-Z0-9_-]', '', re.sub('[=, ]', '_', subject)) |
| with open(filename + '.pem', 'w+') as outfile: |
| outfile.write(certificate) |
| # convert pem file to der |
| try: |
| with open(filename + '.pem', 'rb') as infile: |
| pem_certificate = x509.load_pem_x509_certificate(infile.read()) |
| with open(filename + '.der', 'wb+') as outfile: |
| der_certificate = pem_certificate.public_bytes( |
| serialization.Encoding.DER) |
| outfile.write(der_certificate) |
| except (IOError, ValueError) as e: |
| print( |
| f"ERROR: Failed to convert {filename + '.pem'}: {str(e)}. Skipping...") |
| |
| |
| def parse_paa_root_cert_from_dcld(cmdpipe): |
| subject = None |
| certificate = "" |
| |
| while True: |
| line = cmdpipe.stdout.readline() |
| if not line: |
| break |
| else: |
| if b'pemCert: |' in line: |
| while True: |
| line = cmdpipe.stdout.readline() |
| certificate += line.strip(b' \t').decode("utf-8") |
| if b'-----END CERTIFICATE-----' in line: |
| break |
| if b'subjectAsText:' in line: |
| subject = line.split(b': ')[1].strip().decode("utf-8") |
| break |
| |
| return (certificate, subject) |
| |
| |
| def use_dcld(dcld, production, cmdlist): |
| return [dcld] + cmdlist + (['--node', PRODUCTION_NODE_URL] if production else []) |
| |
| |
| @click.command() |
| @click.help_option('-h', '--help') |
| @optgroup.group('Input data sources', cls=RequiredMutuallyExclusiveOptionGroup) |
| @optgroup.option('--use-main-net-dcld', type=str, default='', metavar='PATH', help="Location of `dcld` binary, to use `dcld` for mirroring MainNet.") |
| @optgroup.option('--use-test-net-dcld', type=str, default='', metavar='PATH', help="Location of `dcld` binary, to use `dcld` for mirroring TestNet.") |
| @optgroup.option('--use-main-net-http', is_flag=True, type=str, help="Use RESTful API with HTTPS against public MainNet observer.") |
| @optgroup.option('--use-test-net-http', is_flag=True, type=str, help="Use RESTful API with HTTPS against public TestNet observer.") |
| @optgroup.group('Optional arguments') |
| @optgroup.option('--paa-trust-store-path', default='paa-root-certs', type=str, metavar='PATH', help="PAA trust store path (default: paa-root-certs)") |
| def main(use_main_net_dcld, use_test_net_dcld, use_main_net_http, use_test_net_http, paa_trust_store_path): |
| """DCL PAA mirroring tools""" |
| fetch_paa_certs(use_main_net_dcld, use_test_net_dcld, use_main_net_http, use_test_net_http, paa_trust_store_path) |
| |
| |
| def get_cert_from_rest(rest_node_url, subject, subject_key_id): |
| response = requests.get( |
| f"{rest_node_url}/dcl/pki/certificates/{subject}/{subject_key_id}").json()["approvedCertificates"]["certs"][0] |
| certificate = response["pemCert"].rstrip("\n") |
| subject = response["subjectAsText"] |
| return certificate, subject |
| |
| |
| def fetch_cd_signing_certs(store_path): |
| ''' Only supports using main net http currently.''' |
| rest_node_url = PRODUCTION_NODE_URL_REST |
| os.makedirs(store_path, exist_ok=True) |
| original_dir = os.getcwd() |
| os.chdir(store_path) |
| |
| cd_signer_ids = requests.get( |
| f"{rest_node_url}/dcl/pki/child-certificates/{MATTER_CERT_CA_SUBJECT}/{MATTER_CERT_CA_SUBJECT_KEY_ID}").json()['childCertificates']['certIds'] |
| for signer in cd_signer_ids: |
| subject = signer['subject'] |
| subject_key_id = signer['subjectKeyId'] |
| certificate, subject = get_cert_from_rest(rest_node_url, subject, subject_key_id) |
| |
| print(f"Downloaded CD signing cert with subject: {subject}") |
| write_cert(certificate, subject) |
| |
| os.chdir(original_dir) |
| |
| |
| def fetch_paa_certs(use_main_net_dcld, use_test_net_dcld, use_main_net_http, use_test_net_http, paa_trust_store_path): |
| production = False |
| dcld = use_test_net_dcld |
| |
| if len(use_main_net_dcld) > 0: |
| dcld = use_main_net_dcld |
| production = True |
| |
| use_rest = use_main_net_http or use_test_net_http |
| if use_main_net_http: |
| production = True |
| |
| rest_node_url = PRODUCTION_NODE_URL_REST if production else TEST_NODE_URL_REST |
| |
| os.makedirs(paa_trust_store_path, exist_ok=True) |
| original_dir = os.getcwd() |
| os.chdir(paa_trust_store_path) |
| |
| if use_rest: |
| paa_list = requests.get( |
| f"{rest_node_url}/dcl/pki/root-certificates").json()["approvedRootCertificates"]["certs"] |
| else: |
| cmdlist = ['query', 'pki', 'all-x509-root-certs'] |
| |
| cmdpipe = subprocess.Popen(use_dcld( |
| dcld, production, cmdlist), stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| |
| paa_list = [] |
| parse_paa_root_certs.counter = 0 |
| parse_paa_root_certs(cmdpipe, paa_list) |
| |
| for paa in paa_list: |
| if paa['subject'] == MATTER_CERT_CA_SUBJECT and paa['subjectKeyId'] == MATTER_CERT_CA_SUBJECT_KEY_ID: |
| # Don't include the CD signing cert as a PAA root. |
| continue |
| if use_rest: |
| certificate, subject = get_cert_from_rest(rest_node_url, paa['subject'], paa['subjectKeyId']) |
| else: |
| cmdlist = ['query', 'pki', 'x509-cert', '-u', |
| paa['subject'], '-k', paa['subjectKeyId']] |
| |
| cmdpipe = subprocess.Popen(use_dcld( |
| dcld, production, cmdlist), stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| |
| (certificate, subject) = parse_paa_root_cert_from_dcld(cmdpipe) |
| |
| certificate = certificate.rstrip('\n') |
| |
| print(f"Downloaded PAA certificate with subject: {subject}") |
| write_cert(certificate, subject) |
| |
| os.chdir(original_dir) |
| |
| |
| if __name__ == "__main__": |
| if len(sys.argv) == 1: |
| main.main(['--help']) |
| else: |
| main() |