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