blob: 0d1425b28ba9737fb326711a3cc5b3b586ed70f3 [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
29import types
30import argparse
31import datetime
Pengyu Lv45e32032023-04-06 14:33:41 +080032import glob
Pengyu Lv7f6933a2023-04-04 16:05:54 +080033from enum import Enum
34
35from cryptography import x509
36
37class DataType(Enum):
38 CRT = 1 # Certificate
39 CRL = 2 # Certificate Revocation List
40 CSR = 3 # Certificate Signing Request
41
42class DataFormat(Enum):
43 PEM = 1 # Privacy-Enhanced Mail
44 DER = 2 # Distinguished Encoding Rules
45
46class AuditData:
47 """Store file, type and expiration date for audit."""
48 #pylint: disable=too-few-public-methods
49 def __init__(self, data_type: DataType):
50 self.data_type = data_type
51 self.filename = ""
52 self.not_valid_after: datetime.datetime
53 self.not_valid_before: datetime.datetime
54
55 def fill_validity_duration(self, x509_obj):
56 """Fill expiration_date field from a x509 object"""
57 # Certificate expires after "not_valid_after"
58 # Certificate is invalid before "not_valid_before"
59 if self.data_type == DataType.CRT:
60 self.not_valid_after = x509_obj.not_valid_after
61 self.not_valid_before = x509_obj.not_valid_before
62 # CertificateRevocationList expires after "next_update"
63 # CertificateRevocationList is invalid before "last_update"
64 elif self.data_type == DataType.CRL:
65 self.not_valid_after = x509_obj.next_update
66 self.not_valid_before = x509_obj.last_update
67 # CertificateSigningRequest is always valid.
68 elif self.data_type == DataType.CSR:
69 self.not_valid_after = datetime.datetime.max
70 self.not_valid_before = datetime.datetime.min
71 else:
72 raise ValueError("Unsupported file_type: {}".format(self.data_type))
73
74class X509Parser():
75 """A parser class to parse crt/crl/csr file or data in PEM/DER format."""
76 PEM_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n(?P<data>.*?)-{5}END (?P=type)-{5}\n'
77 PEM_TAG_REGEX = br'-{5}BEGIN (?P<type>.*?)-{5}\n'
78 PEM_TAGS = {
79 DataType.CRT: 'CERTIFICATE',
80 DataType.CRL: 'X509 CRL',
81 DataType.CSR: 'CERTIFICATE REQUEST'
82 }
83
84 def __init__(self, backends: dict):
85 self.backends = backends
86 self.__generate_parsers()
87
88 def __generate_parser(self, data_type: DataType):
89 """Parser generator for a specific DataType"""
90 tag = self.PEM_TAGS[data_type]
91 pem_loader = self.backends[data_type][DataFormat.PEM]
92 der_loader = self.backends[data_type][DataFormat.DER]
93 def wrapper(data: bytes):
94 pem_type = X509Parser.pem_data_type(data)
95 # It is in PEM format with target tag
96 if pem_type == tag:
97 return pem_loader(data)
98 # It is in PEM format without target tag
99 if pem_type:
100 return None
101 # It might be in DER format
102 try:
103 result = der_loader(data)
104 except ValueError:
105 result = None
106 return result
107 wrapper.__name__ = "{}.parser[{}]".format(type(self).__name__, tag)
108 return wrapper
109
110 def __generate_parsers(self):
111 """Generate parsers for all support DataType"""
112 self.parsers = {}
113 for data_type, _ in self.PEM_TAGS.items():
114 self.parsers[data_type] = self.__generate_parser(data_type)
115
116 def __getitem__(self, item):
117 return self.parsers[item]
118
119 @staticmethod
120 def pem_data_type(data: bytes) -> str:
121 """Get the tag from the data in PEM format
122
123 :param data: data to be checked in binary mode.
124 :return: PEM tag or "" when no tag detected.
125 """
126 m = re.search(X509Parser.PEM_TAG_REGEX, data)
127 if m is not None:
128 return m.group('type').decode('UTF-8')
129 else:
130 return ""
131
132class Auditor:
133 """A base class for audit."""
134 def __init__(self, verbose):
135 self.verbose = verbose
136 self.default_files = []
137 self.audit_data = []
138 self.parser = X509Parser({
139 DataType.CRT: {
140 DataFormat.PEM: x509.load_pem_x509_certificate,
141 DataFormat.DER: x509.load_der_x509_certificate
142 },
143 DataType.CRL: {
144 DataFormat.PEM: x509.load_pem_x509_crl,
145 DataFormat.DER: x509.load_der_x509_crl
146 },
147 DataType.CSR: {
148 DataFormat.PEM: x509.load_pem_x509_csr,
149 DataFormat.DER: x509.load_der_x509_csr
150 },
151 })
152
153 def error(self, *args):
154 #pylint: disable=no-self-use
155 print("Error: ", *args, file=sys.stderr)
156
157 def warn(self, *args):
158 if self.verbose:
159 print("Warn: ", *args, file=sys.stderr)
160
161 def parse_file(self, filename: str) -> typing.List[AuditData]:
162 """
163 Parse a list of AuditData from file.
164
165 :param filename: name of the file to parse.
166 :return list of AuditData parsed from the file.
167 """
168 with open(filename, 'rb') as f:
169 data = f.read()
170 result_list = []
171 result = self.parse_bytes(data)
172 if result is not None:
173 result.filename = filename
174 result_list.append(result)
175 return result_list
176
177 def parse_bytes(self, data: bytes):
178 """Parse AuditData from bytes."""
179 for data_type in list(DataType):
180 try:
181 result = self.parser[data_type](data)
182 except ValueError as val_error:
183 result = None
184 self.warn(val_error)
185 if result is not None:
186 audit_data = AuditData(data_type)
187 audit_data.fill_validity_duration(result)
188 return audit_data
189 return None
190
191 def walk_all(self, file_list):
192 """
193 Iterate over all the files in the list and get audit data.
194 """
195 if not file_list:
196 file_list = self.default_files
197 for filename in file_list:
198 data_list = self.parse_file(filename)
199 self.audit_data.extend(data_list)
200
201 def for_each(self, do, *args, **kwargs):
202 """
203 Sort the audit data and iterate over them.
204 """
205 if not isinstance(do, types.FunctionType):
206 return
207 for d in self.audit_data:
208 do(d, *args, **kwargs)
209
210 @staticmethod
211 def find_test_dir():
212 """Get the relative path for the MbedTLS test directory."""
213 if os.path.isdir('tests'):
214 tests_dir = 'tests'
215 elif os.path.isdir('suites'):
216 tests_dir = '.'
217 elif os.path.isdir('../suites'):
218 tests_dir = '..'
219 else:
220 raise Exception("Mbed TLS source tree not found")
221 return tests_dir
222
223class TestDataAuditor(Auditor):
224 """Class for auditing files in tests/data_files/"""
225 def __init__(self, verbose):
226 super().__init__(verbose)
227 self.default_files = self.collect_default_files()
228
229 def collect_default_files(self):
Pengyu Lv45e32032023-04-06 14:33:41 +0800230 """Collect all files in tests/data_files/"""
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800231 test_dir = self.find_test_dir()
232 test_data_folder = os.path.join(test_dir, 'data_files')
233 data_files = []
234 for (dir_path, _, file_names) in os.walk(test_data_folder):
235 data_files.extend(os.path.join(dir_path, file_name)
236 for file_name in file_names)
237 return data_files
238
Pengyu Lv45e32032023-04-06 14:33:41 +0800239class SuiteDataAuditor(Auditor):
240 """Class for auditing files in tests/suites/*.data"""
241 def __init__(self, options):
242 super().__init__(options)
243 self.default_files = self.collect_default_files()
244
245 def collect_default_files(self):
246 """Collect all files in tests/suites/*.data"""
247 test_dir = self.find_test_dir()
248 suites_data_folder = os.path.join(test_dir, 'suites')
249 # collect all data files in tests/suites (114 in total)
250 data_files = glob.glob(os.path.join(suites_data_folder, '*.data'))
251 return data_files
252
253 def parse_file(self, filename: str):
254 """Parse AuditData from file."""
255 with open(filename, 'r') as f:
256 data = f.read()
257 audit_data_list = []
258 # extract hex strings from the data file.
259 hex_strings = re.findall(r'"(?P<data>[0-9a-fA-F]+)"', data)
260 for hex_str in hex_strings:
261 # We regard hex string with odd number length as invaild data.
262 if len(hex_str) & 1:
263 continue
264 bytes_data = bytes.fromhex(hex_str)
265 audit_data = self.parse_bytes(bytes_data)
266 if audit_data is None:
267 continue
268 audit_data.filename = filename
269 audit_data_list.append(audit_data)
270 return audit_data_list
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800271
272def list_all(audit_data: AuditData):
273 print("{}\t{}\t{}\t{}".format(
274 audit_data.not_valid_before.isoformat(timespec='seconds'),
275 audit_data.not_valid_after.isoformat(timespec='seconds'),
276 audit_data.data_type.name,
277 audit_data.filename))
278
279def main():
280 """
281 Perform argument parsing.
282 """
283 parser = argparse.ArgumentParser(
284 description='Audit script for X509 crt/crl/csr files.'
285 )
286
287 parser.add_argument('-a', '--all',
288 action='store_true',
289 help='list the information of all files')
290 parser.add_argument('-v', '--verbose',
291 action='store_true', dest='verbose',
292 help='Show warnings')
293 parser.add_argument('-f', '--file', dest='file',
294 help='file to audit (Debug only)',
295 metavar='FILE')
296
297 args = parser.parse_args()
298
299 # start main routine
300 td_auditor = TestDataAuditor(args.verbose)
Pengyu Lv45e32032023-04-06 14:33:41 +0800301 sd_auditor = SuiteDataAuditor(args.verbose)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800302
303 if args.file:
304 data_files = [args.file]
Pengyu Lv45e32032023-04-06 14:33:41 +0800305 suite_data_files = [args.file]
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800306 else:
307 data_files = td_auditor.default_files
Pengyu Lv45e32032023-04-06 14:33:41 +0800308 suite_data_files = sd_auditor.default_files
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800309
310 td_auditor.walk_all(data_files)
Pengyu Lv45e32032023-04-06 14:33:41 +0800311 # TODO: Improve the method for auditing test suite data files
312 # It takes 6 times longer than td_auditor.walk_all(),
313 # typically 0.827 s VS 0.147 s.
314 sd_auditor.walk_all(suite_data_files)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800315
316 if args.all:
317 td_auditor.for_each(list_all)
Pengyu Lv45e32032023-04-06 14:33:41 +0800318 sd_auditor.for_each(list_all)
Pengyu Lv7f6933a2023-04-04 16:05:54 +0800319
320 print("\nDone!\n")
321
322if __name__ == "__main__":
323 main()