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