Dirk Pranke | 4d164bb | 2021-03-24 06:52:40 | [diff] [blame] | 1 | #!/usr/bin/env python |
Avi Drissman | 6459548 | 2022-09-14 20:52:29 | [diff] [blame^] | 2 | # Copyright 2016 The Chromium Authors |
svaldez | be48178 | 2016-03-24 17:16:32 | [diff] [blame] | 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | # TODO(svaldez): Deduplicate various annotate_test_data. |
| 6 | |
| 7 | """This script is called without any arguments to re-format all of the *.pem |
| 8 | files in the script's parent directory. |
| 9 | |
| 10 | The main formatting change is to run "openssl asn1parse" for each of the PEM |
| 11 | block sections, and add that output to the comment. It also runs the command |
| 12 | on the OCTET STRING representing BasicOCSPResponse. |
| 13 | |
| 14 | """ |
| 15 | |
| 16 | import glob |
| 17 | import os |
| 18 | import re |
| 19 | import base64 |
| 20 | import subprocess |
| 21 | |
| 22 | |
| 23 | def Transform(file_data): |
| 24 | """Returns a transformed (formatted) version of file_data""" |
| 25 | |
| 26 | result = '' |
| 27 | |
| 28 | for block in GetPemBlocks(file_data): |
| 29 | if len(result) != 0: |
| 30 | result += '\n' |
| 31 | |
| 32 | # If there was a user comment (non-script-generated comment) associated |
| 33 | # with the block, output it immediately before the block. |
| 34 | user_comment = GetUserComment(block.comment) |
| 35 | if user_comment: |
Eric Roman | bce9c6b | 2017-09-26 00:02:15 | [diff] [blame] | 36 | result += user_comment |
svaldez | be48178 | 2016-03-24 17:16:32 | [diff] [blame] | 37 | |
| 38 | generated_comment = GenerateCommentForBlock(block.name, block.data) |
| 39 | result += generated_comment + '\n' |
| 40 | |
| 41 | |
| 42 | result += MakePemBlockString(block.name, block.data) |
| 43 | |
| 44 | return result |
| 45 | |
| 46 | |
| 47 | def GenerateCommentForBlock(block_name, block_data): |
Eric Roman | bce9c6b | 2017-09-26 00:02:15 | [diff] [blame] | 48 | """Returns a string describing |block_data|. The format of |block_data| is |
| 49 | inferred from |block_name|, and is pretty-printed accordingly. For |
| 50 | instance CERTIFICATE is understood to be an X.509 certificate and pretty |
| 51 | printed using OpenSSL's x509 command. If there is no specific pretty-printer |
| 52 | for the data type, it is annotated using "openssl asn1parse".""" |
| 53 | |
| 54 | # Try to pretty printing as X.509 certificate. |
| 55 | if "CERTIFICATE" in block_name: |
| 56 | p = subprocess.Popen(["openssl", "x509", "-text", "-noout", |
| 57 | "-inform", "DER"], |
| 58 | stdin=subprocess.PIPE, |
| 59 | stdout=subprocess.PIPE, |
| 60 | stderr=subprocess.PIPE) |
| 61 | stdout_data, stderr_data = p.communicate(block_data) |
| 62 | |
| 63 | # If pretty printing succeeded, return it. |
| 64 | if p.returncode == 0: |
| 65 | stdout_data = stdout_data.strip() |
| 66 | return '$ openssl x509 -text < [%s]\n%s' % (block_name, stdout_data) |
| 67 | |
| 68 | # Try pretty printing as OCSP Response. |
| 69 | if block_name == "OCSP RESPONSE": |
| 70 | tmp_file_path = "tmp_ocsp.der" |
| 71 | WriteStringToFile(block_data, tmp_file_path) |
Matt Mueller | f820ff01 | 2018-01-31 02:51:26 | [diff] [blame] | 72 | p = subprocess.Popen(["openssl", "ocsp", "-noverify", "-resp_text", |
| 73 | "-respin", tmp_file_path], |
Eric Roman | bce9c6b | 2017-09-26 00:02:15 | [diff] [blame] | 74 | stdin=subprocess.PIPE, |
| 75 | stdout=subprocess.PIPE, |
| 76 | stderr=subprocess.PIPE) |
| 77 | stdout_data, stderr_data = p.communicate(block_data) |
| 78 | os.remove(tmp_file_path) |
| 79 | |
| 80 | # If pretty printing succeeded, return it. |
| 81 | if p.returncode == 0: |
| 82 | stdout_data = stdout_data.strip() |
| 83 | # May contain embedded CERTIFICATE pem blocks. Escape these since |
| 84 | # CERTIFICATE already has meanining in the test file. |
| 85 | stdout_data = stdout_data.replace("-----", "~~~~~") |
| 86 | return '$ openssl ocsp -resp_text -respin <([%s])\n%s' % (block_name, |
| 87 | stdout_data) |
Matt Mueller | f820ff01 | 2018-01-31 02:51:26 | [diff] [blame] | 88 | print 'Error pretty printing OCSP response:\n',stderr_data |
Eric Roman | bce9c6b | 2017-09-26 00:02:15 | [diff] [blame] | 89 | |
| 90 | # Otherwise try pretty printing using asn1parse. |
svaldez | be48178 | 2016-03-24 17:16:32 | [diff] [blame] | 91 | |
| 92 | p = subprocess.Popen(['openssl', 'asn1parse', '-i', '-inform', 'DER'], |
| 93 | stdout=subprocess.PIPE, stdin=subprocess.PIPE, |
| 94 | stderr=subprocess.PIPE) |
| 95 | stdout_data, stderr_data = p.communicate(input=block_data) |
| 96 | generated_comment = '$ openssl asn1parse -i < [%s]\n%s' % (block_name, |
| 97 | stdout_data) |
| 98 | |
| 99 | # The OCTET STRING encoded BasicOCSPResponse is also parsed out using |
| 100 | #'openssl asn1parse'. |
| 101 | if block_name == 'OCSP RESPONSE': |
| 102 | if '[HEX DUMP]:' in generated_comment: |
| 103 | (generated_comment, response) = generated_comment.split('[HEX DUMP]:', 1) |
| 104 | response = response.replace('\n', '') |
| 105 | if len(response) % 2 != 0: |
| 106 | response = '0' + response |
| 107 | response = GenerateCommentForBlock('INNER', response.decode('hex')) |
| 108 | response = response.split('\n', 1)[1] |
| 109 | response = response.replace(': ', ': ') |
| 110 | generated_comment += '\n%s' % (response) |
| 111 | return generated_comment.strip('\n') |
| 112 | |
| 113 | |
| 114 | |
| 115 | def GetUserComment(comment): |
| 116 | """Removes any script-generated lines (everything after the $ openssl line)""" |
| 117 | |
| 118 | # Consider everything after "$ openssl" to be a generated comment. |
Eric Roman | bce9c6b | 2017-09-26 00:02:15 | [diff] [blame] | 119 | comment = comment.split('$ openssl', 1)[0] |
svaldez | be48178 | 2016-03-24 17:16:32 | [diff] [blame] | 120 | if IsEntirelyWhiteSpace(comment): |
| 121 | comment = '' |
Eric Roman | bc2c9db | 2017-09-26 00:11:55 | [diff] [blame] | 122 | elif not comment.endswith("\n\n"): |
| 123 | comment += "\n\n" |
svaldez | be48178 | 2016-03-24 17:16:32 | [diff] [blame] | 124 | return comment |
| 125 | |
| 126 | |
| 127 | def MakePemBlockString(name, data): |
| 128 | return ('-----BEGIN %s-----\n' |
| 129 | '%s' |
| 130 | '-----END %s-----\n') % (name, EncodeDataForPem(data), name) |
| 131 | |
| 132 | |
| 133 | def GetPemFilePaths(): |
| 134 | """Returns an iterable for all the paths to the PEM test files""" |
| 135 | |
| 136 | base_dir = os.path.dirname(os.path.realpath(__file__)) |
| 137 | return glob.iglob(os.path.join(base_dir, '*.pem')) |
| 138 | |
| 139 | |
| 140 | def ReadFileToString(path): |
| 141 | with open(path, 'r') as f: |
| 142 | return f.read() |
| 143 | |
| 144 | |
| 145 | def WrapTextToLineWidth(text, column_width): |
| 146 | result = '' |
| 147 | pos = 0 |
| 148 | while pos < len(text): |
| 149 | result += text[pos : pos + column_width] + '\n' |
| 150 | pos += column_width |
| 151 | return result |
| 152 | |
| 153 | |
| 154 | def EncodeDataForPem(data): |
| 155 | result = base64.b64encode(data) |
| 156 | return WrapTextToLineWidth(result, 75) |
| 157 | |
| 158 | |
| 159 | class PemBlock(object): |
| 160 | def __init__(self): |
| 161 | self.name = None |
| 162 | self.data = None |
| 163 | self.comment = None |
| 164 | |
| 165 | |
| 166 | def StripAllWhitespace(text): |
| 167 | pattern = re.compile(r'\s+') |
| 168 | return re.sub(pattern, '', text) |
| 169 | |
| 170 | |
| 171 | def IsEntirelyWhiteSpace(text): |
| 172 | return len(StripAllWhitespace(text)) == 0 |
| 173 | |
| 174 | |
| 175 | def DecodePemBlockData(text): |
| 176 | text = StripAllWhitespace(text) |
| 177 | return base64.b64decode(text) |
| 178 | |
| 179 | |
| 180 | def GetPemBlocks(data): |
| 181 | """Returns an iterable of PemBlock""" |
| 182 | |
| 183 | comment_start = 0 |
| 184 | |
| 185 | regex = re.compile(r'-----BEGIN ([\w ]+)-----(.*?)-----END \1-----', |
| 186 | re.DOTALL) |
| 187 | |
| 188 | for match in regex.finditer(data): |
| 189 | block = PemBlock() |
| 190 | |
| 191 | block.name = match.group(1) |
| 192 | block.data = DecodePemBlockData(match.group(2)) |
| 193 | |
| 194 | # Keep track of any non-PEM text above blocks |
| 195 | block.comment = data[comment_start : match.start()].strip() |
| 196 | comment_start = match.end() |
| 197 | |
| 198 | yield block |
| 199 | |
| 200 | |
| 201 | def WriteStringToFile(data, path): |
| 202 | with open(path, "w") as f: |
| 203 | f.write(data) |
| 204 | |
| 205 | |
| 206 | def main(): |
| 207 | for path in GetPemFilePaths(): |
| 208 | print "Processing %s ..." % (path) |
| 209 | original_data = ReadFileToString(path) |
| 210 | transformed_data = Transform(original_data) |
| 211 | if original_data != transformed_data: |
| 212 | WriteStringToFile(transformed_data, path) |
| 213 | print "Rewrote %s" % (path) |
| 214 | |
| 215 | |
| 216 | if __name__ == "__main__": |
| 217 | main() |