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