blob: 1f8b7bb417c6fbb3557767212a99f8c533699f2e [file] [log] [blame]
Tao Bao9c63fb52016-09-13 11:13:48 -07001#!/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
Tao Baoa198b1e2017-08-31 16:52:55 -070028import tempfile
29import zipfile
Tao Bao9c63fb52016-09-13 11:13:48 -070030
31from hashlib import sha1
32from hashlib import sha256
33
Tao Baoa198b1e2017-08-31 16:52:55 -070034# '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
Tao Bao9c63fb52016-09-13 11:13:48 -070038
Tao Baoa198b1e2017-08-31 16:52:55 -070039
40def CertUsesSha256(cert):
Tao Bao9c63fb52016-09-13 11:13:48 -070041 """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
Tao Baoa198b1e2017-08-31 16:52:55 -070056def VerifyPackage(cert, package):
Tao Bao9c63fb52016-09-13 11:13:48 -070057 """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
Tao Baoa198b1e2017-08-31 16:52:55 -0700100 use_sha256 = CertUsesSha256(cert)
Tao Bao9c63fb52016-09-13 11:13:48 -0700101 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
Tao Baoa198b1e2017-08-31 16:52:55 -0700110 print('Digest: %s' % (package_digest,))
Tao Bao9c63fb52016-09-13 11:13:48 -0700111
112 # Get the signature from the input package.
113 signature = package_bytes[signature_start:-6]
Tao Bao4c851b12016-09-19 13:54:38 -0700114 sig_file = common.MakeTempFile(prefix='sig-')
Tao Bao9c63fb52016-09-13 11:13:48 -0700115 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]
Tao Bao4c851b12016-09-19 13:54:38 -0700126 digest_file = common.MakeTempFile(prefix='digest-')
Tao Bao9c63fb52016-09-13 11:13:48 -0700127 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.
Tao Bao4c851b12016-09-19 13:54:38 -0700131 decrypted_file = common.MakeTempFile(prefix='decrypted-')
Tao Bao9c63fb52016-09-13 11:13:48 -0700132 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.
Tao Baoa198b1e2017-08-31 16:52:55 -0700151 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')
Tao Bao9c63fb52016-09-13 11:13:48 -0700232
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
Tao Baoa198b1e2017-08-31 16:52:55 -0700240 VerifyPackage(args.certificate, args.package)
241 VerifyAbOtaPayload(args.certificate, args.package)
Tao Bao9c63fb52016-09-13 11:13:48 -0700242
243
244if __name__ == '__main__':
245 try:
246 main()
247 except AssertionError as err:
248 print('\n ERROR: %s\n' % (err,))
249 sys.exit(1)
Tao Baoa198b1e2017-08-31 16:52:55 -0700250 finally:
251 common.Cleanup()