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