Bob Beck | bc97b7a | 2023-04-18 08:35:15 -0600 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2015 The Chromium Authors |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """This script is called without any arguments to re-format all of the *.pem |
| 7 | files in the script's parent directory. |
| 8 | |
| 9 | The main formatting change is to run "openssl asn1parse" for each of the PEM |
| 10 | block sections (except for DATA), and add that output to the comment. |
| 11 | |
| 12 | Refer to the README file for more information. |
| 13 | """ |
| 14 | |
| 15 | import glob |
| 16 | import os |
| 17 | import re |
| 18 | import base64 |
| 19 | import subprocess |
| 20 | |
| 21 | |
| 22 | def Transform(file_data): |
| 23 | """Returns a transformed (formatted) version of file_data""" |
| 24 | |
| 25 | result = '' |
| 26 | |
| 27 | for block in GetPemBlocks(file_data): |
| 28 | if len(result) != 0: |
| 29 | result += '\n' |
| 30 | |
| 31 | # If there was a user comment (non-script-generated comment) associated |
| 32 | # with the block, output it immediately before the block. |
| 33 | user_comment = GetUserComment(block.comment) |
| 34 | if user_comment: |
| 35 | result += user_comment |
| 36 | |
| 37 | # For every block except for DATA, try to pretty print the parsed ASN.1. |
| 38 | # DATA blocks likely would be DER in practice, but for the purposes of |
| 39 | # these tests seeing its structure doesn't clarify |
| 40 | # anything and is just a distraction. |
| 41 | if block.name != 'DATA': |
| 42 | generated_comment = GenerateCommentForBlock(block.name, block.data) |
| 43 | result += generated_comment + '\n' |
| 44 | |
| 45 | |
| 46 | result += MakePemBlockString(block.name, block.data) |
| 47 | |
| 48 | return result |
| 49 | |
| 50 | |
| 51 | def GenerateCommentForBlock(block_name, block_data): |
| 52 | """Returns a string describing the ASN.1 structure of block_data""" |
| 53 | |
| 54 | p = subprocess.Popen(['openssl', 'asn1parse', '-i', '-inform', 'DER'], |
| 55 | stdout=subprocess.PIPE, stdin=subprocess.PIPE, |
| 56 | stderr=subprocess.PIPE) |
| 57 | stdout_data, stderr_data = p.communicate(input=block_data) |
| 58 | generated_comment = '$ openssl asn1parse -i < [%s]\n%s' % (block_name, |
| 59 | stdout_data) |
| 60 | return generated_comment.strip('\n') |
| 61 | |
| 62 | |
| 63 | |
| 64 | def GetUserComment(comment): |
| 65 | """Removes any script-generated lines (everything after the $ openssl line)""" |
| 66 | |
| 67 | # Consider everything after "$ openssl" to be a generated comment. |
| 68 | comment = comment.split('$ openssl asn1parse -i', 1)[0] |
| 69 | if IsEntirelyWhiteSpace(comment): |
| 70 | comment = '' |
| 71 | return comment |
| 72 | |
| 73 | |
| 74 | def MakePemBlockString(name, data): |
| 75 | return ('-----BEGIN %s-----\n' |
| 76 | '%s' |
| 77 | '-----END %s-----\n') % (name, EncodeDataForPem(data), name) |
| 78 | |
| 79 | |
| 80 | def GetPemFilePaths(): |
| 81 | """Returns an iterable for all the paths to the PEM test files""" |
| 82 | |
| 83 | base_dir = os.path.dirname(os.path.realpath(__file__)) |
| 84 | return glob.iglob(os.path.join(base_dir, '*.pem')) |
| 85 | |
| 86 | |
| 87 | def ReadFileToString(path): |
| 88 | with open(path, 'r') as f: |
| 89 | return f.read() |
| 90 | |
| 91 | |
| 92 | def WrapTextToLineWidth(text, column_width): |
| 93 | result = '' |
| 94 | pos = 0 |
| 95 | while pos < len(text): |
| 96 | result += text[pos : pos + column_width] + '\n' |
| 97 | pos += column_width |
| 98 | return result |
| 99 | |
| 100 | |
| 101 | def EncodeDataForPem(data): |
| 102 | result = base64.b64encode(data) |
| 103 | return WrapTextToLineWidth(result, 75) |
| 104 | |
| 105 | |
| 106 | class PemBlock(object): |
| 107 | def __init__(self): |
| 108 | self.name = None |
| 109 | self.data = None |
| 110 | self.comment = None |
| 111 | |
| 112 | |
| 113 | def StripAllWhitespace(text): |
| 114 | pattern = re.compile(r'\s+') |
| 115 | return re.sub(pattern, '', text) |
| 116 | |
| 117 | |
| 118 | def IsEntirelyWhiteSpace(text): |
| 119 | return len(StripAllWhitespace(text)) == 0 |
| 120 | |
| 121 | |
| 122 | def DecodePemBlockData(text): |
| 123 | text = StripAllWhitespace(text) |
| 124 | return base64.b64decode(text) |
| 125 | |
| 126 | |
| 127 | def GetPemBlocks(data): |
| 128 | """Returns an iterable of PemBlock""" |
| 129 | |
| 130 | comment_start = 0 |
| 131 | |
| 132 | regex = re.compile(r'-----BEGIN ([\w ]+)-----(.*?)-----END \1-----', |
| 133 | re.DOTALL) |
| 134 | |
| 135 | for match in regex.finditer(data): |
| 136 | block = PemBlock() |
| 137 | |
| 138 | block.name = match.group(1) |
| 139 | block.data = DecodePemBlockData(match.group(2)) |
| 140 | |
| 141 | # Keep track of any non-PEM text above blocks |
| 142 | block.comment = data[comment_start : match.start()].strip() |
| 143 | comment_start = match.end() |
| 144 | |
| 145 | yield block |
| 146 | |
| 147 | |
| 148 | def WriteStringToFile(data, path): |
| 149 | with open(path, "w") as f: |
| 150 | f.write(data) |
| 151 | |
| 152 | |
| 153 | def main(): |
| 154 | for path in GetPemFilePaths(): |
| 155 | print "Processing %s ..." % (path) |
| 156 | original_data = ReadFileToString(path) |
| 157 | transformed_data = Transform(original_data) |
| 158 | if original_data != transformed_data: |
| 159 | WriteStringToFile(transformed_data, path) |
| 160 | print "Rewrote %s" % (path) |
| 161 | |
| 162 | |
| 163 | if __name__ == "__main__": |
| 164 | main() |