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