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