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