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