1 /* 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 17 package com.android.verity; 18 19 import java.io.File; 20 import java.io.RandomAccessFile; 21 import java.nio.ByteBuffer; 22 import java.nio.ByteOrder; 23 import java.lang.Math; 24 import java.lang.Process; 25 import java.lang.Runtime; 26 import java.math.BigInteger; 27 import java.security.KeyFactory; 28 import java.security.MessageDigest; 29 import java.security.PublicKey; 30 import java.security.Security; 31 import java.security.cert.X509Certificate; 32 import java.security.interfaces.RSAPublicKey; 33 import java.security.spec.RSAPublicKeySpec; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import javax.xml.bind.DatatypeConverter; 37 import org.bouncycastle.jce.provider.BouncyCastleProvider; 38 39 public class VerityVerifier { 40 41 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 57 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; 63 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; 68 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 72 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 95 /** 96 * Converts a 4-byte little endian value to a Java integer 97 * @param value Little endian integer to convert 98 */ 99 private static int fromle(int value) { 100 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 */ 108 private static int fromle(short value) { 109 return fromle(value << 16); 110 } 111 112 /** 113 * 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 /** 138 * 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 */ 142 private void openImage(String fname) throws Exception { 143 image = File.createTempFile("system", ".raw"); 144 image.deleteOnExit(); 145 146 Process p = Runtime.getRuntime().exec("simg2img " + fname + 147 " " + image.getAbsoluteFile()); 148 149 p.waitFor(); 150 if (p.exitValue() != 0) { 151 throw new IllegalArgumentException("Invalid image: failed to unsparse"); 152 } 153 } 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 /** 184 * 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 296 * given public key 297 * @param key Public key to use for signature verification 298 */ 299 public boolean verifyMetaData(PublicKey key) 300 throws Exception { 301 return Utils.verify(key, table, signature, 302 Utils.getSignatureAlgorithmIdentifier(key)); 303 } 304 305 /** 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 392 public static void main(String[] args) throws Exception { 393 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>"); 403 System.exit(1); 404 } 405 406 VerityVerifier verifier = new VerityVerifier(args[0]); 407 408 try { 409 if (verifier.verify(key)) { 410 System.err.println("Signature is VALID"); 411 System.exit(0); 412 } 413 } catch (Exception e) { 414 e.printStackTrace(System.err); 415 } 416 417 System.exit(1); 418 } 419 } 420