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