1 #!/usr/bin/env python 2 # 3 # Copyright (C) 2016 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 """ 18 Verify a given OTA package with the specifed certificate. 19 """ 20 21 from __future__ import print_function 22 23 import argparse 24 import logging 25 import re 26 import subprocess 27 import sys 28 import zipfile 29 from hashlib import sha1 30 from hashlib import sha256 31 32 import common 33 34 logger = logging.getLogger(__name__) 35 36 37 def CertUsesSha256(cert): 38 """Check if the cert uses SHA-256 hashing algorithm.""" 39 40 cmd = ['openssl', 'x509', '-text', '-noout', '-in', cert] 41 p1 = common.Run(cmd, stdout=subprocess.PIPE) 42 cert_dump, _ = p1.communicate() 43 44 algorithm = re.search(r'Signature Algorithm: ([a-zA-Z0-9]+)', cert_dump) 45 assert algorithm, "Failed to identify the signature algorithm." 46 47 assert not algorithm.group(1).startswith('ecdsa'), ( 48 'This script doesn\'t support verifying ECDSA signed package yet.') 49 50 return algorithm.group(1).startswith('sha256') 51 52 53 def VerifyPackage(cert, package): 54 """Verify the given package with the certificate. 55 56 (Comments from bootable/recovery/verifier.cpp:) 57 58 An archive with a whole-file signature will end in six bytes: 59 60 (2-byte signature start) $ff $ff (2-byte comment size) 61 62 (As far as the ZIP format is concerned, these are part of the 63 archive comment.) We start by reading this footer, this tells 64 us how far back from the end we have to start reading to find 65 the whole comment. 66 """ 67 68 print('Package: %s' % (package,)) 69 print('Certificate: %s' % (cert,)) 70 71 # Read in the package. 72 with open(package) as package_file: 73 package_bytes = package_file.read() 74 75 length = len(package_bytes) 76 assert length >= 6, "Not big enough to contain footer." 77 78 footer = [ord(x) for x in package_bytes[-6:]] 79 assert footer[2] == 0xff and footer[3] == 0xff, "Footer is wrong." 80 81 signature_start_from_end = (footer[1] << 8) + footer[0] 82 assert signature_start_from_end > 6, "Signature start is in the footer." 83 84 signature_start = length - signature_start_from_end 85 86 # Determine how much of the file is covered by the signature. This is 87 # everything except the signature data and length, which includes all of the 88 # EOCD except for the comment length field (2 bytes) and the comment data. 89 comment_len = (footer[5] << 8) + footer[4] 90 signed_len = length - comment_len - 2 91 92 print('Package length: %d' % (length,)) 93 print('Comment length: %d' % (comment_len,)) 94 print('Signed data length: %d' % (signed_len,)) 95 print('Signature start: %d' % (signature_start,)) 96 97 use_sha256 = CertUsesSha256(cert) 98 print('Use SHA-256: %s' % (use_sha256,)) 99 100 h = sha256() if use_sha256 else sha1() 101 h.update(package_bytes[:signed_len]) 102 package_digest = h.hexdigest().lower() 103 104 print('Digest: %s' % (package_digest,)) 105 106 # Get the signature from the input package. 107 signature = package_bytes[signature_start:-6] 108 sig_file = common.MakeTempFile(prefix='sig-') 109 with open(sig_file, 'wb') as f: 110 f.write(signature) 111 112 # Parse the signature and get the hash. 113 cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', sig_file] 114 p1 = common.Run(cmd, stdout=subprocess.PIPE) 115 sig, _ = p1.communicate() 116 assert p1.returncode == 0, "Failed to parse the signature." 117 118 digest_line = sig.strip().split('\n')[-1] 119 digest_string = digest_line.split(':')[3] 120 digest_file = common.MakeTempFile(prefix='digest-') 121 with open(digest_file, 'wb') as f: 122 f.write(digest_string.decode('hex')) 123 124 # Verify the digest by outputing the decrypted result in ASN.1 structure. 125 decrypted_file = common.MakeTempFile(prefix='decrypted-') 126 cmd = ['openssl', 'rsautl', '-verify', '-certin', '-inkey', cert, 127 '-in', digest_file, '-out', decrypted_file] 128 p1 = common.Run(cmd, stdout=subprocess.PIPE) 129 p1.communicate() 130 assert p1.returncode == 0, "Failed to run openssl rsautl -verify." 131 132 # Parse the output ASN.1 structure. 133 cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', decrypted_file] 134 p1 = common.Run(cmd, stdout=subprocess.PIPE) 135 decrypted_output, _ = p1.communicate() 136 assert p1.returncode == 0, "Failed to parse the output." 137 138 digest_line = decrypted_output.strip().split('\n')[-1] 139 digest_string = digest_line.split(':')[3].lower() 140 141 # Verify that the two digest strings match. 142 assert package_digest == digest_string, "Verification failed." 143 144 # Verified successfully upon reaching here. 145 print('\nWhole package signature VERIFIED\n') 146 147 148 def VerifyAbOtaPayload(cert, package): 149 """Verifies the payload and metadata signatures in an A/B OTA payload.""" 150 package_zip = zipfile.ZipFile(package, 'r') 151 if 'payload.bin' not in package_zip.namelist(): 152 common.ZipClose(package_zip) 153 return 154 155 print('Verifying A/B OTA payload signatures...') 156 157 # Dump pubkey from the certificate. 158 pubkey = common.MakeTempFile(prefix="key-", suffix=".pem") 159 with open(pubkey, 'wb') as pubkey_fp: 160 pubkey_fp.write(common.ExtractPublicKey(cert)) 161 162 package_dir = common.MakeTempDir(prefix='package-') 163 164 # Signature verification with delta_generator. 165 payload_file = package_zip.extract('payload.bin', package_dir) 166 cmd = ['delta_generator', 167 '--in_file=' + payload_file, 168 '--public_key=' + pubkey] 169 proc = common.Run(cmd) 170 stdoutdata, _ = proc.communicate() 171 assert proc.returncode == 0, \ 172 'Failed to verify payload with delta_generator: {}\n{}'.format( 173 package, stdoutdata) 174 common.ZipClose(package_zip) 175 176 # Verified successfully upon reaching here. 177 print('\nPayload signatures VERIFIED\n\n') 178 179 180 def main(): 181 parser = argparse.ArgumentParser() 182 parser.add_argument('certificate', help='The certificate to be used.') 183 parser.add_argument('package', help='The OTA package to be verified.') 184 args = parser.parse_args() 185 186 common.InitLogging() 187 188 VerifyPackage(args.certificate, args.package) 189 VerifyAbOtaPayload(args.certificate, args.package) 190 191 192 if __name__ == '__main__': 193 try: 194 main() 195 except AssertionError as err: 196 print('\n ERROR: %s\n' % (err,)) 197 sys.exit(1) 198 finally: 199 common.Cleanup() 200