blob: 577179d0b4f5dff1e6d97e7d16f5640710112a0c [file] [log] [blame]
Pengyu Lv7f6933a2023-04-04 16:05:54 +08001#!/usr/bin/env python3
2#
3# copyright the mbed tls contributors
4# spdx-license-identifier: apache-2.0
5#
6# licensed under the apache license, version 2.0 (the "license"); you may
7# not use this file except in compliance with the license.
8# you may obtain a copy of the license at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
Pengyu Lv57240952023-04-13 14:42:37 +080018"""Audit validity date of X509 crt/crl/csr.
Pengyu Lv7f6933a2023-04-04 16:05:54 +080019
20This script is used to audit the validity date of crt/crl/csr used for testing.
Pengyu Lv57240952023-04-13 14:42:37 +080021It prints the information of X509 data whose validity duration does not cover
22the provided validity duration. The data are collected from tests/data_files/
23and tests/suites/*.data files by default.
Pengyu Lv7f6933a2023-04-04 16:05:54 +080024"""
25
26import os
27import sys
28import re
29import typing
Pengyu Lv7f6933a2023-04-04 16:05:54 +080030import argparse
31import datetime
Pengyu Lv45e32032023-04-06 14:33:41 +080032import glob
Pengyu Lv7f6933a2023-04-04 16:05:54 +080033from enum import Enum
34
Pengyu Lv31792322023-04-11 16:30:54 +080035# The script requires cryptography >= 35.0.0 which is only available
36# for Python >= 3.6. Disable the pylint error here until we were
37# using modern system on our CI.
38from cryptography import x509 #pylint: disable=import-error
Pengyu Lv7f6933a2023-04-04 16:05:54 +080039
Pengyu Lv30f26832023-04-07 18:04:07 +080040# reuse the function to parse *.data file in tests/suites/
41from generate_test_code import parse_test_data as parse_suite_data
42
Pengyu Lv7f6933a2023-04-04 16:05:54 +080043class DataType(Enum):
44 CRT = 1 # Certificate
45 CRL = 2 # Certificate Revocation List
46 CSR = 3 # Certificate Signing Request
47
48class DataFormat(Enum):
49 PEM = 1 # Privacy-Enhanced Mail
50 DER = 2 # Distinguished Encoding Rules
51
52class AuditData:
53 """Store file, type and expiration date for audit."""
54 #pylint: disable=too-few-public-methods
Pengyu Lvcb8fc322023-04-11 15:05:29 +080055 def __init__(self, data_type: DataType, x509_obj):
Pengyu Lv7f6933a2023-04-04 16:05:54 +080056 self.data_type = data_type
57 self.filename = ""
Pengyu Lvcb8fc322023-04-11 15:05:29 +080058 self.fill_validity_duration(x509_obj)
Pengyu Lv7f6933a2023-04-04 16:05:54 +080059
60 def fill_validity_duration(self, x509_obj):
61 """Fill expiration_date field from a x509 object"""
62 # Certificate expires after "not_valid_after"
63 # Certificate is invalid before "not_valid_before"
64 if self.data_type == DataType.CRT:
65 self.not_valid_after = x509_obj.not_valid_after
66 self.not_valid_before = x509_obj.not_valid_before
67 # CertificateRevocationList expires after "next_update"
68 # CertificateRevocationList is invalid before "last_update"
69 elif self.data_type == DataType.CRL:
70 self.not_valid_after = x509_obj.next_update
71 self.not_valid_before = x509_obj.last_update
72 # CertificateSigningRequest is always valid.
73 elif self.data_type == DataType.CSR:
74 self.not_valid_after = datetime.datetime.max
75 self.not_valid_before = datetime.datetime.min
76 else:
77 raise ValueError("Unsupported file_type: {}".format(self.data_type))
78
79class X509Parser():
80 """A parser class to parse crt/crl/csr file or data in PEM/DER format."""
81 PEM_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n(?P<data>.*?)-{5}END (?P=type)-{5}\n'
82 PEM_TAG_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n'
83 PEM_TAGS = {
84 DataType.CRT: 'CERTIFICATE',
85 DataType.CRL: 'X509 CRL',
86 DataType.CSR: 'CERTIFICATE REQUEST'
87 }
88
89 def __init__(self, backends: dict):
90 self.backends = backends
91 self.__generate_parsers()
92
93 def __generate_parser(self, data_type: DataType):
94 """Parser generator for a specific DataType"""
95 tag = self.PEM_TAGS[data_type]
96 pem_loader = self.backends[data_type][DataFormat.PEM]
97 der_loader = self.backends[data_type][DataFormat.DER]
98 def wrapper(data: bytes):
99 pem_type = X509Parser.pem_data_type(data)
100 # It is in PEM format with target tag
101 if pem_type == tag:
102 return pem_loader(data)
103 # It is in PEM format without target tag
104 if pem_type:
105 return None
106 # It might be in DER format
107 try:
108 result = der_loader(data)
109 except ValueError:
110 result = None
111 return result
112 wrapper.__name__ = "{}.parser[{}]".format(type(self).__name__, tag)
113 return wrapper
114
115 def __generate_parsers(self):
116 """Generate parsers for all support DataType"""
117 self.parsers = {}
118 for data_type, _ in self.PEM_TAGS.items():
119 self.parsers[data_type] = self.__generate_parser(data_type)
120
121 def __getitem__(self, item):
122 return self.parsers[item]
123
124 @staticmethod
125 def pem_data_type(data: bytes) -> str:
126 """Get the tag from the data in PEM format
127
128 :param data: data to be checked in binary mode.
129 :return: PEM tag or "" when no tag detected.
130 """
131 m = re.search(X509Parser.PEM_TAG_REGEX, data)
132 if m is not None:
133 return m.group('type').decode('UTF-8')
134 else:
135 return ""
136
Pengyu Lv30f26832023-04-07 18:04:07 +0800137 @staticmethod
138 def check_hex_string(hex_str: str) -> bool:
139 """Check if the hex string is possibly DER data."""
140 hex_len = len(hex_str)
141 # At least 6 hex char for 3 bytes: Type + Length + Content
142 if hex_len < 6:
143 return False
144 # Check if Type (1 byte) is SEQUENCE.
145 if hex_str[0:2] != '30':
146 return False
147 # Check LENGTH (1 byte) value
148 content_len = int(hex_str[2:4], base=16)
149 consumed = 4
150 if content_len in (128, 255):
151 # Indefinite or Reserved
152 return False
153 elif content_len > 127:
154 # Definite, Long
155 length_len = (content_len - 128) * 2
156 content_len = int(hex_str[consumed:consumed+length_len], base=16)
157 consumed += length_len
158 # Check LENGTH
159 if hex_len != content_len * 2 + consumed:
160 return False
161 return True
162
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800163class Auditor:
164 """A base class for audit."""
165 def __init__(self, verbose):
166 self.verbose = verbose
167 self.default_files = []
168 self.audit_data = []
169 self.parser = X509Parser({
170 DataType.CRT: {
171 DataFormat.PEM: x509.load_pem_x509_certificate,
172 DataFormat.DER: x509.load_der_x509_certificate
173 },
174 DataType.CRL: {
175 DataFormat.PEM: x509.load_pem_x509_crl,
176 DataFormat.DER: x509.load_der_x509_crl
177 },
178 DataType.CSR: {
179 DataFormat.PEM: x509.load_pem_x509_csr,
180 DataFormat.DER: x509.load_der_x509_csr
181 },
182 })
183
184 def error(self, *args):
185 #pylint: disable=no-self-use
186 print("Error: ", *args, file=sys.stderr)
187
188 def warn(self, *args):
189 if self.verbose:
190 print("Warn: ", *args, file=sys.stderr)
191
192 def parse_file(self, filename: str) -> typing.List[AuditData]:
193 """
194 Parse a list of AuditData from file.
195
196 :param filename: name of the file to parse.
197 :return list of AuditData parsed from the file.
198 """
199 with open(filename, 'rb') as f:
200 data = f.read()
201 result_list = []
202 result = self.parse_bytes(data)
203 if result is not None:
204 result.filename = filename
205 result_list.append(result)
206 return result_list
207
208 def parse_bytes(self, data: bytes):
209 """Parse AuditData from bytes."""
210 for data_type in list(DataType):
211 try:
212 result = self.parser[data_type](data)
213 except ValueError as val_error:
214 result = None
215 self.warn(val_error)
216 if result is not None:
Pengyu Lvcb8fc322023-04-11 15:05:29 +0800217 audit_data = AuditData(data_type, result)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800218 return audit_data
219 return None
220
221 def walk_all(self, file_list):
222 """
223 Iterate over all the files in the list and get audit data.
224 """
225 if not file_list:
226 file_list = self.default_files
227 for filename in file_list:
228 data_list = self.parse_file(filename)
229 self.audit_data.extend(data_list)
230
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800231 @staticmethod
232 def find_test_dir():
233 """Get the relative path for the MbedTLS test directory."""
234 if os.path.isdir('tests'):
235 tests_dir = 'tests'
236 elif os.path.isdir('suites'):
237 tests_dir = '.'
238 elif os.path.isdir('../suites'):
239 tests_dir = '..'
240 else:
241 raise Exception("Mbed TLS source tree not found")
242 return tests_dir
243
244class TestDataAuditor(Auditor):
245 """Class for auditing files in tests/data_files/"""
246 def __init__(self, verbose):
247 super().__init__(verbose)
248 self.default_files = self.collect_default_files()
249
250 def collect_default_files(self):
Pengyu Lv45e32032023-04-06 14:33:41 +0800251 """Collect all files in tests/data_files/"""
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800252 test_dir = self.find_test_dir()
253 test_data_folder = os.path.join(test_dir, 'data_files')
254 data_files = []
255 for (dir_path, _, file_names) in os.walk(test_data_folder):
256 data_files.extend(os.path.join(dir_path, file_name)
257 for file_name in file_names)
258 return data_files
259
Pengyu Lv30f26832023-04-07 18:04:07 +0800260class FileWrapper():
261 """
262 This a stub class of generate_test_code.FileWrapper.
263
264 This class reads the whole file to memory before iterating
265 over the lines.
266 """
267
268 def __init__(self, file_name):
269 """
270 Read the file and initialize the line number to 0.
271
272 :param file_name: File path to open.
273 """
274 with open(file_name, 'rb') as f:
275 self.buf = f.read()
276 self.buf_len = len(self.buf)
277 self._line_no = 0
278 self._line_start = 0
279
280 def __iter__(self):
281 """Make the class iterable."""
282 return self
283
284 def __next__(self):
285 """
286 This method for returning a line of the file per iteration.
287
288 :return: Line read from file.
289 """
290 # If we reach the end of the file.
291 if not self._line_start < self.buf_len:
292 raise StopIteration
293
294 line_end = self.buf.find(b'\n', self._line_start) + 1
295 if line_end > 0:
296 # Find the first LF as the end of the new line.
297 line = self.buf[self._line_start:line_end]
298 self._line_start = line_end
299 self._line_no += 1
300 else:
301 # No LF found. We are at the last line without LF.
302 line = self.buf[self._line_start:]
303 self._line_start = self.buf_len
304 self._line_no += 1
305
306 # Convert byte array to string with correct encoding and
307 # strip any whitespaces added in the decoding process.
308 return line.decode(sys.getdefaultencoding()).rstrip() + '\n'
309
310 def get_line_no(self):
311 """
312 Gives current line number.
313 """
314 return self._line_no
315
316 line_no = property(get_line_no)
317
Pengyu Lv45e32032023-04-06 14:33:41 +0800318class SuiteDataAuditor(Auditor):
319 """Class for auditing files in tests/suites/*.data"""
320 def __init__(self, options):
321 super().__init__(options)
322 self.default_files = self.collect_default_files()
323
324 def collect_default_files(self):
325 """Collect all files in tests/suites/*.data"""
326 test_dir = self.find_test_dir()
327 suites_data_folder = os.path.join(test_dir, 'suites')
Pengyu Lv45e32032023-04-06 14:33:41 +0800328 data_files = glob.glob(os.path.join(suites_data_folder, '*.data'))
329 return data_files
330
331 def parse_file(self, filename: str):
Pengyu Lv30f26832023-04-07 18:04:07 +0800332 """
333 Parse a list of AuditData from file.
334
335 :param filename: name of the file to parse.
336 :return list of AuditData parsed from the file.
337 """
Pengyu Lv45e32032023-04-06 14:33:41 +0800338 audit_data_list = []
Pengyu Lv30f26832023-04-07 18:04:07 +0800339 data_f = FileWrapper(filename)
340 for _, _, _, test_args in parse_suite_data(data_f):
341 for test_arg in test_args:
342 match = re.match(r'"(?P<data>[0-9a-fA-F]+)"', test_arg)
343 if not match:
344 continue
345 if not X509Parser.check_hex_string(match.group('data')):
346 continue
347 audit_data = self.parse_bytes(bytes.fromhex(match.group('data')))
348 if audit_data is None:
349 continue
350 audit_data.filename = filename
351 audit_data_list.append(audit_data)
352
Pengyu Lv45e32032023-04-06 14:33:41 +0800353 return audit_data_list
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800354
355def list_all(audit_data: AuditData):
356 print("{}\t{}\t{}\t{}".format(
357 audit_data.not_valid_before.isoformat(timespec='seconds'),
358 audit_data.not_valid_after.isoformat(timespec='seconds'),
359 audit_data.data_type.name,
360 audit_data.filename))
361
362def main():
363 """
364 Perform argument parsing.
365 """
Pengyu Lv57240952023-04-13 14:42:37 +0800366 parser = argparse.ArgumentParser(description=__doc__)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800367
368 parser.add_argument('-a', '--all',
369 action='store_true',
Pengyu Lv57240952023-04-13 14:42:37 +0800370 help='list the information of all the files')
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800371 parser.add_argument('-v', '--verbose',
372 action='store_true', dest='verbose',
Pengyu Lv57240952023-04-13 14:42:37 +0800373 help='show warnings')
Pengyu Lvebf011f2023-04-11 13:39:31 +0800374 parser.add_argument('--not-before', dest='not_before',
Pengyu Lv57240952023-04-13 14:42:37 +0800375 help=('not valid before this date (UTC, YYYY-MM-DD). '
376 'Default: today'),
Pengyu Lvebf011f2023-04-11 13:39:31 +0800377 metavar='DATE')
378 parser.add_argument('--not-after', dest='not_after',
Pengyu Lv57240952023-04-13 14:42:37 +0800379 help=('not valid after this date (UTC, YYYY-MM-DD). '
380 'Default: not-before'),
Pengyu Lvebf011f2023-04-11 13:39:31 +0800381 metavar='DATE')
Pengyu Lv57240952023-04-13 14:42:37 +0800382 parser.add_argument('files', nargs='*', help='files to audit',
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800383 metavar='FILE')
384
385 args = parser.parse_args()
386
387 # start main routine
388 td_auditor = TestDataAuditor(args.verbose)
Pengyu Lv45e32032023-04-06 14:33:41 +0800389 sd_auditor = SuiteDataAuditor(args.verbose)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800390
Pengyu Lv57240952023-04-13 14:42:37 +0800391 if args.files:
392 data_files = args.files
393 suite_data_files = args.files
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800394 else:
395 data_files = td_auditor.default_files
Pengyu Lv45e32032023-04-06 14:33:41 +0800396 suite_data_files = sd_auditor.default_files
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800397
Pengyu Lvebf011f2023-04-11 13:39:31 +0800398 if args.not_before:
399 not_before_date = datetime.datetime.fromisoformat(args.not_before)
400 else:
401 not_before_date = datetime.datetime.today()
402 if args.not_after:
403 not_after_date = datetime.datetime.fromisoformat(args.not_after)
404 else:
405 not_after_date = not_before_date
406
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800407 td_auditor.walk_all(data_files)
Pengyu Lv45e32032023-04-06 14:33:41 +0800408 sd_auditor.walk_all(suite_data_files)
Pengyu Lvebf011f2023-04-11 13:39:31 +0800409 audit_results = td_auditor.audit_data + sd_auditor.audit_data
410
Pengyu Lv57240952023-04-13 14:42:37 +0800411 # we filter out the files whose validity duration covers the provided
Pengyu Lvebf011f2023-04-11 13:39:31 +0800412 # duration.
413 filter_func = lambda d: (not_before_date < d.not_valid_before) or \
414 (d.not_valid_after < not_after_date)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800415
416 if args.all:
Pengyu Lvebf011f2023-04-11 13:39:31 +0800417 filter_func = None
418
419 for d in filter(filter_func, audit_results):
420 list_all(d)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800421
422 print("\nDone!\n")
423
424if __name__ == "__main__":
425 main()