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