blob: 472041e168860a5759ddbdd891f8636f29b346d5 [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
18"""Audit validity date of X509 crt/crl/csr
19
20This script is used to audit the validity date of crt/crl/csr used for testing.
21The files are in tests/data_files/ while some data are in test suites data in
22tests/suites/*.data files.
23"""
24
25import os
26import sys
27import re
28import typing
Pengyu Lv7f6933a2023-04-04 16:05:54 +080029import argparse
30import datetime
Pengyu Lv45e32032023-04-06 14:33:41 +080031import glob
Pengyu Lv7f6933a2023-04-04 16:05:54 +080032from enum import Enum
33
34from cryptography import x509
35
Pengyu Lv30f26832023-04-07 18:04:07 +080036# reuse the function to parse *.data file in tests/suites/
37from generate_test_code import parse_test_data as parse_suite_data
38
Pengyu Lv7f6933a2023-04-04 16:05:54 +080039class DataType(Enum):
40 CRT = 1 # Certificate
41 CRL = 2 # Certificate Revocation List
42 CSR = 3 # Certificate Signing Request
43
44class DataFormat(Enum):
45 PEM = 1 # Privacy-Enhanced Mail
46 DER = 2 # Distinguished Encoding Rules
47
48class AuditData:
49 """Store file, type and expiration date for audit."""
50 #pylint: disable=too-few-public-methods
Pengyu Lvcb8fc322023-04-11 15:05:29 +080051 def __init__(self, data_type: DataType, x509_obj):
Pengyu Lv7f6933a2023-04-04 16:05:54 +080052 self.data_type = data_type
53 self.filename = ""
Pengyu Lvcb8fc322023-04-11 15:05:29 +080054 self.fill_validity_duration(x509_obj)
Pengyu Lv7f6933a2023-04-04 16:05:54 +080055
56 def fill_validity_duration(self, x509_obj):
57 """Fill expiration_date field from a x509 object"""
58 # Certificate expires after "not_valid_after"
59 # Certificate is invalid before "not_valid_before"
60 if self.data_type == DataType.CRT:
61 self.not_valid_after = x509_obj.not_valid_after
62 self.not_valid_before = x509_obj.not_valid_before
63 # CertificateRevocationList expires after "next_update"
64 # CertificateRevocationList is invalid before "last_update"
65 elif self.data_type == DataType.CRL:
66 self.not_valid_after = x509_obj.next_update
67 self.not_valid_before = x509_obj.last_update
68 # CertificateSigningRequest is always valid.
69 elif self.data_type == DataType.CSR:
70 self.not_valid_after = datetime.datetime.max
71 self.not_valid_before = datetime.datetime.min
72 else:
73 raise ValueError("Unsupported file_type: {}".format(self.data_type))
74
75class X509Parser():
76 """A parser class to parse crt/crl/csr file or data in PEM/DER format."""
77 PEM_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n(?P<data>.*?)-{5}END (?P=type)-{5}\n'
78 PEM_TAG_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n'
79 PEM_TAGS = {
80 DataType.CRT: 'CERTIFICATE',
81 DataType.CRL: 'X509 CRL',
82 DataType.CSR: 'CERTIFICATE REQUEST'
83 }
84
85 def __init__(self, backends: dict):
86 self.backends = backends
87 self.__generate_parsers()
88
89 def __generate_parser(self, data_type: DataType):
90 """Parser generator for a specific DataType"""
91 tag = self.PEM_TAGS[data_type]
92 pem_loader = self.backends[data_type][DataFormat.PEM]
93 der_loader = self.backends[data_type][DataFormat.DER]
94 def wrapper(data: bytes):
95 pem_type = X509Parser.pem_data_type(data)
96 # It is in PEM format with target tag
97 if pem_type == tag:
98 return pem_loader(data)
99 # It is in PEM format without target tag
100 if pem_type:
101 return None
102 # It might be in DER format
103 try:
104 result = der_loader(data)
105 except ValueError:
106 result = None
107 return result
108 wrapper.__name__ = "{}.parser[{}]".format(type(self).__name__, tag)
109 return wrapper
110
111 def __generate_parsers(self):
112 """Generate parsers for all support DataType"""
113 self.parsers = {}
114 for data_type, _ in self.PEM_TAGS.items():
115 self.parsers[data_type] = self.__generate_parser(data_type)
116
117 def __getitem__(self, item):
118 return self.parsers[item]
119
120 @staticmethod
121 def pem_data_type(data: bytes) -> str:
122 """Get the tag from the data in PEM format
123
124 :param data: data to be checked in binary mode.
125 :return: PEM tag or "" when no tag detected.
126 """
127 m = re.search(X509Parser.PEM_TAG_REGEX, data)
128 if m is not None:
129 return m.group('type').decode('UTF-8')
130 else:
131 return ""
132
Pengyu Lv30f26832023-04-07 18:04:07 +0800133 @staticmethod
134 def check_hex_string(hex_str: str) -> bool:
135 """Check if the hex string is possibly DER data."""
136 hex_len = len(hex_str)
137 # At least 6 hex char for 3 bytes: Type + Length + Content
138 if hex_len < 6:
139 return False
140 # Check if Type (1 byte) is SEQUENCE.
141 if hex_str[0:2] != '30':
142 return False
143 # Check LENGTH (1 byte) value
144 content_len = int(hex_str[2:4], base=16)
145 consumed = 4
146 if content_len in (128, 255):
147 # Indefinite or Reserved
148 return False
149 elif content_len > 127:
150 # Definite, Long
151 length_len = (content_len - 128) * 2
152 content_len = int(hex_str[consumed:consumed+length_len], base=16)
153 consumed += length_len
154 # Check LENGTH
155 if hex_len != content_len * 2 + consumed:
156 return False
157 return True
158
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800159class Auditor:
160 """A base class for audit."""
161 def __init__(self, verbose):
162 self.verbose = verbose
163 self.default_files = []
164 self.audit_data = []
165 self.parser = X509Parser({
166 DataType.CRT: {
167 DataFormat.PEM: x509.load_pem_x509_certificate,
168 DataFormat.DER: x509.load_der_x509_certificate
169 },
170 DataType.CRL: {
171 DataFormat.PEM: x509.load_pem_x509_crl,
172 DataFormat.DER: x509.load_der_x509_crl
173 },
174 DataType.CSR: {
175 DataFormat.PEM: x509.load_pem_x509_csr,
176 DataFormat.DER: x509.load_der_x509_csr
177 },
178 })
179
180 def error(self, *args):
181 #pylint: disable=no-self-use
182 print("Error: ", *args, file=sys.stderr)
183
184 def warn(self, *args):
185 if self.verbose:
186 print("Warn: ", *args, file=sys.stderr)
187
188 def parse_file(self, filename: str) -> typing.List[AuditData]:
189 """
190 Parse a list of AuditData from file.
191
192 :param filename: name of the file to parse.
193 :return list of AuditData parsed from the file.
194 """
195 with open(filename, 'rb') as f:
196 data = f.read()
197 result_list = []
198 result = self.parse_bytes(data)
199 if result is not None:
200 result.filename = filename
201 result_list.append(result)
202 return result_list
203
204 def parse_bytes(self, data: bytes):
205 """Parse AuditData from bytes."""
206 for data_type in list(DataType):
207 try:
208 result = self.parser[data_type](data)
209 except ValueError as val_error:
210 result = None
211 self.warn(val_error)
212 if result is not None:
Pengyu Lvcb8fc322023-04-11 15:05:29 +0800213 audit_data = AuditData(data_type, result)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800214 return audit_data
215 return None
216
217 def walk_all(self, file_list):
218 """
219 Iterate over all the files in the list and get audit data.
220 """
221 if not file_list:
222 file_list = self.default_files
223 for filename in file_list:
224 data_list = self.parse_file(filename)
225 self.audit_data.extend(data_list)
226
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800227 @staticmethod
228 def find_test_dir():
229 """Get the relative path for the MbedTLS test directory."""
230 if os.path.isdir('tests'):
231 tests_dir = 'tests'
232 elif os.path.isdir('suites'):
233 tests_dir = '.'
234 elif os.path.isdir('../suites'):
235 tests_dir = '..'
236 else:
237 raise Exception("Mbed TLS source tree not found")
238 return tests_dir
239
240class TestDataAuditor(Auditor):
241 """Class for auditing files in tests/data_files/"""
242 def __init__(self, verbose):
243 super().__init__(verbose)
244 self.default_files = self.collect_default_files()
245
246 def collect_default_files(self):
Pengyu Lv45e32032023-04-06 14:33:41 +0800247 """Collect all files in tests/data_files/"""
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800248 test_dir = self.find_test_dir()
249 test_data_folder = os.path.join(test_dir, 'data_files')
250 data_files = []
251 for (dir_path, _, file_names) in os.walk(test_data_folder):
252 data_files.extend(os.path.join(dir_path, file_name)
253 for file_name in file_names)
254 return data_files
255
Pengyu Lv30f26832023-04-07 18:04:07 +0800256class FileWrapper():
257 """
258 This a stub class of generate_test_code.FileWrapper.
259
260 This class reads the whole file to memory before iterating
261 over the lines.
262 """
263
264 def __init__(self, file_name):
265 """
266 Read the file and initialize the line number to 0.
267
268 :param file_name: File path to open.
269 """
270 with open(file_name, 'rb') as f:
271 self.buf = f.read()
272 self.buf_len = len(self.buf)
273 self._line_no = 0
274 self._line_start = 0
275
276 def __iter__(self):
277 """Make the class iterable."""
278 return self
279
280 def __next__(self):
281 """
282 This method for returning a line of the file per iteration.
283
284 :return: Line read from file.
285 """
286 # If we reach the end of the file.
287 if not self._line_start < self.buf_len:
288 raise StopIteration
289
290 line_end = self.buf.find(b'\n', self._line_start) + 1
291 if line_end > 0:
292 # Find the first LF as the end of the new line.
293 line = self.buf[self._line_start:line_end]
294 self._line_start = line_end
295 self._line_no += 1
296 else:
297 # No LF found. We are at the last line without LF.
298 line = self.buf[self._line_start:]
299 self._line_start = self.buf_len
300 self._line_no += 1
301
302 # Convert byte array to string with correct encoding and
303 # strip any whitespaces added in the decoding process.
304 return line.decode(sys.getdefaultencoding()).rstrip() + '\n'
305
306 def get_line_no(self):
307 """
308 Gives current line number.
309 """
310 return self._line_no
311
312 line_no = property(get_line_no)
313
Pengyu Lv45e32032023-04-06 14:33:41 +0800314class SuiteDataAuditor(Auditor):
315 """Class for auditing files in tests/suites/*.data"""
316 def __init__(self, options):
317 super().__init__(options)
318 self.default_files = self.collect_default_files()
319
320 def collect_default_files(self):
321 """Collect all files in tests/suites/*.data"""
322 test_dir = self.find_test_dir()
323 suites_data_folder = os.path.join(test_dir, 'suites')
Pengyu Lv45e32032023-04-06 14:33:41 +0800324 data_files = glob.glob(os.path.join(suites_data_folder, '*.data'))
325 return data_files
326
327 def parse_file(self, filename: str):
Pengyu Lv30f26832023-04-07 18:04:07 +0800328 """
329 Parse a list of AuditData from file.
330
331 :param filename: name of the file to parse.
332 :return list of AuditData parsed from the file.
333 """
Pengyu Lv45e32032023-04-06 14:33:41 +0800334 audit_data_list = []
Pengyu Lv30f26832023-04-07 18:04:07 +0800335 data_f = FileWrapper(filename)
336 for _, _, _, test_args in parse_suite_data(data_f):
337 for test_arg in test_args:
338 match = re.match(r'"(?P<data>[0-9a-fA-F]+)"', test_arg)
339 if not match:
340 continue
341 if not X509Parser.check_hex_string(match.group('data')):
342 continue
343 audit_data = self.parse_bytes(bytes.fromhex(match.group('data')))
344 if audit_data is None:
345 continue
346 audit_data.filename = filename
347 audit_data_list.append(audit_data)
348
Pengyu Lv45e32032023-04-06 14:33:41 +0800349 return audit_data_list
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800350
351def list_all(audit_data: AuditData):
352 print("{}\t{}\t{}\t{}".format(
353 audit_data.not_valid_before.isoformat(timespec='seconds'),
354 audit_data.not_valid_after.isoformat(timespec='seconds'),
355 audit_data.data_type.name,
356 audit_data.filename))
357
358def main():
359 """
360 Perform argument parsing.
361 """
362 parser = argparse.ArgumentParser(
363 description='Audit script for X509 crt/crl/csr files.'
364 )
365
366 parser.add_argument('-a', '--all',
367 action='store_true',
368 help='list the information of all files')
369 parser.add_argument('-v', '--verbose',
370 action='store_true', dest='verbose',
371 help='Show warnings')
Pengyu Lvebf011f2023-04-11 13:39:31 +0800372 parser.add_argument('--not-before', dest='not_before',
373 help='not valid before this date(UTC), YYYY-MM-DD',
374 metavar='DATE')
375 parser.add_argument('--not-after', dest='not_after',
376 help='not valid after this date(UTC), YYYY-MM-DD',
377 metavar='DATE')
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800378 parser.add_argument('-f', '--file', dest='file',
379 help='file to audit (Debug only)',
380 metavar='FILE')
381
382 args = parser.parse_args()
383
384 # start main routine
385 td_auditor = TestDataAuditor(args.verbose)
Pengyu Lv45e32032023-04-06 14:33:41 +0800386 sd_auditor = SuiteDataAuditor(args.verbose)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800387
388 if args.file:
389 data_files = [args.file]
Pengyu Lv45e32032023-04-06 14:33:41 +0800390 suite_data_files = [args.file]
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800391 else:
392 data_files = td_auditor.default_files
Pengyu Lv45e32032023-04-06 14:33:41 +0800393 suite_data_files = sd_auditor.default_files
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800394
Pengyu Lvebf011f2023-04-11 13:39:31 +0800395 if args.not_before:
396 not_before_date = datetime.datetime.fromisoformat(args.not_before)
397 else:
398 not_before_date = datetime.datetime.today()
399 if args.not_after:
400 not_after_date = datetime.datetime.fromisoformat(args.not_after)
401 else:
402 not_after_date = not_before_date
403
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800404 td_auditor.walk_all(data_files)
Pengyu Lv45e32032023-04-06 14:33:41 +0800405 sd_auditor.walk_all(suite_data_files)
Pengyu Lvebf011f2023-04-11 13:39:31 +0800406 audit_results = td_auditor.audit_data + sd_auditor.audit_data
407
408 # we filter out the files whose validity duration covers the provide
409 # duration.
410 filter_func = lambda d: (not_before_date < d.not_valid_before) or \
411 (d.not_valid_after < not_after_date)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800412
413 if args.all:
Pengyu Lvebf011f2023-04-11 13:39:31 +0800414 filter_func = None
415
416 for d in filter(filter_func, audit_results):
417 list_all(d)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800418
419 print("\nDone!\n")
420
421if __name__ == "__main__":
422 main()