SignApk.java revision 9b75e272b6bbcc79dcdf8eef0524a30872c84ef6
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 17package com.android.signapk; 18 19import org.bouncycastle.asn1.ASN1InputStream; 20import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21import org.bouncycastle.asn1.DEROutputStream; 22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24import org.bouncycastle.cert.jcajce.JcaCertStore; 25import org.bouncycastle.cms.CMSException; 26import org.bouncycastle.cms.CMSSignedData; 27import org.bouncycastle.cms.CMSSignedDataGenerator; 28import org.bouncycastle.cms.CMSTypedData; 29import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30import org.bouncycastle.jce.provider.BouncyCastleProvider; 31import org.bouncycastle.operator.ContentSigner; 32import org.bouncycastle.operator.OperatorCreationException; 33import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35import org.conscrypt.OpenSSLProvider; 36 37import com.android.apksig.ApkSignerEngine; 38import com.android.apksig.DefaultApkSignerEngine; 39import com.android.apksig.apk.ApkUtils; 40import com.android.apksig.apk.MinSdkVersionException; 41import com.android.apksig.util.DataSink; 42import com.android.apksig.util.DataSources; 43import com.android.apksig.zip.ZipFormatException; 44 45import java.io.Console; 46import java.io.BufferedReader; 47import java.io.ByteArrayInputStream; 48import java.io.ByteArrayOutputStream; 49import java.io.DataInputStream; 50import java.io.File; 51import java.io.FileInputStream; 52import java.io.FileOutputStream; 53import java.io.FilterOutputStream; 54import java.io.IOException; 55import java.io.InputStream; 56import java.io.InputStreamReader; 57import java.io.OutputStream; 58import java.lang.reflect.Constructor; 59import java.nio.ByteBuffer; 60import java.nio.ByteOrder; 61import java.nio.charset.StandardCharsets; 62import java.security.GeneralSecurityException; 63import java.security.Key; 64import java.security.KeyFactory; 65import java.security.PrivateKey; 66import java.security.Provider; 67import java.security.Security; 68import java.security.cert.CertificateEncodingException; 69import java.security.cert.CertificateFactory; 70import java.security.cert.X509Certificate; 71import java.security.spec.InvalidKeySpecException; 72import java.security.spec.PKCS8EncodedKeySpec; 73import java.util.ArrayList; 74import java.util.Collections; 75import java.util.Enumeration; 76import java.util.List; 77import java.util.Locale; 78import java.util.TimeZone; 79import java.util.jar.JarEntry; 80import java.util.jar.JarFile; 81import java.util.jar.JarOutputStream; 82import java.util.regex.Pattern; 83 84import javax.crypto.Cipher; 85import javax.crypto.EncryptedPrivateKeyInfo; 86import javax.crypto.SecretKeyFactory; 87import 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 */ 112class 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 */ 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 */ 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 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 */ 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 */ 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. */ 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 */ 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'. */ 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 */ 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 */ 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 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 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 */ 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 581 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 582 super(out); 583 this.tee = tee; 584 } 585 586 public void notifyClosing() { 587 closing = true; 588 } 589 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 599 public byte[] getTail() { 600 return footer.toByteArray(); 601 } 602 603 @Override 604 public void write(byte[] b) throws IOException { 605 write(b, 0, b.length); 606 } 607 608 @Override 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 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 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 669 public Object getContent() { 670 return this; 671 } 672 673 @Override 674 public ASN1ObjectIdentifier getContentType() { 675 return type; 676 } 677 678 @Override 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 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 704 public WholeFileSignerOutputStream getSigner() { 705 return signer; 706 } 707 } 708 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 */ 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 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 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 */ 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 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 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 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.OutputApkSigningBlockRequest addV2SignatureRequest = 1086 apkSigner.outputZipSections( 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 byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); 1094 // Because the APK Signing Block is inserted before the Central Directory, 1095 // we need to adjust accordingly the offset of Central Directory inside the 1096 // ZIP End of Central Directory (EoCD) record. 1097 ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining()); 1098 modifiedEocd.put(zipSections.eocd); 1099 modifiedEocd.flip(); 1100 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1101 ApkUtils.setZipEocdCentralDirectoryOffset( 1102 modifiedEocd, 1103 zipSections.beforeCentralDir.remaining() + apkSigningBlock.length); 1104 outputChunks = 1105 new ByteBuffer[] { 1106 zipSections.beforeCentralDir, 1107 ByteBuffer.wrap(apkSigningBlock), 1108 zipSections.centralDir, 1109 modifiedEocd}; 1110 addV2SignatureRequest.done(); 1111 } 1112 1113 // This assumes outputChunks are array-backed. To avoid this assumption, the 1114 // code could be rewritten to use FileChannel. 1115 for (ByteBuffer outputChunk : outputChunks) { 1116 outputFile.write( 1117 outputChunk.array(), 1118 outputChunk.arrayOffset() + outputChunk.position(), 1119 outputChunk.remaining()); 1120 outputChunk.position(outputChunk.limit()); 1121 } 1122 1123 outputFile.close(); 1124 outputFile = null; 1125 apkSigner.outputDone(); 1126 } 1127 1128 return; 1129 } 1130 } catch (Exception e) { 1131 e.printStackTrace(); 1132 System.exit(1); 1133 } finally { 1134 try { 1135 if (inputJar != null) inputJar.close(); 1136 if (outputFile != null) outputFile.close(); 1137 } catch (IOException e) { 1138 e.printStackTrace(); 1139 System.exit(1); 1140 } 1141 } 1142 } 1143} 1144