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