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