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