1 /* 2 * Copyright (C) 2008 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.signapk; 18 19 import org.bouncycastle.asn1.ASN1InputStream; 20 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21 import org.bouncycastle.asn1.DEROutputStream; 22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24 import org.bouncycastle.cert.jcajce.JcaCertStore; 25 import org.bouncycastle.cms.CMSException; 26 import org.bouncycastle.cms.CMSSignedData; 27 import org.bouncycastle.cms.CMSSignedDataGenerator; 28 import org.bouncycastle.cms.CMSTypedData; 29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30 import org.bouncycastle.jce.provider.BouncyCastleProvider; 31 import org.bouncycastle.operator.ContentSigner; 32 import org.bouncycastle.operator.OperatorCreationException; 33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35 import org.conscrypt.OpenSSLProvider; 36 37 import com.android.apksig.ApkSignerEngine; 38 import com.android.apksig.DefaultApkSignerEngine; 39 import com.android.apksig.apk.ApkUtils; 40 import com.android.apksig.apk.MinSdkVersionException; 41 import com.android.apksig.util.DataSink; 42 import com.android.apksig.util.DataSources; 43 import com.android.apksig.zip.ZipFormatException; 44 45 import java.io.Console; 46 import java.io.BufferedReader; 47 import java.io.ByteArrayInputStream; 48 import java.io.ByteArrayOutputStream; 49 import java.io.DataInputStream; 50 import java.io.File; 51 import java.io.FileInputStream; 52 import java.io.FileOutputStream; 53 import java.io.FilterOutputStream; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.InputStreamReader; 57 import java.io.OutputStream; 58 import java.lang.reflect.Constructor; 59 import java.nio.ByteBuffer; 60 import java.nio.ByteOrder; 61 import java.nio.charset.StandardCharsets; 62 import java.security.GeneralSecurityException; 63 import java.security.Key; 64 import java.security.KeyFactory; 65 import java.security.PrivateKey; 66 import java.security.Provider; 67 import java.security.Security; 68 import java.security.cert.CertificateEncodingException; 69 import java.security.cert.CertificateFactory; 70 import java.security.cert.X509Certificate; 71 import java.security.spec.InvalidKeySpecException; 72 import java.security.spec.PKCS8EncodedKeySpec; 73 import java.util.ArrayList; 74 import java.util.Collections; 75 import java.util.Enumeration; 76 import java.util.List; 77 import java.util.Locale; 78 import java.util.TimeZone; 79 import java.util.jar.JarEntry; 80 import java.util.jar.JarFile; 81 import java.util.jar.JarOutputStream; 82 import java.util.regex.Pattern; 83 84 import javax.crypto.Cipher; 85 import javax.crypto.EncryptedPrivateKeyInfo; 86 import javax.crypto.SecretKeyFactory; 87 import javax.crypto.spec.PBEKeySpec; 88 89 /** 90 * HISTORICAL NOTE: 91 * 92 * Prior to the keylimepie release, SignApk ignored the signature 93 * algorithm specified in the certificate and always used SHA1withRSA. 94 * 95 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 96 * the signature algorithm in the certificate to select which to use 97 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 98 * 99 * Because there are old keys still in use whose certificate actually 100 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 101 * for compatibility with older releases. This can be changed by 102 * altering the getAlgorithm() function below. 103 */ 104 105 106 /** 107 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 108 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 109 * SHA-256 (see historical note). The tool can additionally sign APKs using 110 * APK Signature Scheme v2. 111 */ 112 class SignApk { 113 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 114 115 /** 116 * Extensible data block/field header ID used for storing information about alignment of 117 * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section 118 * 4.5 Extensible data fields. 119 */ 120 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; 121 122 /** 123 * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed 124 * entries. 125 */ 126 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; 127 128 // bitmasks for which hash algorithms we need the manifest to include. 129 private static final int USE_SHA1 = 1; 130 private static final int USE_SHA256 = 2; 131 132 /** 133 * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used 134 * for signing an OTA update package using the private key corresponding to the provided 135 * certificate. 136 */ getDigestAlgorithmForOta(X509Certificate cert)137 private static int getDigestAlgorithmForOta(X509Certificate cert) { 138 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 139 if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { 140 // see "HISTORICAL NOTE" above. 141 return USE_SHA1; 142 } else if (sigAlg.startsWith("SHA256WITH")) { 143 return USE_SHA256; 144 } else { 145 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 146 "\" in cert [" + cert.getSubjectDN()); 147 } 148 } 149 150 /** 151 * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA 152 * update package using the private key corresponding to the provided certificate and the 153 * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). 154 */ getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash)155 private static String getJcaSignatureAlgorithmForOta( 156 X509Certificate cert, int hash) { 157 String sigAlgDigestPrefix; 158 switch (hash) { 159 case USE_SHA1: 160 sigAlgDigestPrefix = "SHA1"; 161 break; 162 case USE_SHA256: 163 sigAlgDigestPrefix = "SHA256"; 164 break; 165 default: 166 throw new IllegalArgumentException("Unknown hash ID: " + hash); 167 } 168 169 String keyAlgorithm = cert.getPublicKey().getAlgorithm(); 170 if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 171 return sigAlgDigestPrefix + "withRSA"; 172 } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 173 return sigAlgDigestPrefix + "withECDSA"; 174 } else { 175 throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 176 } 177 } 178 readPublicKey(File file)179 private static X509Certificate readPublicKey(File file) 180 throws IOException, GeneralSecurityException { 181 FileInputStream input = new FileInputStream(file); 182 try { 183 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 184 return (X509Certificate) cf.generateCertificate(input); 185 } finally { 186 input.close(); 187 } 188 } 189 190 /** 191 * If a console doesn't exist, reads the password from stdin 192 * If a console exists, reads the password from console and returns it as a string. 193 * 194 * @param keyFile The file containing the private key. Used to prompt the user. 195 */ readPassword(File keyFile)196 private static String readPassword(File keyFile) { 197 Console console; 198 char[] pwd; 199 if ((console = System.console()) == null) { 200 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 201 System.out.flush(); 202 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 203 try { 204 return stdin.readLine(); 205 } catch (IOException ex) { 206 return null; 207 } 208 } else { 209 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) { 210 return String.valueOf(pwd); 211 } else { 212 return null; 213 } 214 } 215 } 216 217 /** 218 * Decrypt an encrypted PKCS#8 format private key. 219 * 220 * Based on ghstark's post on Aug 6, 2006 at 221 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 222 * 223 * @param encryptedPrivateKey The raw data of the private key 224 * @param keyFile The file containing the private key 225 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)226 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 227 throws GeneralSecurityException { 228 EncryptedPrivateKeyInfo epkInfo; 229 try { 230 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 231 } catch (IOException ex) { 232 // Probably not an encrypted key. 233 return null; 234 } 235 236 char[] password = readPassword(keyFile).toCharArray(); 237 238 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 239 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 240 241 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 242 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 243 244 try { 245 return epkInfo.getKeySpec(cipher); 246 } catch (InvalidKeySpecException ex) { 247 System.err.println("signapk: Password for " + keyFile + " may be bad."); 248 throw ex; 249 } 250 } 251 252 /** Read a PKCS#8 format private key. */ readPrivateKey(File file)253 private static PrivateKey readPrivateKey(File file) 254 throws IOException, GeneralSecurityException { 255 DataInputStream input = new DataInputStream(new FileInputStream(file)); 256 try { 257 byte[] bytes = new byte[(int) file.length()]; 258 input.read(bytes); 259 260 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 261 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); 262 if (spec == null) { 263 spec = new PKCS8EncodedKeySpec(bytes); 264 } 265 266 /* 267 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 268 * OID and use that to construct a KeyFactory. 269 */ 270 PrivateKeyInfo pki; 271 try (ASN1InputStream bIn = 272 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { 273 pki = PrivateKeyInfo.getInstance(bIn.readObject()); 274 } 275 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 276 277 return KeyFactory.getInstance(algOid).generatePrivate(spec); 278 } finally { 279 input.close(); 280 } 281 } 282 283 /** 284 * Add a copy of the public key to the archive; this should 285 * exactly match one of the files in 286 * /system/etc/security/otacerts.zip on the device. (The same 287 * cert can be extracted from the OTA update package's signature 288 * block but this is much easier to get at.) 289 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp)290 private static void addOtacert(JarOutputStream outputJar, 291 File publicKeyFile, 292 long timestamp) 293 throws IOException { 294 295 JarEntry je = new JarEntry(OTACERT_NAME); 296 je.setTime(timestamp); 297 outputJar.putNextEntry(je); 298 FileInputStream input = new FileInputStream(publicKeyFile); 299 byte[] b = new byte[4096]; 300 int read; 301 while ((read = input.read(b)) != -1) { 302 outputJar.write(b, 0, read); 303 } 304 input.close(); 305 } 306 307 308 /** Sign data and write the digital signature to 'out'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out)309 private static void writeSignatureBlock( 310 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, 311 OutputStream out) 312 throws IOException, 313 CertificateEncodingException, 314 OperatorCreationException, 315 CMSException { 316 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 317 certList.add(publicKey); 318 JcaCertStore certs = new JcaCertStore(certList); 319 320 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 321 ContentSigner signer = 322 new JcaContentSignerBuilder( 323 getJcaSignatureAlgorithmForOta(publicKey, hash)) 324 .build(privateKey); 325 gen.addSignerInfoGenerator( 326 new JcaSignerInfoGeneratorBuilder( 327 new JcaDigestCalculatorProviderBuilder() 328 .build()) 329 .setDirectSignature(true) 330 .build(signer, publicKey)); 331 gen.addCertificates(certs); 332 CMSSignedData sigData = gen.generate(data, false); 333 334 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 335 DEROutputStream dos = new DEROutputStream(out); 336 dos.writeObject(asn1.readObject()); 337 } 338 } 339 340 /** 341 * Adds ZIP entries which represent the v1 signature (JAR signature scheme). 342 */ addV1Signature( ApkSignerEngine apkSigner, ApkSignerEngine.OutputJarSignatureRequest v1Signature, JarOutputStream out, long timestamp)343 private static void addV1Signature( 344 ApkSignerEngine apkSigner, 345 ApkSignerEngine.OutputJarSignatureRequest v1Signature, 346 JarOutputStream out, 347 long timestamp) throws IOException { 348 for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry 349 : v1Signature.getAdditionalJarEntries()) { 350 String entryName = entry.getName(); 351 JarEntry outEntry = new JarEntry(entryName); 352 outEntry.setTime(timestamp); 353 out.putNextEntry(outEntry); 354 byte[] entryData = entry.getData(); 355 out.write(entryData); 356 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 357 apkSigner.outputJarEntry(entryName); 358 if (inspectEntryRequest != null) { 359 inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); 360 inspectEntryRequest.done(); 361 } 362 } 363 } 364 365 /** 366 * Copy all JAR entries from input to output. We set the modification times in the output to a 367 * fixed time, so as to reduce variation in the output file and make incremental OTAs more 368 * efficient. 369 */ copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, long timestamp, int defaultAlignment)370 private static void copyFiles( 371 JarFile in, 372 Pattern ignoredFilenamePattern, 373 ApkSignerEngine apkSigner, 374 JarOutputStream out, 375 long timestamp, 376 int defaultAlignment) throws IOException { 377 byte[] buffer = new byte[4096]; 378 int num; 379 380 ArrayList<String> names = new ArrayList<String>(); 381 for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) { 382 JarEntry entry = e.nextElement(); 383 if (entry.isDirectory()) { 384 continue; 385 } 386 String entryName = entry.getName(); 387 if ((ignoredFilenamePattern != null) 388 && (ignoredFilenamePattern.matcher(entryName).matches())) { 389 continue; 390 } 391 names.add(entryName); 392 } 393 Collections.sort(names); 394 395 boolean firstEntry = true; 396 long offset = 0L; 397 398 // We do the copy in two passes -- first copying all the 399 // entries that are STORED, then copying all the entries that 400 // have any other compression flag (which in practice means 401 // DEFLATED). This groups all the stored entries together at 402 // the start of the file and makes it easier to do alignment 403 // on them (since only stored entries are aligned). 404 405 List<String> remainingNames = new ArrayList<>(names.size()); 406 for (String name : names) { 407 JarEntry inEntry = in.getJarEntry(name); 408 if (inEntry.getMethod() != JarEntry.STORED) { 409 // Defer outputting this entry until we're ready to output compressed entries. 410 remainingNames.add(name); 411 continue; 412 } 413 414 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 415 continue; 416 } 417 418 // Preserve the STORED method of the input entry. 419 JarEntry outEntry = new JarEntry(inEntry); 420 outEntry.setTime(timestamp); 421 // Discard comment and extra fields of this entry to 422 // simplify alignment logic below and for consistency with 423 // how compressed entries are handled later. 424 outEntry.setComment(null); 425 outEntry.setExtra(null); 426 427 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 428 // Alignment of the entry's data is achieved by adding a data block to the entry's Local 429 // File Header extra field. The data block contains information about the alignment 430 // value and the necessary padding bytes (0x00) to achieve the alignment. This works 431 // because the entry's data will be located immediately after the extra field. 432 // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format 433 // of the extra field. 434 435 // 'offset' is the offset into the file at which we expect the entry's data to begin. 436 // This is the value we need to make a multiple of 'alignment'. 437 offset += JarFile.LOCHDR + outEntry.getName().length(); 438 if (firstEntry) { 439 // The first entry in a jar file has an extra field of four bytes that you can't get 440 // rid of; any extra data you specify in the JarEntry is appended to these forced 441 // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. 442 // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 443 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. 444 offset += 4; 445 firstEntry = false; 446 } 447 int extraPaddingSizeBytes = 0; 448 if (alignment > 0) { 449 long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; 450 extraPaddingSizeBytes = 451 (alignment - (int) (paddingStartOffset % alignment)) % alignment; 452 } 453 byte[] extra = 454 new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; 455 ByteBuffer extraBuf = ByteBuffer.wrap(extra); 456 extraBuf.order(ByteOrder.LITTLE_ENDIAN); 457 extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID 458 extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size 459 extraBuf.putShort((short) alignment); 460 outEntry.setExtra(extra); 461 offset += extra.length; 462 463 out.putNextEntry(outEntry); 464 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 465 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 466 DataSink entryDataSink = 467 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 468 469 try (InputStream data = in.getInputStream(inEntry)) { 470 while ((num = data.read(buffer)) > 0) { 471 out.write(buffer, 0, num); 472 if (entryDataSink != null) { 473 entryDataSink.consume(buffer, 0, num); 474 } 475 offset += num; 476 } 477 } 478 out.flush(); 479 if (inspectEntryRequest != null) { 480 inspectEntryRequest.done(); 481 } 482 } 483 484 // Copy all the non-STORED entries. We don't attempt to 485 // maintain the 'offset' variable past this point; we don't do 486 // alignment on these entries. 487 488 for (String name : remainingNames) { 489 JarEntry inEntry = in.getJarEntry(name); 490 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 491 continue; 492 } 493 494 // Create a new entry so that the compressed len is recomputed. 495 JarEntry outEntry = new JarEntry(name); 496 outEntry.setTime(timestamp); 497 out.putNextEntry(outEntry); 498 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 499 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 500 DataSink entryDataSink = 501 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 502 503 InputStream data = in.getInputStream(inEntry); 504 while ((num = data.read(buffer)) > 0) { 505 out.write(buffer, 0, num); 506 if (entryDataSink != null) { 507 entryDataSink.consume(buffer, 0, num); 508 } 509 } 510 out.flush(); 511 if (inspectEntryRequest != null) { 512 inspectEntryRequest.done(); 513 } 514 } 515 } 516 shouldOutputApkEntry( ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)517 private static boolean shouldOutputApkEntry( 518 ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) 519 throws IOException { 520 if (apkSigner == null) { 521 return true; 522 } 523 524 ApkSignerEngine.InputJarEntryInstructions instructions = 525 apkSigner.inputJarEntry(inEntry.getName()); 526 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 527 instructions.getInspectJarEntryRequest(); 528 if (inspectEntryRequest != null) { 529 provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); 530 } 531 switch (instructions.getOutputPolicy()) { 532 case OUTPUT: 533 return true; 534 case SKIP: 535 case OUTPUT_BY_ENGINE: 536 return false; 537 default: 538 throw new RuntimeException( 539 "Unsupported output policy: " + instructions.getOutputPolicy()); 540 } 541 } 542 provideJarEntry( JarFile jarFile, JarEntry jarEntry, ApkSignerEngine.InspectJarEntryRequest request, byte[] tmpbuf)543 private static void provideJarEntry( 544 JarFile jarFile, 545 JarEntry jarEntry, 546 ApkSignerEngine.InspectJarEntryRequest request, 547 byte[] tmpbuf) throws IOException { 548 DataSink dataSink = request.getDataSink(); 549 try (InputStream in = jarFile.getInputStream(jarEntry)) { 550 int chunkSize; 551 while ((chunkSize = in.read(tmpbuf)) > 0) { 552 dataSink.consume(tmpbuf, 0, chunkSize); 553 } 554 request.done(); 555 } 556 } 557 558 /** 559 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 560 * relative to start of file or {@code 0} if alignment of this entry's data is not important. 561 */ getStoredEntryDataAlignment(String entryName, int defaultAlignment)562 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 563 if (defaultAlignment <= 0) { 564 return 0; 565 } 566 567 if (entryName.endsWith(".so")) { 568 // Align .so contents to memory page boundary to enable memory-mapped 569 // execution. 570 return 4096; 571 } else { 572 return defaultAlignment; 573 } 574 } 575 576 private static class WholeFileSignerOutputStream extends FilterOutputStream { 577 private boolean closing = false; 578 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 579 private OutputStream tee; 580 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)581 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 582 super(out); 583 this.tee = tee; 584 } 585 notifyClosing()586 public void notifyClosing() { 587 closing = true; 588 } 589 finish()590 public void finish() throws IOException { 591 closing = false; 592 593 byte[] data = footer.toByteArray(); 594 if (data.length < 2) 595 throw new IOException("Less than two bytes written to footer"); 596 write(data, 0, data.length - 2); 597 } 598 getTail()599 public byte[] getTail() { 600 return footer.toByteArray(); 601 } 602 603 @Override write(byte[] b)604 public void write(byte[] b) throws IOException { 605 write(b, 0, b.length); 606 } 607 608 @Override write(byte[] b, int off, int len)609 public void write(byte[] b, int off, int len) throws IOException { 610 if (closing) { 611 // if the jar is about to close, save the footer that will be written 612 footer.write(b, off, len); 613 } 614 else { 615 // write to both output streams. out is the CMSTypedData signer and tee is the file. 616 out.write(b, off, len); 617 tee.write(b, off, len); 618 } 619 } 620 621 @Override write(int b)622 public void write(int b) throws IOException { 623 if (closing) { 624 // if the jar is about to close, save the footer that will be written 625 footer.write(b); 626 } 627 else { 628 // write to both output streams. out is the CMSTypedData signer and tee is the file. 629 out.write(b); 630 tee.write(b); 631 } 632 } 633 } 634 635 private static class CMSSigner implements CMSTypedData { 636 private final JarFile inputJar; 637 private final File publicKeyFile; 638 private final X509Certificate publicKey; 639 private final PrivateKey privateKey; 640 private final int hash; 641 private final long timestamp; 642 private final OutputStream outputStream; 643 private final ASN1ObjectIdentifier type; 644 private WholeFileSignerOutputStream signer; 645 646 // Files matching this pattern are not copied to the output. 647 private static final Pattern STRIP_PATTERN = 648 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" 649 + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 650 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)651 public CMSSigner(JarFile inputJar, File publicKeyFile, 652 X509Certificate publicKey, PrivateKey privateKey, int hash, 653 long timestamp, OutputStream outputStream) { 654 this.inputJar = inputJar; 655 this.publicKeyFile = publicKeyFile; 656 this.publicKey = publicKey; 657 this.privateKey = privateKey; 658 this.hash = hash; 659 this.timestamp = timestamp; 660 this.outputStream = outputStream; 661 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 662 } 663 664 /** 665 * This should actually return byte[] or something similar, but nothing 666 * actually checks it currently. 667 */ 668 @Override getContent()669 public Object getContent() { 670 return this; 671 } 672 673 @Override getContentType()674 public ASN1ObjectIdentifier getContentType() { 675 return type; 676 } 677 678 @Override write(OutputStream out)679 public void write(OutputStream out) throws IOException { 680 try { 681 signer = new WholeFileSignerOutputStream(out, outputStream); 682 JarOutputStream outputJar = new JarOutputStream(signer); 683 684 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0); 685 addOtacert(outputJar, publicKeyFile, timestamp); 686 687 signer.notifyClosing(); 688 outputJar.close(); 689 signer.finish(); 690 } 691 catch (Exception e) { 692 throw new IOException(e); 693 } 694 } 695 writeSignatureBlock(ByteArrayOutputStream temp)696 public void writeSignatureBlock(ByteArrayOutputStream temp) 697 throws IOException, 698 CertificateEncodingException, 699 OperatorCreationException, 700 CMSException { 701 SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); 702 } 703 getSigner()704 public WholeFileSignerOutputStream getSigner() { 705 return signer; 706 } 707 } 708 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)709 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 710 X509Certificate publicKey, PrivateKey privateKey, 711 int hash, long timestamp, 712 OutputStream outputStream) throws Exception { 713 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 714 publicKey, privateKey, hash, timestamp, outputStream); 715 716 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 717 718 // put a readable message and a null char at the start of the 719 // archive comment, so that tools that display the comment 720 // (hopefully) show something sensible. 721 // TODO: anything more useful we can put in this message? 722 byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8); 723 temp.write(message); 724 temp.write(0); 725 726 cmsOut.writeSignatureBlock(temp); 727 728 byte[] zipData = cmsOut.getSigner().getTail(); 729 730 // For a zip with no archive comment, the 731 // end-of-central-directory record will be 22 bytes long, so 732 // we expect to find the EOCD marker 22 bytes from the end. 733 if (zipData[zipData.length-22] != 0x50 || 734 zipData[zipData.length-21] != 0x4b || 735 zipData[zipData.length-20] != 0x05 || 736 zipData[zipData.length-19] != 0x06) { 737 throw new IllegalArgumentException("zip data already has an archive comment"); 738 } 739 740 int total_size = temp.size() + 6; 741 if (total_size > 0xffff) { 742 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 743 } 744 // signature starts this many bytes from the end of the file 745 int signature_start = total_size - message.length - 1; 746 temp.write(signature_start & 0xff); 747 temp.write((signature_start >> 8) & 0xff); 748 // Why the 0xff bytes? In a zip file with no archive comment, 749 // bytes [-6:-2] of the file are the little-endian offset from 750 // the start of the file to the central directory. So for the 751 // two high bytes to be 0xff 0xff, the archive would have to 752 // be nearly 4GB in size. So it's unlikely that a real 753 // commentless archive would have 0xffs here, and lets us tell 754 // an old signed archive from a new one. 755 temp.write(0xff); 756 temp.write(0xff); 757 temp.write(total_size & 0xff); 758 temp.write((total_size >> 8) & 0xff); 759 temp.flush(); 760 761 // Signature verification checks that the EOCD header is the 762 // last such sequence in the file (to avoid minzip finding a 763 // fake EOCD appended after the signature in its scan). The 764 // odds of producing this sequence by chance are very low, but 765 // let's catch it here if it does. 766 byte[] b = temp.toByteArray(); 767 for (int i = 0; i < b.length-3; ++i) { 768 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 769 throw new IllegalArgumentException("found spurious EOCD header at " + i); 770 } 771 } 772 773 outputStream.write(total_size & 0xff); 774 outputStream.write((total_size >> 8) & 0xff); 775 temp.writeTo(outputStream); 776 } 777 778 /** 779 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 780 * types that might be stored in PKCS#11-like storage. 781 */ loadProviderIfNecessary(String providerClassName)782 private static void loadProviderIfNecessary(String providerClassName) { 783 if (providerClassName == null) { 784 return; 785 } 786 787 final Class<?> klass; 788 try { 789 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 790 if (sysLoader != null) { 791 klass = sysLoader.loadClass(providerClassName); 792 } else { 793 klass = Class.forName(providerClassName); 794 } 795 } catch (ClassNotFoundException e) { 796 e.printStackTrace(); 797 System.exit(1); 798 return; 799 } 800 801 Constructor<?> constructor = null; 802 for (Constructor<?> c : klass.getConstructors()) { 803 if (c.getParameterTypes().length == 0) { 804 constructor = c; 805 break; 806 } 807 } 808 if (constructor == null) { 809 System.err.println("No zero-arg constructor found for " + providerClassName); 810 System.exit(1); 811 return; 812 } 813 814 final Object o; 815 try { 816 o = constructor.newInstance(); 817 } catch (Exception e) { 818 e.printStackTrace(); 819 System.exit(1); 820 return; 821 } 822 if (!(o instanceof Provider)) { 823 System.err.println("Not a Provider class: " + providerClassName); 824 System.exit(1); 825 } 826 827 Security.insertProviderAt((Provider) o, 1); 828 } 829 createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)830 private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs( 831 PrivateKey[] privateKeys, X509Certificate[] certificates) { 832 if (privateKeys.length != certificates.length) { 833 throw new IllegalArgumentException( 834 "The number of private keys must match the number of certificates: " 835 + privateKeys.length + " vs" + certificates.length); 836 } 837 List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>(); 838 String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; 839 for (int i = 0; i < privateKeys.length; i++) { 840 String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); 841 DefaultApkSignerEngine.SignerConfig signerConfig = 842 new DefaultApkSignerEngine.SignerConfig.Builder( 843 signerName, 844 privateKeys[i], 845 Collections.singletonList(certificates[i])) 846 .build(); 847 signerConfigs.add(signerConfig); 848 } 849 return signerConfigs; 850 } 851 852 private static class ZipSections { 853 ByteBuffer beforeCentralDir; 854 ByteBuffer centralDir; 855 ByteBuffer eocd; 856 } 857 findMainZipSections(ByteBuffer apk)858 private static ZipSections findMainZipSections(ByteBuffer apk) 859 throws IOException, ZipFormatException { 860 apk.slice(); 861 ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk)); 862 long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); 863 long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); 864 long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; 865 long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); 866 if (centralDirEndOffset != eocdStartOffset) { 867 throw new ZipFormatException( 868 "ZIP Central Directory is not immediately followed by End of Central Directory" 869 + ". CD end: " + centralDirEndOffset 870 + ", EoCD start: " + eocdStartOffset); 871 } 872 apk.position(0); 873 apk.limit((int) centralDirStartOffset); 874 ByteBuffer beforeCentralDir = apk.slice(); 875 876 apk.position((int) centralDirStartOffset); 877 apk.limit((int) centralDirEndOffset); 878 ByteBuffer centralDir = apk.slice(); 879 880 apk.position((int) eocdStartOffset); 881 apk.limit(apk.capacity()); 882 ByteBuffer eocd = apk.slice(); 883 884 apk.position(0); 885 apk.limit(apk.capacity()); 886 887 ZipSections result = new ZipSections(); 888 result.beforeCentralDir = beforeCentralDir; 889 result.centralDir = centralDir; 890 result.eocd = eocd; 891 return result; 892 } 893 894 /** 895 * Returns the API Level corresponding to the APK's minSdkVersion. 896 * 897 * @throws MinSdkVersionException if the API Level cannot be determined from the APK. 898 */ getMinSdkVersion(JarFile apk)899 private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException { 900 JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml"); 901 if (manifestEntry == null) { 902 throw new MinSdkVersionException("No AndroidManifest.xml in APK"); 903 } 904 byte[] manifestBytes; 905 try { 906 try (InputStream manifestIn = apk.getInputStream(manifestEntry)) { 907 manifestBytes = toByteArray(manifestIn); 908 } 909 } catch (IOException e) { 910 throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e); 911 } 912 return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes)); 913 } 914 toByteArray(InputStream in)915 private static byte[] toByteArray(InputStream in) throws IOException { 916 ByteArrayOutputStream result = new ByteArrayOutputStream(); 917 byte[] buf = new byte[65536]; 918 int chunkSize; 919 while ((chunkSize = in.read(buf)) != -1) { 920 result.write(buf, 0, chunkSize); 921 } 922 return result.toByteArray(); 923 } 924 usage()925 private static void usage() { 926 System.err.println("Usage: signapk [-w] " + 927 "[-a <alignment>] " + 928 "[-providerClass <className>] " + 929 "[--min-sdk-version <n>] " + 930 "[--disable-v2] " + 931 "publickey.x509[.pem] privatekey.pk8 " + 932 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 933 "input.jar output.jar"); 934 System.exit(2); 935 } 936 main(String[] args)937 public static void main(String[] args) { 938 if (args.length < 4) usage(); 939 940 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than 941 // the standard or Bouncy Castle ones. 942 Security.insertProviderAt(new OpenSSLProvider(), 1); 943 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer 944 // DSA which may still be needed. 945 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. 946 Security.addProvider(new BouncyCastleProvider()); 947 948 boolean signWholeFile = false; 949 String providerClass = null; 950 int alignment = 4; 951 Integer minSdkVersionOverride = null; 952 boolean signUsingApkSignatureSchemeV2 = true; 953 954 int argstart = 0; 955 while (argstart < args.length && args[argstart].startsWith("-")) { 956 if ("-w".equals(args[argstart])) { 957 signWholeFile = true; 958 ++argstart; 959 } else if ("-providerClass".equals(args[argstart])) { 960 if (argstart + 1 >= args.length) { 961 usage(); 962 } 963 providerClass = args[++argstart]; 964 ++argstart; 965 } else if ("-a".equals(args[argstart])) { 966 alignment = Integer.parseInt(args[++argstart]); 967 ++argstart; 968 } else if ("--min-sdk-version".equals(args[argstart])) { 969 String minSdkVersionString = args[++argstart]; 970 try { 971 minSdkVersionOverride = Integer.parseInt(minSdkVersionString); 972 } catch (NumberFormatException e) { 973 throw new IllegalArgumentException( 974 "--min-sdk-version must be a decimal number: " + minSdkVersionString); 975 } 976 ++argstart; 977 } else if ("--disable-v2".equals(args[argstart])) { 978 signUsingApkSignatureSchemeV2 = false; 979 ++argstart; 980 } else { 981 usage(); 982 } 983 } 984 985 if ((args.length - argstart) % 2 == 1) usage(); 986 int numKeys = ((args.length - argstart) / 2) - 1; 987 if (signWholeFile && numKeys > 1) { 988 System.err.println("Only one key may be used with -w."); 989 System.exit(2); 990 } 991 992 loadProviderIfNecessary(providerClass); 993 994 String inputFilename = args[args.length-2]; 995 String outputFilename = args[args.length-1]; 996 997 JarFile inputJar = null; 998 FileOutputStream outputFile = null; 999 1000 try { 1001 File firstPublicKeyFile = new File(args[argstart+0]); 1002 1003 X509Certificate[] publicKey = new X509Certificate[numKeys]; 1004 try { 1005 for (int i = 0; i < numKeys; ++i) { 1006 int argNum = argstart + i*2; 1007 publicKey[i] = readPublicKey(new File(args[argNum])); 1008 } 1009 } catch (IllegalArgumentException e) { 1010 System.err.println(e); 1011 System.exit(1); 1012 } 1013 1014 // Set all ZIP file timestamps to Jan 1 2009 00:00:00. 1015 long timestamp = 1230768000000L; 1016 // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS 1017 // timestamp using the current timezone. We thus adjust the milliseconds since epoch 1018 // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. 1019 timestamp -= TimeZone.getDefault().getOffset(timestamp); 1020 1021 PrivateKey[] privateKey = new PrivateKey[numKeys]; 1022 for (int i = 0; i < numKeys; ++i) { 1023 int argNum = argstart + i*2 + 1; 1024 privateKey[i] = readPrivateKey(new File(args[argNum])); 1025 } 1026 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 1027 1028 outputFile = new FileOutputStream(outputFilename); 1029 1030 // NOTE: Signing currently recompresses any compressed entries using Deflate (default 1031 // compression level for OTA update files and maximum compession level for APKs). 1032 if (signWholeFile) { 1033 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); 1034 signWholeFile(inputJar, firstPublicKeyFile, 1035 publicKey[0], privateKey[0], digestAlgorithm, 1036 timestamp, 1037 outputFile); 1038 } else { 1039 // Determine the value to use as minSdkVersion of the APK being signed 1040 int minSdkVersion; 1041 if (minSdkVersionOverride != null) { 1042 minSdkVersion = minSdkVersionOverride; 1043 } else { 1044 try { 1045 minSdkVersion = getMinSdkVersion(inputJar); 1046 } catch (MinSdkVersionException e) { 1047 throw new IllegalArgumentException( 1048 "Cannot detect minSdkVersion. Use --min-sdk-version to override", 1049 e); 1050 } 1051 } 1052 1053 try (ApkSignerEngine apkSigner = 1054 new DefaultApkSignerEngine.Builder( 1055 createSignerConfigs(privateKey, publicKey), minSdkVersion) 1056 .setV1SigningEnabled(true) 1057 .setV2SigningEnabled(signUsingApkSignatureSchemeV2) 1058 .setOtherSignersSignaturesPreserved(false) 1059 .setCreatedBy("1.0 (Android SignApk)") 1060 .build()) { 1061 // We don't preserve the input APK's APK Signing Block (which contains v2 1062 // signatures) 1063 apkSigner.inputApkSigningBlock(null); 1064 1065 // Build the output APK in memory, by copying input APK's ZIP entries across 1066 // and then signing the output APK. 1067 ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); 1068 JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); 1069 // Use maximum compression for compressed entries because the APK lives forever 1070 // on the system partition. 1071 outputJar.setLevel(9); 1072 copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment); 1073 ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = 1074 apkSigner.outputJarEntries(); 1075 if (addV1SignatureRequest != null) { 1076 addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); 1077 addV1SignatureRequest.done(); 1078 } 1079 outputJar.close(); 1080 ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); 1081 v1SignedApkBuf.reset(); 1082 ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk}; 1083 1084 ZipSections zipSections = findMainZipSections(v1SignedApk); 1085 ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest = 1086 apkSigner.outputZipSections2( 1087 DataSources.asDataSource(zipSections.beforeCentralDir), 1088 DataSources.asDataSource(zipSections.centralDir), 1089 DataSources.asDataSource(zipSections.eocd)); 1090 if (addV2SignatureRequest != null) { 1091 // Need to insert the returned APK Signing Block before ZIP Central 1092 // Directory. 1093 int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock(); 1094 byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); 1095 // Because the APK Signing Block is inserted before the Central Directory, 1096 // we need to adjust accordingly the offset of Central Directory inside the 1097 // ZIP End of Central Directory (EoCD) record. 1098 ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining()); 1099 modifiedEocd.put(zipSections.eocd); 1100 modifiedEocd.flip(); 1101 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1102 ApkUtils.setZipEocdCentralDirectoryOffset( 1103 modifiedEocd, 1104 zipSections.beforeCentralDir.remaining() + padding + 1105 apkSigningBlock.length); 1106 outputChunks = 1107 new ByteBuffer[] { 1108 zipSections.beforeCentralDir, 1109 ByteBuffer.allocate(padding), 1110 ByteBuffer.wrap(apkSigningBlock), 1111 zipSections.centralDir, 1112 modifiedEocd}; 1113 addV2SignatureRequest.done(); 1114 } 1115 1116 // This assumes outputChunks are array-backed. To avoid this assumption, the 1117 // code could be rewritten to use FileChannel. 1118 for (ByteBuffer outputChunk : outputChunks) { 1119 outputFile.write( 1120 outputChunk.array(), 1121 outputChunk.arrayOffset() + outputChunk.position(), 1122 outputChunk.remaining()); 1123 outputChunk.position(outputChunk.limit()); 1124 } 1125 1126 outputFile.close(); 1127 outputFile = null; 1128 apkSigner.outputDone(); 1129 } 1130 1131 return; 1132 } 1133 } catch (Exception e) { 1134 e.printStackTrace(); 1135 System.exit(1); 1136 } finally { 1137 try { 1138 if (inputJar != null) inputJar.close(); 1139 if (outputFile != null) outputFile.close(); 1140 } catch (IOException e) { 1141 e.printStackTrace(); 1142 System.exit(1); 1143 } 1144 } 1145 } 1146 } 1147