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