blob: 6b3f49edf207af4efe1df3dace21e809fc3f0cc0 [file] [log] [blame]
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -08001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.verity;
18
19import java.io.File;
20import java.io.RandomAccessFile;
21import java.nio.ByteBuffer;
22import java.nio.ByteOrder;
Sami Tolvanen4de50952014-11-18 19:45:09 +000023import java.lang.Math;
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080024import java.lang.Process;
25import java.lang.Runtime;
Sami Tolvanen4de50952014-11-18 19:45:09 +000026import java.math.BigInteger;
27import java.security.KeyFactory;
28import java.security.MessageDigest;
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080029import java.security.PublicKey;
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080030import java.security.Security;
31import java.security.cert.X509Certificate;
Sami Tolvanen4de50952014-11-18 19:45:09 +000032import java.security.interfaces.RSAPublicKey;
33import java.security.spec.RSAPublicKeySpec;
34import java.util.ArrayList;
35import java.util.Arrays;
36import javax.xml.bind.DatatypeConverter;
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080037import org.bouncycastle.jce.provider.BouncyCastleProvider;
38
39public class VerityVerifier {
40
Sami Tolvanen4de50952014-11-18 19:45:09 +000041 private ArrayList<Integer> hashBlocksLevel;
42 private byte[] hashTree;
43 private byte[] rootHash;
44 private byte[] salt;
45 private byte[] signature;
46 private byte[] table;
47 private File image;
48 private int blockSize;
49 private int hashBlockSize;
50 private int hashOffsetForData;
51 private int hashSize;
52 private int hashTreeSize;
53 private long hashStart;
54 private long imageSize;
55 private MessageDigest digest;
56
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080057 private static final int EXT4_SB_MAGIC = 0xEF53;
58 private static final int EXT4_SB_OFFSET = 0x400;
59 private static final int EXT4_SB_OFFSET_MAGIC = EXT4_SB_OFFSET + 0x38;
60 private static final int EXT4_SB_OFFSET_LOG_BLOCK_SIZE = EXT4_SB_OFFSET + 0x18;
61 private static final int EXT4_SB_OFFSET_BLOCKS_COUNT_LO = EXT4_SB_OFFSET + 0x4;
62 private static final int EXT4_SB_OFFSET_BLOCKS_COUNT_HI = EXT4_SB_OFFSET + 0x150;
Sami Tolvanen4de50952014-11-18 19:45:09 +000063 private static final int MINCRYPT_OFFSET_MODULUS = 0x8;
64 private static final int MINCRYPT_OFFSET_EXPONENT = 0x208;
65 private static final int MINCRYPT_MODULUS_SIZE = 0x100;
66 private static final int MINCRYPT_EXPONENT_SIZE = 0x4;
67 private static final int VERITY_FIELDS = 10;
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080068 private static final int VERITY_MAGIC = 0xB001B001;
69 private static final int VERITY_SIGNATURE_SIZE = 256;
70 private static final int VERITY_VERSION = 0;
71
Sami Tolvanen4de50952014-11-18 19:45:09 +000072 public VerityVerifier(String fname) throws Exception {
73 digest = MessageDigest.getInstance("SHA-256");
74 hashSize = digest.getDigestLength();
75 hashBlocksLevel = new ArrayList<Integer>();
76 hashTreeSize = -1;
77 openImage(fname);
78 readVerityData();
79 }
80
81 /**
82 * Reverses the order of bytes in a byte array
83 * @param value Byte array to reverse
84 */
85 private static byte[] reverse(byte[] value) {
86 for (int i = 0; i < value.length / 2; i++) {
87 byte tmp = value[i];
88 value[i] = value[value.length - i - 1];
89 value[value.length - i - 1] = tmp;
90 }
91
92 return value;
93 }
94
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -080095 /**
96 * Converts a 4-byte little endian value to a Java integer
97 * @param value Little endian integer to convert
98 */
Sami Tolvanen4de50952014-11-18 19:45:09 +000099 private static int fromle(int value) {
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800100 byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
101 return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
102 }
103
104 /**
105 * Converts a 2-byte little endian value to Java a integer
106 * @param value Little endian short to convert
107 */
Sami Tolvanen4de50952014-11-18 19:45:09 +0000108 private static int fromle(short value) {
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800109 return fromle(value << 16);
110 }
111
112 /**
Sami Tolvanen4de50952014-11-18 19:45:09 +0000113 * Reads a 2048-bit RSA public key saved in mincrypt format, and returns
114 * a Java PublicKey for it.
115 * @param fname Name of the mincrypt public key file
116 */
117 private static PublicKey getMincryptPublicKey(String fname) throws Exception {
118 try (RandomAccessFile key = new RandomAccessFile(fname, "r")) {
119 byte[] binaryMod = new byte[MINCRYPT_MODULUS_SIZE];
120 byte[] binaryExp = new byte[MINCRYPT_EXPONENT_SIZE];
121
122 key.seek(MINCRYPT_OFFSET_MODULUS);
123 key.readFully(binaryMod);
124
125 key.seek(MINCRYPT_OFFSET_EXPONENT);
126 key.readFully(binaryExp);
127
128 BigInteger modulus = new BigInteger(1, reverse(binaryMod));
129 BigInteger exponent = new BigInteger(1, reverse(binaryExp));
130
131 RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
132 KeyFactory factory = KeyFactory.getInstance("RSA");
133 return factory.generatePublic(spec);
134 }
135 }
136
137 /**
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800138 * Unsparses a sparse image into a temporary file and returns a
139 * handle to the file
140 * @param fname Path to a sparse image file
141 */
Sami Tolvanen4de50952014-11-18 19:45:09 +0000142 private void openImage(String fname) throws Exception {
143 image = File.createTempFile("system", ".raw");
144 image.deleteOnExit();
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800145
146 Process p = Runtime.getRuntime().exec("simg2img " + fname +
Sami Tolvanen4de50952014-11-18 19:45:09 +0000147 " " + image.getAbsoluteFile());
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800148
149 p.waitFor();
150 if (p.exitValue() != 0) {
151 throw new IllegalArgumentException("Invalid image: failed to unsparse");
152 }
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800153 }
154
155 /**
156 * Reads the ext4 superblock and calculates the size of the system image,
157 * after which we should find the verity metadata
158 * @param img File handle to the image file
159 */
160 public static long getMetadataPosition(RandomAccessFile img)
161 throws Exception {
162 img.seek(EXT4_SB_OFFSET_MAGIC);
163 int magic = fromle(img.readShort());
164
165 if (magic != EXT4_SB_MAGIC) {
166 throw new IllegalArgumentException("Invalid image: not a valid ext4 image");
167 }
168
169 img.seek(EXT4_SB_OFFSET_BLOCKS_COUNT_LO);
170 long blocksCountLo = fromle(img.readInt());
171
172 img.seek(EXT4_SB_OFFSET_LOG_BLOCK_SIZE);
173 long logBlockSize = fromle(img.readInt());
174
175 img.seek(EXT4_SB_OFFSET_BLOCKS_COUNT_HI);
176 long blocksCountHi = fromle(img.readInt());
177
178 long blockSizeBytes = 1L << (10 + logBlockSize);
179 long blockCount = (blocksCountHi << 32) + blocksCountLo;
180 return blockSizeBytes * blockCount;
181 }
182
183 /**
Sami Tolvanen4de50952014-11-18 19:45:09 +0000184 * Calculates the size of the verity hash tree based on the image size
185 */
186 private int calculateHashTreeSize() {
187 if (hashTreeSize > 0) {
188 return hashTreeSize;
189 }
190
191 int totalBlocks = 0;
192 int hashes = (int) (imageSize / blockSize);
193
194 hashBlocksLevel.clear();
195
196 do {
197 hashBlocksLevel.add(0, hashes);
198
199 int hashBlocks =
200 (int) Math.ceil((double) hashes * hashSize / hashBlockSize);
201
202 totalBlocks += hashBlocks;
203
204 hashes = hashBlocks;
205 } while (hashes > 1);
206
207 hashTreeSize = totalBlocks * hashBlockSize;
208 return hashTreeSize;
209 }
210
211 /**
212 * Parses the verity mapping table and reads the hash tree from
213 * the image file
214 * @param img Handle to the image file
215 * @param table Verity mapping table
216 */
217 private void readHashTree(RandomAccessFile img, byte[] table)
218 throws Exception {
219 String tableStr = new String(table);
220 String[] fields = tableStr.split(" ");
221
222 if (fields.length != VERITY_FIELDS) {
223 throw new IllegalArgumentException("Invalid image: unexpected number of fields "
224 + "in verity mapping table (" + fields.length + ")");
225 }
226
227 String hashVersion = fields[0];
228
229 if (!"1".equals(hashVersion)) {
230 throw new IllegalArgumentException("Invalid image: unsupported hash format");
231 }
232
233 String alg = fields[7];
234
235 if (!"sha256".equals(alg)) {
236 throw new IllegalArgumentException("Invalid image: unsupported hash algorithm");
237 }
238
239 blockSize = Integer.parseInt(fields[3]);
240 hashBlockSize = Integer.parseInt(fields[4]);
241
242 int blocks = Integer.parseInt(fields[5]);
243 int start = Integer.parseInt(fields[6]);
244
245 if (imageSize != (long) blocks * blockSize) {
246 throw new IllegalArgumentException("Invalid image: size mismatch in mapping "
247 + "table");
248 }
249
250 rootHash = DatatypeConverter.parseHexBinary(fields[8]);
251 salt = DatatypeConverter.parseHexBinary(fields[9]);
252
253 hashStart = (long) start * blockSize;
254 img.seek(hashStart);
255
256 int treeSize = calculateHashTreeSize();
257
258 hashTree = new byte[treeSize];
259 img.readFully(hashTree);
260 }
261
262 /**
263 * Reads verity data from the image file
264 */
265 private void readVerityData() throws Exception {
266 try (RandomAccessFile img = new RandomAccessFile(image, "r")) {
267 imageSize = getMetadataPosition(img);
268 img.seek(imageSize);
269
270 int magic = fromle(img.readInt());
271
272 if (magic != VERITY_MAGIC) {
273 throw new IllegalArgumentException("Invalid image: verity metadata not found");
274 }
275
276 int version = fromle(img.readInt());
277
278 if (version != VERITY_VERSION) {
279 throw new IllegalArgumentException("Invalid image: unknown metadata version");
280 }
281
282 signature = new byte[VERITY_SIGNATURE_SIZE];
283 img.readFully(signature);
284
285 int tableSize = fromle(img.readInt());
286
287 table = new byte[tableSize];
288 img.readFully(table);
289
290 readHashTree(img, table);
291 }
292 }
293
294 /**
295 * Reads and validates verity metadata, and checks the signature against the
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800296 * given public key
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800297 * @param key Public key to use for signature verification
298 */
Sami Tolvanen4de50952014-11-18 19:45:09 +0000299 public boolean verifyMetaData(PublicKey key)
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800300 throws Exception {
Sami Tolvanen4de50952014-11-18 19:45:09 +0000301 return Utils.verify(key, table, signature,
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800302 Utils.getSignatureAlgorithmIdentifier(key));
303 }
304
Sami Tolvanen4de50952014-11-18 19:45:09 +0000305 /**
306 * Hashes a block of data using a salt and checks of the results are expected
307 * @param hash The expected hash value
308 * @param data The data block to check
309 */
310 private boolean checkBlock(byte[] hash, byte[] data) {
311 digest.reset();
312 digest.update(salt);
313 digest.update(data);
314 return Arrays.equals(hash, digest.digest());
315 }
316
317 /**
318 * Verifies the root hash and the first N-1 levels of the hash tree
319 */
320 private boolean verifyHashTree() throws Exception {
321 int hashOffset = 0;
322 int dataOffset = hashBlockSize;
323
324 if (!checkBlock(rootHash, Arrays.copyOfRange(hashTree, 0, hashBlockSize))) {
325 System.err.println("Root hash mismatch");
326 return false;
327 }
328
329 for (int level = 0; level < hashBlocksLevel.size() - 1; level++) {
330 int blocks = hashBlocksLevel.get(level);
331
332 for (int i = 0; i < blocks; i++) {
333 byte[] hashBlock = Arrays.copyOfRange(hashTree,
334 hashOffset + i * hashSize,
335 hashOffset + i * hashSize + hashSize);
336
337 byte[] dataBlock = Arrays.copyOfRange(hashTree,
338 dataOffset + i * hashBlockSize,
339 dataOffset + i * hashBlockSize + hashBlockSize);
340
341 if (!checkBlock(hashBlock, dataBlock)) {
342 System.err.printf("Hash mismatch at tree level %d, block %d\n", level, i);
343 return false;
344 }
345 }
346
347 hashOffset = dataOffset;
348 hashOffsetForData = dataOffset;
349 dataOffset += blocks * hashBlockSize;
350 }
351
352 return true;
353 }
354
355 /**
356 * Validates the image against the hash tree
357 */
358 public boolean verifyData() throws Exception {
359 if (!verifyHashTree()) {
360 return false;
361 }
362
363 try (RandomAccessFile img = new RandomAccessFile(image, "r")) {
364 byte[] dataBlock = new byte[blockSize];
365 int hashOffset = hashOffsetForData;
366
367 for (int i = 0; (long) i * blockSize < imageSize; i++) {
368 byte[] hashBlock = Arrays.copyOfRange(hashTree,
369 hashOffset + i * hashSize,
370 hashOffset + i * hashSize + hashSize);
371
372 img.readFully(dataBlock);
373
374 if (!checkBlock(hashBlock, dataBlock)) {
375 System.err.printf("Hash mismatch at block %d\n", i);
376 return false;
377 }
378 }
379 }
380
381 return true;
382 }
383
384 /**
385 * Verifies the integrity of the image and the verity metadata
386 * @param key Public key to use for signature verification
387 */
388 public boolean verify(PublicKey key) throws Exception {
389 return (verifyMetaData(key) && verifyData());
390 }
391
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800392 public static void main(String[] args) throws Exception {
Sami Tolvanen4de50952014-11-18 19:45:09 +0000393 Security.addProvider(new BouncyCastleProvider());
394 PublicKey key = null;
395
396 if (args.length == 3 && "-mincrypt".equals(args[1])) {
397 key = getMincryptPublicKey(args[2]);
398 } else if (args.length == 2) {
399 X509Certificate cert = Utils.loadPEMCertificate(args[1]);
400 key = cert.getPublicKey();
401 } else {
402 System.err.println("Usage: VerityVerifier <sparse.img> <certificate.x509.pem> | -mincrypt <mincrypt_key>");
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800403 System.exit(1);
404 }
405
Sami Tolvanen4de50952014-11-18 19:45:09 +0000406 VerityVerifier verifier = new VerityVerifier(args[0]);
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800407
408 try {
Sami Tolvanen4de50952014-11-18 19:45:09 +0000409 if (verifier.verify(key)) {
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800410 System.err.println("Signature is VALID");
411 System.exit(0);
Sami Tolvanenc18aa9d2014-11-12 07:47:53 -0800412 }
413 } catch (Exception e) {
414 e.printStackTrace(System.err);
415 }
416
417 System.exit(1);
418 }
419}