1a8e1175bSopenharmony_ci#!/usr/bin/env python3
2a8e1175bSopenharmony_ci#
3a8e1175bSopenharmony_ci# Copyright The Mbed TLS Contributors
4a8e1175bSopenharmony_ci# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
5a8e1175bSopenharmony_ci
6a8e1175bSopenharmony_ci"""Audit validity date of X509 crt/crl/csr.
7a8e1175bSopenharmony_ci
8a8e1175bSopenharmony_ciThis script is used to audit the validity date of crt/crl/csr used for testing.
9a8e1175bSopenharmony_ciIt prints the information about X.509 objects excluding the objects that
10a8e1175bSopenharmony_ciare valid throughout the desired validity period. The data are collected
11a8e1175bSopenharmony_cifrom tests/data_files/ and tests/suites/*.data files by default.
12a8e1175bSopenharmony_ci"""
13a8e1175bSopenharmony_ci
14a8e1175bSopenharmony_ciimport os
15a8e1175bSopenharmony_ciimport re
16a8e1175bSopenharmony_ciimport typing
17a8e1175bSopenharmony_ciimport argparse
18a8e1175bSopenharmony_ciimport datetime
19a8e1175bSopenharmony_ciimport glob
20a8e1175bSopenharmony_ciimport logging
21a8e1175bSopenharmony_ciimport hashlib
22a8e1175bSopenharmony_cifrom enum import Enum
23a8e1175bSopenharmony_ci
24a8e1175bSopenharmony_ci# The script requires cryptography >= 35.0.0 which is only available
25a8e1175bSopenharmony_ci# for Python >= 3.6.
26a8e1175bSopenharmony_ciimport cryptography
27a8e1175bSopenharmony_cifrom cryptography import x509
28a8e1175bSopenharmony_ci
29a8e1175bSopenharmony_cifrom generate_test_code import FileWrapper
30a8e1175bSopenharmony_ci
31a8e1175bSopenharmony_ciimport scripts_path # pylint: disable=unused-import
32a8e1175bSopenharmony_cifrom mbedtls_dev import build_tree
33a8e1175bSopenharmony_cifrom mbedtls_dev import logging_util
34a8e1175bSopenharmony_ci
35a8e1175bSopenharmony_cidef check_cryptography_version():
36a8e1175bSopenharmony_ci    match = re.match(r'^[0-9]+', cryptography.__version__)
37a8e1175bSopenharmony_ci    if match is None or int(match.group(0)) < 35:
38a8e1175bSopenharmony_ci        raise Exception("audit-validity-dates requires cryptography >= 35.0.0"
39a8e1175bSopenharmony_ci                        + "({} is too old)".format(cryptography.__version__))
40a8e1175bSopenharmony_ci
41a8e1175bSopenharmony_ciclass DataType(Enum):
42a8e1175bSopenharmony_ci    CRT = 1 # Certificate
43a8e1175bSopenharmony_ci    CRL = 2 # Certificate Revocation List
44a8e1175bSopenharmony_ci    CSR = 3 # Certificate Signing Request
45a8e1175bSopenharmony_ci
46a8e1175bSopenharmony_ci
47a8e1175bSopenharmony_ciclass DataFormat(Enum):
48a8e1175bSopenharmony_ci    PEM = 1 # Privacy-Enhanced Mail
49a8e1175bSopenharmony_ci    DER = 2 # Distinguished Encoding Rules
50a8e1175bSopenharmony_ci
51a8e1175bSopenharmony_ci
52a8e1175bSopenharmony_ciclass AuditData:
53a8e1175bSopenharmony_ci    """Store data location, type and validity period of X.509 objects."""
54a8e1175bSopenharmony_ci    #pylint: disable=too-few-public-methods
55a8e1175bSopenharmony_ci    def __init__(self, data_type: DataType, x509_obj):
56a8e1175bSopenharmony_ci        self.data_type = data_type
57a8e1175bSopenharmony_ci        # the locations that the x509 object could be found
58a8e1175bSopenharmony_ci        self.locations = [] # type: typing.List[str]
59a8e1175bSopenharmony_ci        self.fill_validity_duration(x509_obj)
60a8e1175bSopenharmony_ci        self._obj = x509_obj
61a8e1175bSopenharmony_ci        encoding = cryptography.hazmat.primitives.serialization.Encoding.DER
62a8e1175bSopenharmony_ci        self._identifier = hashlib.sha1(self._obj.public_bytes(encoding)).hexdigest()
63a8e1175bSopenharmony_ci
64a8e1175bSopenharmony_ci    @property
65a8e1175bSopenharmony_ci    def identifier(self):
66a8e1175bSopenharmony_ci        """
67a8e1175bSopenharmony_ci        Identifier of the underlying X.509 object, which is consistent across
68a8e1175bSopenharmony_ci        different runs.
69a8e1175bSopenharmony_ci        """
70a8e1175bSopenharmony_ci        return self._identifier
71a8e1175bSopenharmony_ci
72a8e1175bSopenharmony_ci    def fill_validity_duration(self, x509_obj):
73a8e1175bSopenharmony_ci        """Read validity period from an X.509 object."""
74a8e1175bSopenharmony_ci        # Certificate expires after "not_valid_after"
75a8e1175bSopenharmony_ci        # Certificate is invalid before "not_valid_before"
76a8e1175bSopenharmony_ci        if self.data_type == DataType.CRT:
77a8e1175bSopenharmony_ci            self.not_valid_after = x509_obj.not_valid_after
78a8e1175bSopenharmony_ci            self.not_valid_before = x509_obj.not_valid_before
79a8e1175bSopenharmony_ci        # CertificateRevocationList expires after "next_update"
80a8e1175bSopenharmony_ci        # CertificateRevocationList is invalid before "last_update"
81a8e1175bSopenharmony_ci        elif self.data_type == DataType.CRL:
82a8e1175bSopenharmony_ci            self.not_valid_after = x509_obj.next_update
83a8e1175bSopenharmony_ci            self.not_valid_before = x509_obj.last_update
84a8e1175bSopenharmony_ci        # CertificateSigningRequest is always valid.
85a8e1175bSopenharmony_ci        elif self.data_type == DataType.CSR:
86a8e1175bSopenharmony_ci            self.not_valid_after = datetime.datetime.max
87a8e1175bSopenharmony_ci            self.not_valid_before = datetime.datetime.min
88a8e1175bSopenharmony_ci        else:
89a8e1175bSopenharmony_ci            raise ValueError("Unsupported file_type: {}".format(self.data_type))
90a8e1175bSopenharmony_ci
91a8e1175bSopenharmony_ci
92a8e1175bSopenharmony_ciclass X509Parser:
93a8e1175bSopenharmony_ci    """A parser class to parse crt/crl/csr file or data in PEM/DER format."""
94a8e1175bSopenharmony_ci    PEM_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}(?P<data>.*?)-{5}END (?P=type)-{5}'
95a8e1175bSopenharmony_ci    PEM_TAG_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n'
96a8e1175bSopenharmony_ci    PEM_TAGS = {
97a8e1175bSopenharmony_ci        DataType.CRT: 'CERTIFICATE',
98a8e1175bSopenharmony_ci        DataType.CRL: 'X509 CRL',
99a8e1175bSopenharmony_ci        DataType.CSR: 'CERTIFICATE REQUEST'
100a8e1175bSopenharmony_ci    }
101a8e1175bSopenharmony_ci
102a8e1175bSopenharmony_ci    def __init__(self,
103a8e1175bSopenharmony_ci                 backends:
104a8e1175bSopenharmony_ci                 typing.Dict[DataType,
105a8e1175bSopenharmony_ci                             typing.Dict[DataFormat,
106a8e1175bSopenharmony_ci                                         typing.Callable[[bytes], object]]]) \
107a8e1175bSopenharmony_ci    -> None:
108a8e1175bSopenharmony_ci        self.backends = backends
109a8e1175bSopenharmony_ci        self.__generate_parsers()
110a8e1175bSopenharmony_ci
111a8e1175bSopenharmony_ci    def __generate_parser(self, data_type: DataType):
112a8e1175bSopenharmony_ci        """Parser generator for a specific DataType"""
113a8e1175bSopenharmony_ci        tag = self.PEM_TAGS[data_type]
114a8e1175bSopenharmony_ci        pem_loader = self.backends[data_type][DataFormat.PEM]
115a8e1175bSopenharmony_ci        der_loader = self.backends[data_type][DataFormat.DER]
116a8e1175bSopenharmony_ci        def wrapper(data: bytes):
117a8e1175bSopenharmony_ci            pem_type = X509Parser.pem_data_type(data)
118a8e1175bSopenharmony_ci            # It is in PEM format with target tag
119a8e1175bSopenharmony_ci            if pem_type == tag:
120a8e1175bSopenharmony_ci                return pem_loader(data)
121a8e1175bSopenharmony_ci            # It is in PEM format without target tag
122a8e1175bSopenharmony_ci            if pem_type:
123a8e1175bSopenharmony_ci                return None
124a8e1175bSopenharmony_ci            # It might be in DER format
125a8e1175bSopenharmony_ci            try:
126a8e1175bSopenharmony_ci                result = der_loader(data)
127a8e1175bSopenharmony_ci            except ValueError:
128a8e1175bSopenharmony_ci                result = None
129a8e1175bSopenharmony_ci            return result
130a8e1175bSopenharmony_ci        wrapper.__name__ = "{}.parser[{}]".format(type(self).__name__, tag)
131a8e1175bSopenharmony_ci        return wrapper
132a8e1175bSopenharmony_ci
133a8e1175bSopenharmony_ci    def __generate_parsers(self):
134a8e1175bSopenharmony_ci        """Generate parsers for all support DataType"""
135a8e1175bSopenharmony_ci        self.parsers = {}
136a8e1175bSopenharmony_ci        for data_type, _ in self.PEM_TAGS.items():
137a8e1175bSopenharmony_ci            self.parsers[data_type] = self.__generate_parser(data_type)
138a8e1175bSopenharmony_ci
139a8e1175bSopenharmony_ci    def __getitem__(self, item):
140a8e1175bSopenharmony_ci        return self.parsers[item]
141a8e1175bSopenharmony_ci
142a8e1175bSopenharmony_ci    @staticmethod
143a8e1175bSopenharmony_ci    def pem_data_type(data: bytes) -> typing.Optional[str]:
144a8e1175bSopenharmony_ci        """Get the tag from the data in PEM format
145a8e1175bSopenharmony_ci
146a8e1175bSopenharmony_ci        :param data: data to be checked in binary mode.
147a8e1175bSopenharmony_ci        :return: PEM tag or "" when no tag detected.
148a8e1175bSopenharmony_ci        """
149a8e1175bSopenharmony_ci        m = re.search(X509Parser.PEM_TAG_REGEX, data)
150a8e1175bSopenharmony_ci        if m is not None:
151a8e1175bSopenharmony_ci            return m.group('type').decode('UTF-8')
152a8e1175bSopenharmony_ci        else:
153a8e1175bSopenharmony_ci            return None
154a8e1175bSopenharmony_ci
155a8e1175bSopenharmony_ci    @staticmethod
156a8e1175bSopenharmony_ci    def check_hex_string(hex_str: str) -> bool:
157a8e1175bSopenharmony_ci        """Check if the hex string is possibly DER data."""
158a8e1175bSopenharmony_ci        hex_len = len(hex_str)
159a8e1175bSopenharmony_ci        # At least 6 hex char for 3 bytes: Type + Length + Content
160a8e1175bSopenharmony_ci        if hex_len < 6:
161a8e1175bSopenharmony_ci            return False
162a8e1175bSopenharmony_ci        # Check if Type (1 byte) is SEQUENCE.
163a8e1175bSopenharmony_ci        if hex_str[0:2] != '30':
164a8e1175bSopenharmony_ci            return False
165a8e1175bSopenharmony_ci        # Check LENGTH (1 byte) value
166a8e1175bSopenharmony_ci        content_len = int(hex_str[2:4], base=16)
167a8e1175bSopenharmony_ci        consumed = 4
168a8e1175bSopenharmony_ci        if content_len in (128, 255):
169a8e1175bSopenharmony_ci            # Indefinite or Reserved
170a8e1175bSopenharmony_ci            return False
171a8e1175bSopenharmony_ci        elif content_len > 127:
172a8e1175bSopenharmony_ci            # Definite, Long
173a8e1175bSopenharmony_ci            length_len = (content_len - 128) * 2
174a8e1175bSopenharmony_ci            content_len = int(hex_str[consumed:consumed+length_len], base=16)
175a8e1175bSopenharmony_ci            consumed += length_len
176a8e1175bSopenharmony_ci        # Check LENGTH
177a8e1175bSopenharmony_ci        if hex_len != content_len * 2 + consumed:
178a8e1175bSopenharmony_ci            return False
179a8e1175bSopenharmony_ci        return True
180a8e1175bSopenharmony_ci
181a8e1175bSopenharmony_ci
182a8e1175bSopenharmony_ciclass Auditor:
183a8e1175bSopenharmony_ci    """
184a8e1175bSopenharmony_ci    A base class that uses X509Parser to parse files to a list of AuditData.
185a8e1175bSopenharmony_ci
186a8e1175bSopenharmony_ci    A subclass must implement the following methods:
187a8e1175bSopenharmony_ci      - collect_default_files: Return a list of file names that are defaultly
188a8e1175bSopenharmony_ci        used for parsing (auditing). The list will be stored in
189a8e1175bSopenharmony_ci        Auditor.default_files.
190a8e1175bSopenharmony_ci      - parse_file: Method that parses a single file to a list of AuditData.
191a8e1175bSopenharmony_ci
192a8e1175bSopenharmony_ci    A subclass may override the following methods:
193a8e1175bSopenharmony_ci      - parse_bytes: Defaultly, it parses `bytes` that contains only one valid
194a8e1175bSopenharmony_ci        X.509 data(DER/PEM format) to an X.509 object.
195a8e1175bSopenharmony_ci      - walk_all: Defaultly, it iterates over all the files in the provided
196a8e1175bSopenharmony_ci        file name list, calls `parse_file` for each file and stores the results
197a8e1175bSopenharmony_ci        by extending the `results` passed to the function.
198a8e1175bSopenharmony_ci    """
199a8e1175bSopenharmony_ci    def __init__(self, logger):
200a8e1175bSopenharmony_ci        self.logger = logger
201a8e1175bSopenharmony_ci        self.default_files = self.collect_default_files()
202a8e1175bSopenharmony_ci        self.parser = X509Parser({
203a8e1175bSopenharmony_ci            DataType.CRT: {
204a8e1175bSopenharmony_ci                DataFormat.PEM: x509.load_pem_x509_certificate,
205a8e1175bSopenharmony_ci                DataFormat.DER: x509.load_der_x509_certificate
206a8e1175bSopenharmony_ci            },
207a8e1175bSopenharmony_ci            DataType.CRL: {
208a8e1175bSopenharmony_ci                DataFormat.PEM: x509.load_pem_x509_crl,
209a8e1175bSopenharmony_ci                DataFormat.DER: x509.load_der_x509_crl
210a8e1175bSopenharmony_ci            },
211a8e1175bSopenharmony_ci            DataType.CSR: {
212a8e1175bSopenharmony_ci                DataFormat.PEM: x509.load_pem_x509_csr,
213a8e1175bSopenharmony_ci                DataFormat.DER: x509.load_der_x509_csr
214a8e1175bSopenharmony_ci            },
215a8e1175bSopenharmony_ci        })
216a8e1175bSopenharmony_ci
217a8e1175bSopenharmony_ci    def collect_default_files(self) -> typing.List[str]:
218a8e1175bSopenharmony_ci        """Collect the default files for parsing."""
219a8e1175bSopenharmony_ci        raise NotImplementedError
220a8e1175bSopenharmony_ci
221a8e1175bSopenharmony_ci    def parse_file(self, filename: str) -> typing.List[AuditData]:
222a8e1175bSopenharmony_ci        """
223a8e1175bSopenharmony_ci        Parse a list of AuditData from file.
224a8e1175bSopenharmony_ci
225a8e1175bSopenharmony_ci        :param filename: name of the file to parse.
226a8e1175bSopenharmony_ci        :return list of AuditData parsed from the file.
227a8e1175bSopenharmony_ci        """
228a8e1175bSopenharmony_ci        raise NotImplementedError
229a8e1175bSopenharmony_ci
230a8e1175bSopenharmony_ci    def parse_bytes(self, data: bytes):
231a8e1175bSopenharmony_ci        """Parse AuditData from bytes."""
232a8e1175bSopenharmony_ci        for data_type in list(DataType):
233a8e1175bSopenharmony_ci            try:
234a8e1175bSopenharmony_ci                result = self.parser[data_type](data)
235a8e1175bSopenharmony_ci            except ValueError as val_error:
236a8e1175bSopenharmony_ci                result = None
237a8e1175bSopenharmony_ci                self.logger.warning(val_error)
238a8e1175bSopenharmony_ci            if result is not None:
239a8e1175bSopenharmony_ci                audit_data = AuditData(data_type, result)
240a8e1175bSopenharmony_ci                return audit_data
241a8e1175bSopenharmony_ci        return None
242a8e1175bSopenharmony_ci
243a8e1175bSopenharmony_ci    def walk_all(self,
244a8e1175bSopenharmony_ci                 results: typing.Dict[str, AuditData],
245a8e1175bSopenharmony_ci                 file_list: typing.Optional[typing.List[str]] = None) \
246a8e1175bSopenharmony_ci        -> None:
247a8e1175bSopenharmony_ci        """
248a8e1175bSopenharmony_ci        Iterate over all the files in the list and get audit data. The
249a8e1175bSopenharmony_ci        results will be written to `results` passed to this function.
250a8e1175bSopenharmony_ci
251a8e1175bSopenharmony_ci        :param results: The dictionary used to store the parsed
252a8e1175bSopenharmony_ci                        AuditData. The keys of this dictionary should
253a8e1175bSopenharmony_ci                        be the identifier of the AuditData.
254a8e1175bSopenharmony_ci        """
255a8e1175bSopenharmony_ci        if file_list is None:
256a8e1175bSopenharmony_ci            file_list = self.default_files
257a8e1175bSopenharmony_ci        for filename in file_list:
258a8e1175bSopenharmony_ci            data_list = self.parse_file(filename)
259a8e1175bSopenharmony_ci            for d in data_list:
260a8e1175bSopenharmony_ci                if d.identifier in results:
261a8e1175bSopenharmony_ci                    results[d.identifier].locations.extend(d.locations)
262a8e1175bSopenharmony_ci                else:
263a8e1175bSopenharmony_ci                    results[d.identifier] = d
264a8e1175bSopenharmony_ci
265a8e1175bSopenharmony_ci    @staticmethod
266a8e1175bSopenharmony_ci    def find_test_dir():
267a8e1175bSopenharmony_ci        """Get the relative path for the Mbed TLS test directory."""
268a8e1175bSopenharmony_ci        return os.path.relpath(build_tree.guess_mbedtls_root() + '/tests')
269a8e1175bSopenharmony_ci
270a8e1175bSopenharmony_ci
271a8e1175bSopenharmony_ciclass TestDataAuditor(Auditor):
272a8e1175bSopenharmony_ci    """Class for auditing files in `tests/data_files/`"""
273a8e1175bSopenharmony_ci
274a8e1175bSopenharmony_ci    def collect_default_files(self):
275a8e1175bSopenharmony_ci        """Collect all files in `tests/data_files/`"""
276a8e1175bSopenharmony_ci        test_dir = self.find_test_dir()
277a8e1175bSopenharmony_ci        test_data_glob = os.path.join(test_dir, 'data_files/**')
278a8e1175bSopenharmony_ci        data_files = [f for f in glob.glob(test_data_glob, recursive=True)
279a8e1175bSopenharmony_ci                      if os.path.isfile(f)]
280a8e1175bSopenharmony_ci        return data_files
281a8e1175bSopenharmony_ci
282a8e1175bSopenharmony_ci    def parse_file(self, filename: str) -> typing.List[AuditData]:
283a8e1175bSopenharmony_ci        """
284a8e1175bSopenharmony_ci        Parse a list of AuditData from data file.
285a8e1175bSopenharmony_ci
286a8e1175bSopenharmony_ci        :param filename: name of the file to parse.
287a8e1175bSopenharmony_ci        :return list of AuditData parsed from the file.
288a8e1175bSopenharmony_ci        """
289a8e1175bSopenharmony_ci        with open(filename, 'rb') as f:
290a8e1175bSopenharmony_ci            data = f.read()
291a8e1175bSopenharmony_ci
292a8e1175bSopenharmony_ci        results = []
293a8e1175bSopenharmony_ci        # Try to parse all PEM blocks.
294a8e1175bSopenharmony_ci        is_pem = False
295a8e1175bSopenharmony_ci        for idx, m in enumerate(re.finditer(X509Parser.PEM_REGEX, data, flags=re.S), 1):
296a8e1175bSopenharmony_ci            is_pem = True
297a8e1175bSopenharmony_ci            result = self.parse_bytes(data[m.start():m.end()])
298a8e1175bSopenharmony_ci            if result is not None:
299a8e1175bSopenharmony_ci                result.locations.append("{}#{}".format(filename, idx))
300a8e1175bSopenharmony_ci                results.append(result)
301a8e1175bSopenharmony_ci
302a8e1175bSopenharmony_ci        # Might be DER format.
303a8e1175bSopenharmony_ci        if not is_pem:
304a8e1175bSopenharmony_ci            result = self.parse_bytes(data)
305a8e1175bSopenharmony_ci            if result is not None:
306a8e1175bSopenharmony_ci                result.locations.append("{}".format(filename))
307a8e1175bSopenharmony_ci                results.append(result)
308a8e1175bSopenharmony_ci
309a8e1175bSopenharmony_ci        return results
310a8e1175bSopenharmony_ci
311a8e1175bSopenharmony_ci
312a8e1175bSopenharmony_cidef parse_suite_data(data_f):
313a8e1175bSopenharmony_ci    """
314a8e1175bSopenharmony_ci    Parses .data file for test arguments that possiblly have a
315a8e1175bSopenharmony_ci    valid X.509 data. If you need a more precise parser, please
316a8e1175bSopenharmony_ci    use generate_test_code.parse_test_data instead.
317a8e1175bSopenharmony_ci
318a8e1175bSopenharmony_ci    :param data_f: file object of the data file.
319a8e1175bSopenharmony_ci    :return: Generator that yields test function argument list.
320a8e1175bSopenharmony_ci    """
321a8e1175bSopenharmony_ci    for line in data_f:
322a8e1175bSopenharmony_ci        line = line.strip()
323a8e1175bSopenharmony_ci        # Skip comments
324a8e1175bSopenharmony_ci        if line.startswith('#'):
325a8e1175bSopenharmony_ci            continue
326a8e1175bSopenharmony_ci
327a8e1175bSopenharmony_ci        # Check parameters line
328a8e1175bSopenharmony_ci        match = re.search(r'\A\w+(.*:)?\"', line)
329a8e1175bSopenharmony_ci        if match:
330a8e1175bSopenharmony_ci            # Read test vectors
331a8e1175bSopenharmony_ci            parts = re.split(r'(?<!\\):', line)
332a8e1175bSopenharmony_ci            parts = [x for x in parts if x]
333a8e1175bSopenharmony_ci            args = parts[1:]
334a8e1175bSopenharmony_ci            yield args
335a8e1175bSopenharmony_ci
336a8e1175bSopenharmony_ci
337a8e1175bSopenharmony_ciclass SuiteDataAuditor(Auditor):
338a8e1175bSopenharmony_ci    """Class for auditing files in `tests/suites/*.data`"""
339a8e1175bSopenharmony_ci
340a8e1175bSopenharmony_ci    def collect_default_files(self):
341a8e1175bSopenharmony_ci        """Collect all files in `tests/suites/*.data`"""
342a8e1175bSopenharmony_ci        test_dir = self.find_test_dir()
343a8e1175bSopenharmony_ci        suites_data_folder = os.path.join(test_dir, 'suites')
344a8e1175bSopenharmony_ci        data_files = glob.glob(os.path.join(suites_data_folder, '*.data'))
345a8e1175bSopenharmony_ci        return data_files
346a8e1175bSopenharmony_ci
347a8e1175bSopenharmony_ci    def parse_file(self, filename: str):
348a8e1175bSopenharmony_ci        """
349a8e1175bSopenharmony_ci        Parse a list of AuditData from test suite data file.
350a8e1175bSopenharmony_ci
351a8e1175bSopenharmony_ci        :param filename: name of the file to parse.
352a8e1175bSopenharmony_ci        :return list of AuditData parsed from the file.
353a8e1175bSopenharmony_ci        """
354a8e1175bSopenharmony_ci        audit_data_list = []
355a8e1175bSopenharmony_ci        data_f = FileWrapper(filename)
356a8e1175bSopenharmony_ci        for test_args in parse_suite_data(data_f):
357a8e1175bSopenharmony_ci            for idx, test_arg in enumerate(test_args):
358a8e1175bSopenharmony_ci                match = re.match(r'"(?P<data>[0-9a-fA-F]+)"', test_arg)
359a8e1175bSopenharmony_ci                if not match:
360a8e1175bSopenharmony_ci                    continue
361a8e1175bSopenharmony_ci                if not X509Parser.check_hex_string(match.group('data')):
362a8e1175bSopenharmony_ci                    continue
363a8e1175bSopenharmony_ci                audit_data = self.parse_bytes(bytes.fromhex(match.group('data')))
364a8e1175bSopenharmony_ci                if audit_data is None:
365a8e1175bSopenharmony_ci                    continue
366a8e1175bSopenharmony_ci                audit_data.locations.append("{}:{}:#{}".format(filename,
367a8e1175bSopenharmony_ci                                                               data_f.line_no,
368a8e1175bSopenharmony_ci                                                               idx + 1))
369a8e1175bSopenharmony_ci                audit_data_list.append(audit_data)
370a8e1175bSopenharmony_ci
371a8e1175bSopenharmony_ci        return audit_data_list
372a8e1175bSopenharmony_ci
373a8e1175bSopenharmony_ci
374a8e1175bSopenharmony_cidef list_all(audit_data: AuditData):
375a8e1175bSopenharmony_ci    for loc in audit_data.locations:
376a8e1175bSopenharmony_ci        print("{}\t{:20}\t{:20}\t{:3}\t{}".format(
377a8e1175bSopenharmony_ci            audit_data.identifier,
378a8e1175bSopenharmony_ci            audit_data.not_valid_before.isoformat(timespec='seconds'),
379a8e1175bSopenharmony_ci            audit_data.not_valid_after.isoformat(timespec='seconds'),
380a8e1175bSopenharmony_ci            audit_data.data_type.name,
381a8e1175bSopenharmony_ci            loc))
382a8e1175bSopenharmony_ci
383a8e1175bSopenharmony_ci
384a8e1175bSopenharmony_cidef main():
385a8e1175bSopenharmony_ci    """
386a8e1175bSopenharmony_ci    Perform argument parsing.
387a8e1175bSopenharmony_ci    """
388a8e1175bSopenharmony_ci    parser = argparse.ArgumentParser(description=__doc__)
389a8e1175bSopenharmony_ci
390a8e1175bSopenharmony_ci    parser.add_argument('-a', '--all',
391a8e1175bSopenharmony_ci                        action='store_true',
392a8e1175bSopenharmony_ci                        help='list the information of all the files')
393a8e1175bSopenharmony_ci    parser.add_argument('-v', '--verbose',
394a8e1175bSopenharmony_ci                        action='store_true', dest='verbose',
395a8e1175bSopenharmony_ci                        help='show logs')
396a8e1175bSopenharmony_ci    parser.add_argument('--from', dest='start_date',
397a8e1175bSopenharmony_ci                        help=('Start of desired validity period (UTC, YYYY-MM-DD). '
398a8e1175bSopenharmony_ci                              'Default: today'),
399a8e1175bSopenharmony_ci                        metavar='DATE')
400a8e1175bSopenharmony_ci    parser.add_argument('--to', dest='end_date',
401a8e1175bSopenharmony_ci                        help=('End of desired validity period (UTC, YYYY-MM-DD). '
402a8e1175bSopenharmony_ci                              'Default: --from'),
403a8e1175bSopenharmony_ci                        metavar='DATE')
404a8e1175bSopenharmony_ci    parser.add_argument('--data-files', action='append', nargs='*',
405a8e1175bSopenharmony_ci                        help='data files to audit',
406a8e1175bSopenharmony_ci                        metavar='FILE')
407a8e1175bSopenharmony_ci    parser.add_argument('--suite-data-files', action='append', nargs='*',
408a8e1175bSopenharmony_ci                        help='suite data files to audit',
409a8e1175bSopenharmony_ci                        metavar='FILE')
410a8e1175bSopenharmony_ci
411a8e1175bSopenharmony_ci    args = parser.parse_args()
412a8e1175bSopenharmony_ci
413a8e1175bSopenharmony_ci    # start main routine
414a8e1175bSopenharmony_ci    # setup logger
415a8e1175bSopenharmony_ci    logger = logging.getLogger()
416a8e1175bSopenharmony_ci    logging_util.configure_logger(logger)
417a8e1175bSopenharmony_ci    logger.setLevel(logging.DEBUG if args.verbose else logging.ERROR)
418a8e1175bSopenharmony_ci
419a8e1175bSopenharmony_ci    td_auditor = TestDataAuditor(logger)
420a8e1175bSopenharmony_ci    sd_auditor = SuiteDataAuditor(logger)
421a8e1175bSopenharmony_ci
422a8e1175bSopenharmony_ci    data_files = []
423a8e1175bSopenharmony_ci    suite_data_files = []
424a8e1175bSopenharmony_ci    if args.data_files is None and args.suite_data_files is None:
425a8e1175bSopenharmony_ci        data_files = td_auditor.default_files
426a8e1175bSopenharmony_ci        suite_data_files = sd_auditor.default_files
427a8e1175bSopenharmony_ci    else:
428a8e1175bSopenharmony_ci        if args.data_files is not None:
429a8e1175bSopenharmony_ci            data_files = [x for l in args.data_files for x in l]
430a8e1175bSopenharmony_ci        if args.suite_data_files is not None:
431a8e1175bSopenharmony_ci            suite_data_files = [x for l in args.suite_data_files for x in l]
432a8e1175bSopenharmony_ci
433a8e1175bSopenharmony_ci    # validity period start date
434a8e1175bSopenharmony_ci    if args.start_date:
435a8e1175bSopenharmony_ci        start_date = datetime.datetime.fromisoformat(args.start_date)
436a8e1175bSopenharmony_ci    else:
437a8e1175bSopenharmony_ci        start_date = datetime.datetime.today()
438a8e1175bSopenharmony_ci    # validity period end date
439a8e1175bSopenharmony_ci    if args.end_date:
440a8e1175bSopenharmony_ci        end_date = datetime.datetime.fromisoformat(args.end_date)
441a8e1175bSopenharmony_ci    else:
442a8e1175bSopenharmony_ci        end_date = start_date
443a8e1175bSopenharmony_ci
444a8e1175bSopenharmony_ci    # go through all the files
445a8e1175bSopenharmony_ci    audit_results = {}
446a8e1175bSopenharmony_ci    td_auditor.walk_all(audit_results, data_files)
447a8e1175bSopenharmony_ci    sd_auditor.walk_all(audit_results, suite_data_files)
448a8e1175bSopenharmony_ci
449a8e1175bSopenharmony_ci    logger.info("Total: {} objects found!".format(len(audit_results)))
450a8e1175bSopenharmony_ci
451a8e1175bSopenharmony_ci    # we filter out the files whose validity duration covers the provided
452a8e1175bSopenharmony_ci    # duration.
453a8e1175bSopenharmony_ci    filter_func = lambda d: (start_date < d.not_valid_before) or \
454a8e1175bSopenharmony_ci                            (d.not_valid_after < end_date)
455a8e1175bSopenharmony_ci
456a8e1175bSopenharmony_ci    sortby_end = lambda d: d.not_valid_after
457a8e1175bSopenharmony_ci
458a8e1175bSopenharmony_ci    if args.all:
459a8e1175bSopenharmony_ci        filter_func = None
460a8e1175bSopenharmony_ci
461a8e1175bSopenharmony_ci    # filter and output the results
462a8e1175bSopenharmony_ci    for d in sorted(filter(filter_func, audit_results.values()), key=sortby_end):
463a8e1175bSopenharmony_ci        list_all(d)
464a8e1175bSopenharmony_ci
465a8e1175bSopenharmony_ci    logger.debug("Done!")
466a8e1175bSopenharmony_ci
467a8e1175bSopenharmony_cicheck_cryptography_version()
468a8e1175bSopenharmony_ciif __name__ == "__main__":
469a8e1175bSopenharmony_ci    main()
470