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.BufferedReader; 39 import java.io.ByteArrayInputStream; 40 import java.io.ByteArrayOutputStream; 41 import java.io.DataInputStream; 42 import java.io.File; 43 import java.io.FileInputStream; 44 import java.io.FileOutputStream; 45 import java.io.FilterOutputStream; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.io.InputStreamReader; 49 import java.io.OutputStream; 50 import java.io.PrintStream; 51 import java.lang.reflect.Constructor; 52 import java.security.DigestOutputStream; 53 import java.security.GeneralSecurityException; 54 import java.security.Key; 55 import java.security.KeyFactory; 56 import java.security.MessageDigest; 57 import java.security.PrivateKey; 58 import java.security.Provider; 59 import java.security.Security; 60 import java.security.cert.CertificateEncodingException; 61 import java.security.cert.CertificateFactory; 62 import java.security.cert.X509Certificate; 63 import java.security.spec.InvalidKeySpecException; 64 import java.security.spec.PKCS8EncodedKeySpec; 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.Enumeration; 68 import java.util.Locale; 69 import java.util.Map; 70 import java.util.TreeMap; 71 import java.util.jar.Attributes; 72 import java.util.jar.JarEntry; 73 import java.util.jar.JarFile; 74 import java.util.jar.JarOutputStream; 75 import java.util.jar.Manifest; 76 import java.util.regex.Pattern; 77 import javax.crypto.Cipher; 78 import javax.crypto.EncryptedPrivateKeyInfo; 79 import javax.crypto.SecretKeyFactory; 80 import javax.crypto.spec.PBEKeySpec; 81 82 /** 83 * HISTORICAL NOTE: 84 * 85 * Prior to the keylimepie release, SignApk ignored the signature 86 * algorithm specified in the certificate and always used SHA1withRSA. 87 * 88 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 89 * the signature algorithm in the certificate to select which to use 90 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 91 * 92 * Because there are old keys still in use whose certificate actually 93 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 94 * for compatibility with older releases. This can be changed by 95 * altering the getAlgorithm() function below. 96 */ 97 98 99 /** 100 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 101 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 102 * SHA-256 (see historical note). 103 */ 104 class SignApk { 105 private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 106 private static final String CERT_SIG_NAME = "META-INF/CERT.%s"; 107 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF"; 108 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s"; 109 110 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 111 112 private static Provider sBouncyCastleProvider; 113 114 // bitmasks for which hash algorithms we need the manifest to include. 115 private static final int USE_SHA1 = 1; 116 private static final int USE_SHA256 = 2; 117 118 /** 119 * Return one of USE_SHA1 or USE_SHA256 according to the signature 120 * algorithm specified in the cert. 121 */ getDigestAlgorithm(X509Certificate cert)122 private static int getDigestAlgorithm(X509Certificate cert) { 123 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 124 if ("SHA1WITHRSA".equals(sigAlg) || 125 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above. 126 return USE_SHA1; 127 } else if (sigAlg.startsWith("SHA256WITH")) { 128 return USE_SHA256; 129 } else { 130 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 131 "\" in cert [" + cert.getSubjectDN()); 132 } 133 } 134 135 /** Returns the expected signature algorithm for this key type. */ getSignatureAlgorithm(X509Certificate cert)136 private static String getSignatureAlgorithm(X509Certificate cert) { 137 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 138 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US); 139 if ("RSA".equalsIgnoreCase(keyType)) { 140 if (getDigestAlgorithm(cert) == USE_SHA256) { 141 return "SHA256withRSA"; 142 } else { 143 return "SHA1withRSA"; 144 } 145 } else if ("EC".equalsIgnoreCase(keyType)) { 146 return "SHA256withECDSA"; 147 } else { 148 throw new IllegalArgumentException("unsupported key type: " + keyType); 149 } 150 } 151 152 // Files matching this pattern are not copied to the output. 153 private static Pattern stripPattern = 154 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + 155 Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 156 readPublicKey(File file)157 private static X509Certificate readPublicKey(File file) 158 throws IOException, GeneralSecurityException { 159 FileInputStream input = new FileInputStream(file); 160 try { 161 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 162 return (X509Certificate) cf.generateCertificate(input); 163 } finally { 164 input.close(); 165 } 166 } 167 168 /** 169 * Reads the password from stdin and returns it as a string. 170 * 171 * @param keyFile The file containing the private key. Used to prompt the user. 172 */ readPassword(File keyFile)173 private static String readPassword(File keyFile) { 174 // TODO: use Console.readPassword() when it's available. 175 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 176 System.out.flush(); 177 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 178 try { 179 return stdin.readLine(); 180 } catch (IOException ex) { 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 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)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. */ readPrivateKey(File file)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 */ addDigestsToManifest(JarFile jar, int hashes)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 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest, int hash)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 CountOutputStream(OutputStream out)355 public CountOutputStream(OutputStream out) { 356 super(out); 357 mCount = 0; 358 } 359 360 @Override write(int b)361 public void write(int b) throws IOException { 362 super.write(b); 363 mCount++; 364 } 365 366 @Override write(byte[] b, int off, int len)367 public void write(byte[] b, int off, int len) throws IOException { 368 super.write(b, off, len); 369 mCount += len; 370 } 371 size()372 public int size() { 373 return mCount; 374 } 375 } 376 377 /** Write a .SF file with a digest of the specified manifest. */ writeSignatureFile(Manifest manifest, OutputStream out, int hash)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'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream 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 */ copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp, int alignment)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 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)549 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 550 super(out); 551 this.tee = tee; 552 } 553 notifyClosing()554 public void notifyClosing() { 555 closing = true; 556 } 557 finish()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 getTail()567 public byte[] getTail() { 568 return footer.toByteArray(); 569 } 570 571 @Override write(byte[] b)572 public void write(byte[] b) throws IOException { 573 write(b, 0, b.length); 574 } 575 576 @Override write(byte[] b, int off, int len)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 write(int b)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 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)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 getContent()624 public Object getContent() { 625 throw new UnsupportedOperationException(); 626 } 627 getContentType()628 public ASN1ObjectIdentifier getContentType() { 629 return type; 630 } 631 write(OutputStream out)632 public void write(OutputStream out) throws IOException { 633 try { 634 signer = new WholeFileSignerOutputStream(out, outputStream); 635 JarOutputStream outputJar = new JarOutputStream(signer); 636 637 int hash = getDigestAlgorithm(publicKey); 638 639 // Assume the certificate is valid for at least an hour. 640 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 641 642 Manifest manifest = addDigestsToManifest(inputJar, hash); 643 copyFiles(manifest, inputJar, outputJar, timestamp, 0); 644 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash); 645 646 signFile(manifest, inputJar, 647 new X509Certificate[]{ publicKey }, 648 new PrivateKey[]{ privateKey }, 649 outputJar); 650 651 signer.notifyClosing(); 652 outputJar.close(); 653 signer.finish(); 654 } 655 catch (Exception e) { 656 throw new IOException(e); 657 } 658 } 659 writeSignatureBlock(ByteArrayOutputStream temp)660 public void writeSignatureBlock(ByteArrayOutputStream temp) 661 throws IOException, 662 CertificateEncodingException, 663 OperatorCreationException, 664 CMSException { 665 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp); 666 } 667 getSigner()668 public WholeFileSignerOutputStream getSigner() { 669 return signer; 670 } 671 } 672 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream)673 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 674 X509Certificate publicKey, PrivateKey privateKey, 675 OutputStream outputStream) throws Exception { 676 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 677 publicKey, privateKey, outputStream); 678 679 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 680 681 // put a readable message and a null char at the start of the 682 // archive comment, so that tools that display the comment 683 // (hopefully) show something sensible. 684 // TODO: anything more useful we can put in this message? 685 byte[] message = "signed by SignApk".getBytes("UTF-8"); 686 temp.write(message); 687 temp.write(0); 688 689 cmsOut.writeSignatureBlock(temp); 690 691 byte[] zipData = cmsOut.getSigner().getTail(); 692 693 // For a zip with no archive comment, the 694 // end-of-central-directory record will be 22 bytes long, so 695 // we expect to find the EOCD marker 22 bytes from the end. 696 if (zipData[zipData.length-22] != 0x50 || 697 zipData[zipData.length-21] != 0x4b || 698 zipData[zipData.length-20] != 0x05 || 699 zipData[zipData.length-19] != 0x06) { 700 throw new IllegalArgumentException("zip data already has an archive comment"); 701 } 702 703 int total_size = temp.size() + 6; 704 if (total_size > 0xffff) { 705 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 706 } 707 // signature starts this many bytes from the end of the file 708 int signature_start = total_size - message.length - 1; 709 temp.write(signature_start & 0xff); 710 temp.write((signature_start >> 8) & 0xff); 711 // Why the 0xff bytes? In a zip file with no archive comment, 712 // bytes [-6:-2] of the file are the little-endian offset from 713 // the start of the file to the central directory. So for the 714 // two high bytes to be 0xff 0xff, the archive would have to 715 // be nearly 4GB in size. So it's unlikely that a real 716 // commentless archive would have 0xffs here, and lets us tell 717 // an old signed archive from a new one. 718 temp.write(0xff); 719 temp.write(0xff); 720 temp.write(total_size & 0xff); 721 temp.write((total_size >> 8) & 0xff); 722 temp.flush(); 723 724 // Signature verification checks that the EOCD header is the 725 // last such sequence in the file (to avoid minzip finding a 726 // fake EOCD appended after the signature in its scan). The 727 // odds of producing this sequence by chance are very low, but 728 // let's catch it here if it does. 729 byte[] b = temp.toByteArray(); 730 for (int i = 0; i < b.length-3; ++i) { 731 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 732 throw new IllegalArgumentException("found spurious EOCD header at " + i); 733 } 734 } 735 736 outputStream.write(total_size & 0xff); 737 outputStream.write((total_size >> 8) & 0xff); 738 temp.writeTo(outputStream); 739 } 740 signFile(Manifest manifest, JarFile inputJar, X509Certificate[] publicKey, PrivateKey[] privateKey, JarOutputStream outputJar)741 private static void signFile(Manifest manifest, JarFile inputJar, 742 X509Certificate[] publicKey, PrivateKey[] privateKey, 743 JarOutputStream outputJar) 744 throws Exception { 745 // Assume the certificate is valid for at least an hour. 746 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 747 748 // MANIFEST.MF 749 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); 750 je.setTime(timestamp); 751 outputJar.putNextEntry(je); 752 manifest.write(outputJar); 753 754 int numKeys = publicKey.length; 755 for (int k = 0; k < numKeys; ++k) { 756 // CERT.SF / CERT#.SF 757 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : 758 (String.format(CERT_SF_MULTI_NAME, k))); 759 je.setTime(timestamp); 760 outputJar.putNextEntry(je); 761 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 762 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k])); 763 byte[] signedData = baos.toByteArray(); 764 outputJar.write(signedData); 765 766 // CERT.{EC,RSA} / CERT#.{EC,RSA} 767 final String keyType = publicKey[k].getPublicKey().getAlgorithm(); 768 je = new JarEntry(numKeys == 1 ? 769 (String.format(CERT_SIG_NAME, keyType)) : 770 (String.format(CERT_SIG_MULTI_NAME, k, keyType))); 771 je.setTime(timestamp); 772 outputJar.putNextEntry(je); 773 writeSignatureBlock(new CMSProcessableByteArray(signedData), 774 publicKey[k], privateKey[k], outputJar); 775 } 776 } 777 778 /** 779 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 780 * types that might be stored in PKCS#11-like storage. 781 */ loadProviderIfNecessary(String providerClassName)782 private static void loadProviderIfNecessary(String providerClassName) { 783 if (providerClassName == null) { 784 return; 785 } 786 787 final Class<?> klass; 788 try { 789 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 790 if (sysLoader != null) { 791 klass = sysLoader.loadClass(providerClassName); 792 } else { 793 klass = Class.forName(providerClassName); 794 } 795 } catch (ClassNotFoundException e) { 796 e.printStackTrace(); 797 System.exit(1); 798 return; 799 } 800 801 Constructor<?> constructor = null; 802 for (Constructor<?> c : klass.getConstructors()) { 803 if (c.getParameterTypes().length == 0) { 804 constructor = c; 805 break; 806 } 807 } 808 if (constructor == null) { 809 System.err.println("No zero-arg constructor found for " + providerClassName); 810 System.exit(1); 811 return; 812 } 813 814 final Object o; 815 try { 816 o = constructor.newInstance(); 817 } catch (Exception e) { 818 e.printStackTrace(); 819 System.exit(1); 820 return; 821 } 822 if (!(o instanceof Provider)) { 823 System.err.println("Not a Provider class: " + providerClassName); 824 System.exit(1); 825 } 826 827 Security.insertProviderAt((Provider) o, 1); 828 } 829 usage()830 private static void usage() { 831 System.err.println("Usage: signapk [-w] " + 832 "[-a <alignment>] " + 833 "[-providerClass <className>] " + 834 "publickey.x509[.pem] privatekey.pk8 " + 835 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 836 "input.jar output.jar"); 837 System.exit(2); 838 } 839 main(String[] args)840 public static void main(String[] args) { 841 if (args.length < 4) usage(); 842 843 sBouncyCastleProvider = new BouncyCastleProvider(); 844 Security.addProvider(sBouncyCastleProvider); 845 846 boolean signWholeFile = false; 847 String providerClass = null; 848 String providerArg = null; 849 int alignment = 4; 850 851 int argstart = 0; 852 while (argstart < args.length && args[argstart].startsWith("-")) { 853 if ("-w".equals(args[argstart])) { 854 signWholeFile = true; 855 ++argstart; 856 } else if ("-providerClass".equals(args[argstart])) { 857 if (argstart + 1 >= args.length) { 858 usage(); 859 } 860 providerClass = args[++argstart]; 861 ++argstart; 862 } else if ("-a".equals(args[argstart])) { 863 alignment = Integer.parseInt(args[++argstart]); 864 ++argstart; 865 } else { 866 usage(); 867 } 868 } 869 870 if ((args.length - argstart) % 2 == 1) usage(); 871 int numKeys = ((args.length - argstart) / 2) - 1; 872 if (signWholeFile && numKeys > 1) { 873 System.err.println("Only one key may be used with -w."); 874 System.exit(2); 875 } 876 877 loadProviderIfNecessary(providerClass); 878 879 String inputFilename = args[args.length-2]; 880 String outputFilename = args[args.length-1]; 881 882 JarFile inputJar = null; 883 FileOutputStream outputFile = null; 884 int hashes = 0; 885 886 try { 887 File firstPublicKeyFile = new File(args[argstart+0]); 888 889 X509Certificate[] publicKey = new X509Certificate[numKeys]; 890 try { 891 for (int i = 0; i < numKeys; ++i) { 892 int argNum = argstart + i*2; 893 publicKey[i] = readPublicKey(new File(args[argNum])); 894 hashes |= getDigestAlgorithm(publicKey[i]); 895 } 896 } catch (IllegalArgumentException e) { 897 System.err.println(e); 898 System.exit(1); 899 } 900 901 // Set the ZIP file timestamp to the starting valid time 902 // of the 0th certificate plus one hour (to match what 903 // we've historically done). 904 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 905 906 PrivateKey[] privateKey = new PrivateKey[numKeys]; 907 for (int i = 0; i < numKeys; ++i) { 908 int argNum = argstart + i*2 + 1; 909 privateKey[i] = readPrivateKey(new File(args[argNum])); 910 } 911 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 912 913 outputFile = new FileOutputStream(outputFilename); 914 915 916 if (signWholeFile) { 917 SignApk.signWholeFile(inputJar, firstPublicKeyFile, 918 publicKey[0], privateKey[0], outputFile); 919 } else { 920 JarOutputStream outputJar = new JarOutputStream(outputFile); 921 922 // For signing .apks, use the maximum compression to make 923 // them as small as possible (since they live forever on 924 // the system partition). For OTA packages, use the 925 // default compression level, which is much much faster 926 // and produces output that is only a tiny bit larger 927 // (~0.1% on full OTA packages I tested). 928 outputJar.setLevel(9); 929 930 Manifest manifest = addDigestsToManifest(inputJar, hashes); 931 copyFiles(manifest, inputJar, outputJar, timestamp, alignment); 932 signFile(manifest, inputJar, publicKey, privateKey, outputJar); 933 outputJar.close(); 934 } 935 } catch (Exception e) { 936 e.printStackTrace(); 937 System.exit(1); 938 } finally { 939 try { 940 if (inputJar != null) inputJar.close(); 941 if (outputFile != null) outputFile.close(); 942 } catch (IOException e) { 943 e.printStackTrace(); 944 System.exit(1); 945 } 946 } 947 } 948 } 949