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 */ getAlgorithm(X509Certificate cert)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 readPublicKey(File file)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 */ readPassword(File keyFile)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 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)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. */ readPrivateKey(File file)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 */ addDigestsToManifest(JarFile jar, int hashes)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 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest, int hash)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 CountOutputStream(OutputStream out)331 public CountOutputStream(OutputStream out) { 332 super(out); 333 mCount = 0; 334 } 335 336 @Override write(int b)337 public void write(int b) throws IOException { 338 super.write(b); 339 mCount++; 340 } 341 342 @Override write(byte[] b, int off, int len)343 public void write(byte[] b, int off, int len) throws IOException { 344 super.write(b, off, len); 345 mCount += len; 346 } 347 size()348 public int size() { 349 return mCount; 350 } 351 } 352 353 /** Write a .SF file with a digest of the specified manifest. */ writeSignatureFile(Manifest manifest, OutputStream out, int hash)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'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream 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 */ copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp)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 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)475 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 476 super(out); 477 this.tee = tee; 478 } 479 notifyClosing()480 public void notifyClosing() { 481 closing = true; 482 } 483 finish()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 getTail()493 public byte[] getTail() { 494 return footer.toByteArray(); 495 } 496 497 @Override write(byte[] b)498 public void write(byte[] b) throws IOException { 499 write(b, 0, b.length); 500 } 501 502 @Override write(byte[] b, int off, int len)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 write(int b)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 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)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 getContent()550 public Object getContent() { 551 throw new UnsupportedOperationException(); 552 } 553 getContentType()554 public ASN1ObjectIdentifier getContentType() { 555 return type; 556 } 557 write(OutputStream out)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 writeSignatureBlock(ByteArrayOutputStream temp)586 public void writeSignatureBlock(ByteArrayOutputStream temp) 587 throws IOException, 588 CertificateEncodingException, 589 OperatorCreationException, 590 CMSException { 591 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp); 592 } 593 getSigner()594 public WholeFileSignerOutputStream getSigner() { 595 return signer; 596 } 597 } 598 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)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 signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar)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 usage()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 main(String[] args)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