• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright 2016 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# TODO(svaldez): Deduplicate various annotate_test_data.
6
7"""This script is called without any arguments to re-format all of the *.pem
8files in the script's parent directory.
9
10The main formatting change is to run "openssl asn1parse" for each of the PEM
11block sections, and add that output to the comment. It also runs the command
12on the OCTET STRING representing BasicOCSPResponse.
13
14"""
15
16import glob
17import os
18import re
19import base64
20import subprocess
21
22
23def 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:
36      result += user_comment
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
47def GenerateCommentForBlock(block_name, block_data):
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)
72    p = subprocess.Popen(["openssl", "ocsp", "-noverify", "-resp_text",
73                          "-respin", tmp_file_path],
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)
88    print 'Error pretty printing OCSP response:\n',stderr_data
89
90  # Otherwise try pretty printing using asn1parse.
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
115def GetUserComment(comment):
116  """Removes any script-generated lines (everything after the $ openssl line)"""
117
118  # Consider everything after "$ openssl" to be a generated comment.
119  comment = comment.split('$ openssl', 1)[0]
120  if IsEntirelyWhiteSpace(comment):
121    comment = ''
122  elif not comment.endswith("\n\n"):
123    comment += "\n\n"
124  return comment
125
126
127def MakePemBlockString(name, data):
128  return ('-----BEGIN %s-----\n'
129          '%s'
130          '-----END %s-----\n') % (name, EncodeDataForPem(data), name)
131
132
133def 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
140def ReadFileToString(path):
141  with open(path, 'r') as f:
142    return f.read()
143
144
145def 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
154def EncodeDataForPem(data):
155  result = base64.b64encode(data)
156  return WrapTextToLineWidth(result, 75)
157
158
159class PemBlock(object):
160  def __init__(self):
161    self.name = None
162    self.data = None
163    self.comment = None
164
165
166def StripAllWhitespace(text):
167  pattern = re.compile(r'\s+')
168  return re.sub(pattern, '', text)
169
170
171def IsEntirelyWhiteSpace(text):
172  return len(StripAllWhitespace(text)) == 0
173
174
175def DecodePemBlockData(text):
176  text = StripAllWhitespace(text)
177  return base64.b64decode(text)
178
179
180def 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
201def WriteStringToFile(data, path):
202  with open(path, "w") as f:
203    f.write(data)
204
205
206def 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
216if __name__ == "__main__":
217  main()
218