check_ota_package_signature.py revision a198b1e964cf9c90c0ddbe21b58cab203d769ebd
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 common 25import re 26import subprocess 27import sys 28import tempfile 29import zipfile 30 31from hashlib import sha1 32from hashlib import sha256 33 34# 'update_payload' package is under 'system/update_engine/scripts/', which 35# should to be included in PYTHONPATH. 36from update_payload.payload import Payload 37from update_payload.update_metadata_pb2 import Signatures 38 39 40def CertUsesSha256(cert): 41 """Check if the cert uses SHA-256 hashing algorithm.""" 42 43 cmd = ['openssl', 'x509', '-text', '-noout', '-in', cert] 44 p1 = common.Run(cmd, stdout=subprocess.PIPE) 45 cert_dump, _ = p1.communicate() 46 47 algorithm = re.search(r'Signature Algorithm: ([a-zA-Z0-9]+)', cert_dump) 48 assert algorithm, "Failed to identify the signature algorithm." 49 50 assert not algorithm.group(1).startswith('ecdsa'), ( 51 'This script doesn\'t support verifying ECDSA signed package yet.') 52 53 return algorithm.group(1).startswith('sha256') 54 55 56def VerifyPackage(cert, package): 57 """Verify the given package with the certificate. 58 59 (Comments from bootable/recovery/verifier.cpp:) 60 61 An archive with a whole-file signature will end in six bytes: 62 63 (2-byte signature start) $ff $ff (2-byte comment size) 64 65 (As far as the ZIP format is concerned, these are part of the 66 archive comment.) We start by reading this footer, this tells 67 us how far back from the end we have to start reading to find 68 the whole comment. 69 """ 70 71 print('Package: %s' % (package,)) 72 print('Certificate: %s' % (cert,)) 73 74 # Read in the package. 75 with open(package) as package_file: 76 package_bytes = package_file.read() 77 78 length = len(package_bytes) 79 assert length >= 6, "Not big enough to contain footer." 80 81 footer = [ord(x) for x in package_bytes[-6:]] 82 assert footer[2] == 0xff and footer[3] == 0xff, "Footer is wrong." 83 84 signature_start_from_end = (footer[1] << 8) + footer[0] 85 assert signature_start_from_end > 6, "Signature start is in the footer." 86 87 signature_start = length - signature_start_from_end 88 89 # Determine how much of the file is covered by the signature. This is 90 # everything except the signature data and length, which includes all of the 91 # EOCD except for the comment length field (2 bytes) and the comment data. 92 comment_len = (footer[5] << 8) + footer[4] 93 signed_len = length - comment_len - 2 94 95 print('Package length: %d' % (length,)) 96 print('Comment length: %d' % (comment_len,)) 97 print('Signed data length: %d' % (signed_len,)) 98 print('Signature start: %d' % (signature_start,)) 99 100 use_sha256 = CertUsesSha256(cert) 101 print('Use SHA-256: %s' % (use_sha256,)) 102 103 if use_sha256: 104 h = sha256() 105 else: 106 h = sha1() 107 h.update(package_bytes[:signed_len]) 108 package_digest = h.hexdigest().lower() 109 110 print('Digest: %s' % (package_digest,)) 111 112 # Get the signature from the input package. 113 signature = package_bytes[signature_start:-6] 114 sig_file = common.MakeTempFile(prefix='sig-') 115 with open(sig_file, 'wb') as f: 116 f.write(signature) 117 118 # Parse the signature and get the hash. 119 cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', sig_file] 120 p1 = common.Run(cmd, stdout=subprocess.PIPE) 121 sig, _ = p1.communicate() 122 assert p1.returncode == 0, "Failed to parse the signature." 123 124 digest_line = sig.strip().split('\n')[-1] 125 digest_string = digest_line.split(':')[3] 126 digest_file = common.MakeTempFile(prefix='digest-') 127 with open(digest_file, 'wb') as f: 128 f.write(digest_string.decode('hex')) 129 130 # Verify the digest by outputing the decrypted result in ASN.1 structure. 131 decrypted_file = common.MakeTempFile(prefix='decrypted-') 132 cmd = ['openssl', 'rsautl', '-verify', '-certin', '-inkey', cert, 133 '-in', digest_file, '-out', decrypted_file] 134 p1 = common.Run(cmd, stdout=subprocess.PIPE) 135 p1.communicate() 136 assert p1.returncode == 0, "Failed to run openssl rsautl -verify." 137 138 # Parse the output ASN.1 structure. 139 cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', decrypted_file] 140 p1 = common.Run(cmd, stdout=subprocess.PIPE) 141 decrypted_output, _ = p1.communicate() 142 assert p1.returncode == 0, "Failed to parse the output." 143 144 digest_line = decrypted_output.strip().split('\n')[-1] 145 digest_string = digest_line.split(':')[3].lower() 146 147 # Verify that the two digest strings match. 148 assert package_digest == digest_string, "Verification failed." 149 150 # Verified successfully upon reaching here. 151 print('\nWhole package signature VERIFIED\n') 152 153 154def VerifyAbOtaPayload(cert, package): 155 """Verifies the payload and metadata signatures in an A/B OTA payload.""" 156 157 def VerifySignatureBlob(hash_file, blob): 158 """Verifies the input hash_file against the signature blob.""" 159 signatures = Signatures() 160 signatures.ParseFromString(blob) 161 162 extracted_sig_file = common.MakeTempFile( 163 prefix='extracted-sig-', suffix='.bin') 164 # In Android, we only expect one signature. 165 assert len(signatures.signatures) == 1, \ 166 'Invalid number of signatures: %d' % len(signatures.signatures) 167 signature = signatures.signatures[0] 168 length = len(signature.data) 169 assert length == 256, 'Invalid signature length %d' % (length,) 170 with open(extracted_sig_file, 'w') as f: 171 f.write(signature.data) 172 173 # Verify the signature file extracted from the payload, by reversing the 174 # signing operation. Alternatively, this can be done by calling 'openssl 175 # rsautl -verify -certin -inkey <cert.pem> -in <extracted_sig_file> -out 176 # <output>', then to assert that 177 # <output> == SHA-256 DigestInfo prefix || <hash_file>. 178 cmd = ['openssl', 'pkeyutl', '-verify', '-certin', '-inkey', cert, 179 '-pkeyopt', 'digest:sha256', '-in', hash_file, 180 '-sigfile', extracted_sig_file] 181 p = common.Run(cmd, stdout=subprocess.PIPE) 182 result, _ = p.communicate() 183 184 # https://github.com/openssl/openssl/pull/3213 185 # 'openssl pkeyutl -verify' (prior to 1.1.0) returns non-zero return code, 186 # even on successful verification. To avoid the false alarm with older 187 # openssl, check the output directly. 188 assert result.strip() == 'Signature Verified Successfully', result.strip() 189 190 package_zip = zipfile.ZipFile(package, 'r') 191 if 'payload.bin' not in package_zip.namelist(): 192 common.ZipClose(package_zip) 193 return 194 195 print('Verifying A/B OTA payload signatures...') 196 197 package_dir = tempfile.mkdtemp(prefix='package-') 198 common.OPTIONS.tempfiles.append(package_dir) 199 200 payload_file = package_zip.extract('payload.bin', package_dir) 201 payload = Payload(open(payload_file, 'rb')) 202 payload.Init() 203 204 # Extract the payload hash and metadata hash from the payload.bin. 205 payload_hash_file = common.MakeTempFile(prefix='hash-', suffix='.bin') 206 metadata_hash_file = common.MakeTempFile(prefix='hash-', suffix='.bin') 207 cmd = ['brillo_update_payload', 'hash', 208 '--unsigned_payload', payload_file, 209 '--signature_size', '256', 210 '--metadata_hash_file', metadata_hash_file, 211 '--payload_hash_file', payload_hash_file] 212 p = common.Run(cmd, stdout=subprocess.PIPE) 213 p.communicate() 214 assert p.returncode == 0, 'brillo_update_payload hash failed' 215 216 # Payload signature verification. 217 assert payload.manifest.HasField('signatures_offset') 218 payload_signature = payload.ReadDataBlob( 219 payload.manifest.signatures_offset, payload.manifest.signatures_size) 220 VerifySignatureBlob(payload_hash_file, payload_signature) 221 222 # Metadata signature verification. 223 metadata_signature = payload.ReadDataBlob( 224 -payload.header.metadata_signature_len, 225 payload.header.metadata_signature_len) 226 VerifySignatureBlob(metadata_hash_file, metadata_signature) 227 228 common.ZipClose(package_zip) 229 230 # Verified successfully upon reaching here. 231 print('\nPayload signatures VERIFIED\n\n') 232 233 234def main(): 235 parser = argparse.ArgumentParser() 236 parser.add_argument('certificate', help='The certificate to be used.') 237 parser.add_argument('package', help='The OTA package to be verified.') 238 args = parser.parse_args() 239 240 VerifyPackage(args.certificate, args.package) 241 VerifyAbOtaPayload(args.certificate, args.package) 242 243 244if __name__ == '__main__': 245 try: 246 main() 247 except AssertionError as err: 248 print('\n ERROR: %s\n' % (err,)) 249 sys.exit(1) 250 finally: 251 common.Cleanup() 252