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