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