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