SignApk.java revision fe7c1e59d15c9fd3f5f28ef555926547e26b8640
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 17package com.android.signapk; 18 19import org.bouncycastle.asn1.ASN1InputStream; 20import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21import org.bouncycastle.asn1.DEROutputStream; 22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24import org.bouncycastle.cert.jcajce.JcaCertStore; 25import org.bouncycastle.cms.CMSException; 26import org.bouncycastle.cms.CMSProcessableByteArray; 27import org.bouncycastle.cms.CMSSignedData; 28import org.bouncycastle.cms.CMSSignedDataGenerator; 29import org.bouncycastle.cms.CMSTypedData; 30import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 31import org.bouncycastle.jce.provider.BouncyCastleProvider; 32import org.bouncycastle.operator.ContentSigner; 33import org.bouncycastle.operator.OperatorCreationException; 34import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 35import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 36import org.bouncycastle.util.encoders.Base64; 37import org.conscrypt.OpenSSLProvider; 38 39import java.io.Console; 40import java.io.BufferedReader; 41import java.io.ByteArrayInputStream; 42import java.io.ByteArrayOutputStream; 43import java.io.DataInputStream; 44import java.io.File; 45import java.io.FileInputStream; 46import java.io.FileOutputStream; 47import java.io.FilterOutputStream; 48import java.io.IOException; 49import java.io.InputStream; 50import java.io.InputStreamReader; 51import java.io.OutputStream; 52import java.io.PrintStream; 53import java.lang.reflect.Constructor; 54import java.security.DigestOutputStream; 55import java.security.GeneralSecurityException; 56import java.security.Key; 57import java.security.KeyFactory; 58import java.security.MessageDigest; 59import java.security.PrivateKey; 60import java.security.Provider; 61import java.security.Security; 62import java.security.cert.CertificateEncodingException; 63import java.security.cert.CertificateFactory; 64import java.security.cert.X509Certificate; 65import java.security.spec.InvalidKeySpecException; 66import java.security.spec.PKCS8EncodedKeySpec; 67import java.util.ArrayList; 68import java.util.Collections; 69import java.util.Enumeration; 70import java.util.Locale; 71import java.util.Map; 72import java.util.TreeMap; 73import java.util.jar.Attributes; 74import java.util.jar.JarEntry; 75import java.util.jar.JarFile; 76import java.util.jar.JarOutputStream; 77import java.util.jar.Manifest; 78import java.util.regex.Pattern; 79import javax.crypto.Cipher; 80import javax.crypto.EncryptedPrivateKeyInfo; 81import javax.crypto.SecretKeyFactory; 82import javax.crypto.spec.PBEKeySpec; 83 84/** 85 * HISTORICAL NOTE: 86 * 87 * Prior to the keylimepie release, SignApk ignored the signature 88 * algorithm specified in the certificate and always used SHA1withRSA. 89 * 90 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 91 * the signature algorithm in the certificate to select which to use 92 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 93 * 94 * Because there are old keys still in use whose certificate actually 95 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 96 * for compatibility with older releases. This can be changed by 97 * altering the getAlgorithm() function below. 98 */ 99 100 101/** 102 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 103 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 104 * SHA-256 (see historical note). 105 */ 106class SignApk { 107 private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 108 private static final String CERT_SIG_NAME = "META-INF/CERT.%s"; 109 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF"; 110 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s"; 111 112 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 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 */ 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. */ 136 private static String getSignatureAlgorithm(X509Certificate cert) { 137 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US); 138 if ("RSA".equalsIgnoreCase(keyType)) { 139 if (getDigestAlgorithm(cert) == USE_SHA256) { 140 return "SHA256withRSA"; 141 } else { 142 return "SHA1withRSA"; 143 } 144 } else if ("EC".equalsIgnoreCase(keyType)) { 145 return "SHA256withECDSA"; 146 } else { 147 throw new IllegalArgumentException("unsupported key type: " + keyType); 148 } 149 } 150 151 // Files matching this pattern are not copied to the output. 152 private static Pattern stripPattern = 153 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + 154 Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 155 156 private static X509Certificate readPublicKey(File file) 157 throws IOException, GeneralSecurityException { 158 FileInputStream input = new FileInputStream(file); 159 try { 160 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 161 return (X509Certificate) cf.generateCertificate(input); 162 } finally { 163 input.close(); 164 } 165 } 166 167 /** 168 * If a console doesn't exist, reads the password from stdin 169 * If a console exists, reads the password from console and returns it as a string. 170 * 171 * @param keyFile The file containing the private key. Used to prompt the user. 172 */ 173 private static String readPassword(File keyFile) { 174 Console console; 175 char[] pwd; 176 if ((console = System.console()) == null) { 177 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 178 System.out.flush(); 179 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 180 try { 181 return stdin.readLine(); 182 } catch (IOException ex) { 183 return null; 184 } 185 } else { 186 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) { 187 return String.valueOf(pwd); 188 } else { 189 return null; 190 } 191 } 192 } 193 194 /** 195 * Decrypt an encrypted PKCS#8 format private key. 196 * 197 * Based on ghstark's post on Aug 6, 2006 at 198 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 199 * 200 * @param encryptedPrivateKey The raw data of the private key 201 * @param keyFile The file containing the private key 202 */ 203 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 204 throws GeneralSecurityException { 205 EncryptedPrivateKeyInfo epkInfo; 206 try { 207 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 208 } catch (IOException ex) { 209 // Probably not an encrypted key. 210 return null; 211 } 212 213 char[] password = readPassword(keyFile).toCharArray(); 214 215 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 216 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 217 218 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 219 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 220 221 try { 222 return epkInfo.getKeySpec(cipher); 223 } catch (InvalidKeySpecException ex) { 224 System.err.println("signapk: Password for " + keyFile + " may be bad."); 225 throw ex; 226 } 227 } 228 229 /** Read a PKCS#8 format private key. */ 230 private static PrivateKey readPrivateKey(File file) 231 throws IOException, GeneralSecurityException { 232 DataInputStream input = new DataInputStream(new FileInputStream(file)); 233 try { 234 byte[] bytes = new byte[(int) file.length()]; 235 input.read(bytes); 236 237 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 238 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); 239 if (spec == null) { 240 spec = new PKCS8EncodedKeySpec(bytes); 241 } 242 243 /* 244 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 245 * OID and use that to construct a KeyFactory. 246 */ 247 PrivateKeyInfo pki; 248 try (ASN1InputStream bIn = 249 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { 250 pki = PrivateKeyInfo.getInstance(bIn.readObject()); 251 } 252 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 253 254 return KeyFactory.getInstance(algOid).generatePrivate(spec); 255 } finally { 256 input.close(); 257 } 258 } 259 260 /** 261 * Add the hash(es) of every file to the manifest, creating it if 262 * necessary. 263 */ 264 private static Manifest addDigestsToManifest(JarFile jar, int hashes) 265 throws IOException, GeneralSecurityException { 266 Manifest input = jar.getManifest(); 267 Manifest output = new Manifest(); 268 Attributes main = output.getMainAttributes(); 269 if (input != null) { 270 main.putAll(input.getMainAttributes()); 271 } else { 272 main.putValue("Manifest-Version", "1.0"); 273 main.putValue("Created-By", "1.0 (Android SignApk)"); 274 } 275 276 MessageDigest md_sha1 = null; 277 MessageDigest md_sha256 = null; 278 if ((hashes & USE_SHA1) != 0) { 279 md_sha1 = MessageDigest.getInstance("SHA1"); 280 } 281 if ((hashes & USE_SHA256) != 0) { 282 md_sha256 = MessageDigest.getInstance("SHA256"); 283 } 284 285 byte[] buffer = new byte[4096]; 286 int num; 287 288 // We sort the input entries by name, and add them to the 289 // output manifest in sorted order. We expect that the output 290 // map will be deterministic. 291 292 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); 293 294 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { 295 JarEntry entry = e.nextElement(); 296 byName.put(entry.getName(), entry); 297 } 298 299 for (JarEntry entry: byName.values()) { 300 String name = entry.getName(); 301 if (!entry.isDirectory() && 302 (stripPattern == null || !stripPattern.matcher(name).matches())) { 303 InputStream data = jar.getInputStream(entry); 304 while ((num = data.read(buffer)) > 0) { 305 if (md_sha1 != null) md_sha1.update(buffer, 0, num); 306 if (md_sha256 != null) md_sha256.update(buffer, 0, num); 307 } 308 309 Attributes attr = null; 310 if (input != null) attr = input.getAttributes(name); 311 attr = attr != null ? new Attributes(attr) : new Attributes(); 312 if (md_sha1 != null) { 313 attr.putValue("SHA1-Digest", 314 new String(Base64.encode(md_sha1.digest()), "ASCII")); 315 } 316 if (md_sha256 != null) { 317 attr.putValue("SHA-256-Digest", 318 new String(Base64.encode(md_sha256.digest()), "ASCII")); 319 } 320 output.getEntries().put(name, attr); 321 } 322 } 323 324 return output; 325 } 326 327 /** 328 * Add a copy of the public key to the archive; this should 329 * exactly match one of the files in 330 * /system/etc/security/otacerts.zip on the device. (The same 331 * cert can be extracted from the CERT.RSA file but this is much 332 * easier to get at.) 333 */ 334 private static void addOtacert(JarOutputStream outputJar, 335 File publicKeyFile, 336 long timestamp, 337 Manifest manifest, 338 int hash) 339 throws IOException, GeneralSecurityException { 340 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256"); 341 342 JarEntry je = new JarEntry(OTACERT_NAME); 343 je.setTime(timestamp); 344 outputJar.putNextEntry(je); 345 FileInputStream input = new FileInputStream(publicKeyFile); 346 byte[] b = new byte[4096]; 347 int read; 348 while ((read = input.read(b)) != -1) { 349 outputJar.write(b, 0, read); 350 md.update(b, 0, read); 351 } 352 input.close(); 353 354 Attributes attr = new Attributes(); 355 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest", 356 new String(Base64.encode(md.digest()), "ASCII")); 357 manifest.getEntries().put(OTACERT_NAME, attr); 358 } 359 360 361 /** Write to another stream and track how many bytes have been 362 * written. 363 */ 364 private static class CountOutputStream extends FilterOutputStream { 365 private int mCount; 366 367 public CountOutputStream(OutputStream out) { 368 super(out); 369 mCount = 0; 370 } 371 372 @Override 373 public void write(int b) throws IOException { 374 super.write(b); 375 mCount++; 376 } 377 378 @Override 379 public void write(byte[] b, int off, int len) throws IOException { 380 super.write(b, off, len); 381 mCount += len; 382 } 383 384 public int size() { 385 return mCount; 386 } 387 } 388 389 /** Write a .SF file with a digest of the specified manifest. */ 390 private static void writeSignatureFile(Manifest manifest, OutputStream out, 391 int hash) 392 throws IOException, GeneralSecurityException { 393 Manifest sf = new Manifest(); 394 Attributes main = sf.getMainAttributes(); 395 main.putValue("Signature-Version", "1.0"); 396 main.putValue("Created-By", "1.0 (Android SignApk)"); 397 398 MessageDigest md = MessageDigest.getInstance( 399 hash == USE_SHA256 ? "SHA256" : "SHA1"); 400 PrintStream print = new PrintStream( 401 new DigestOutputStream(new ByteArrayOutputStream(), md), 402 true, "UTF-8"); 403 404 // Digest of the entire manifest 405 manifest.write(print); 406 print.flush(); 407 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", 408 new String(Base64.encode(md.digest()), "ASCII")); 409 410 Map<String, Attributes> entries = manifest.getEntries(); 411 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 412 // Digest of the manifest stanza for this entry. 413 print.print("Name: " + entry.getKey() + "\r\n"); 414 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 415 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 416 } 417 print.print("\r\n"); 418 print.flush(); 419 420 Attributes sfAttr = new Attributes(); 421 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest", 422 new String(Base64.encode(md.digest()), "ASCII")); 423 sf.getEntries().put(entry.getKey(), sfAttr); 424 } 425 426 CountOutputStream cout = new CountOutputStream(out); 427 sf.write(cout); 428 429 // A bug in the java.util.jar implementation of Android platforms 430 // up to version 1.6 will cause a spurious IOException to be thrown 431 // if the length of the signature file is a multiple of 1024 bytes. 432 // As a workaround, add an extra CRLF in this case. 433 if ((cout.size() % 1024) == 0) { 434 cout.write('\r'); 435 cout.write('\n'); 436 } 437 } 438 439 /** Sign data and write the digital signature to 'out'. */ 440 private static void writeSignatureBlock( 441 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, 442 OutputStream out) 443 throws IOException, 444 CertificateEncodingException, 445 OperatorCreationException, 446 CMSException { 447 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 448 certList.add(publicKey); 449 JcaCertStore certs = new JcaCertStore(certList); 450 451 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 452 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey)) 453 .build(privateKey); 454 gen.addSignerInfoGenerator( 455 new JcaSignerInfoGeneratorBuilder( 456 new JcaDigestCalculatorProviderBuilder() 457 .build()) 458 .setDirectSignature(true) 459 .build(signer, publicKey)); 460 gen.addCertificates(certs); 461 CMSSignedData sigData = gen.generate(data, false); 462 463 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 464 DEROutputStream dos = new DEROutputStream(out); 465 dos.writeObject(asn1.readObject()); 466 } 467 } 468 469 /** 470 * Copy all the files in a manifest from input to output. We set 471 * the modification times in the output to a fixed time, so as to 472 * reduce variation in the output file and make incremental OTAs 473 * more efficient. 474 */ 475 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out, 476 long timestamp, int defaultAlignment) throws IOException { 477 byte[] buffer = new byte[4096]; 478 int num; 479 480 Map<String, Attributes> entries = manifest.getEntries(); 481 ArrayList<String> names = new ArrayList<String>(entries.keySet()); 482 Collections.sort(names); 483 484 boolean firstEntry = true; 485 long offset = 0L; 486 487 // We do the copy in two passes -- first copying all the 488 // entries that are STORED, then copying all the entries that 489 // have any other compression flag (which in practice means 490 // DEFLATED). This groups all the stored entries together at 491 // the start of the file and makes it easier to do alignment 492 // on them (since only stored entries are aligned). 493 494 for (String name : names) { 495 JarEntry inEntry = in.getJarEntry(name); 496 JarEntry outEntry = null; 497 if (inEntry.getMethod() != JarEntry.STORED) continue; 498 // Preserve the STORED method of the input entry. 499 outEntry = new JarEntry(inEntry); 500 outEntry.setTime(timestamp); 501 502 // 'offset' is the offset into the file at which we expect 503 // the file data to begin. This is the value we need to 504 // make a multiple of 'alignement'. 505 offset += JarFile.LOCHDR + outEntry.getName().length(); 506 if (firstEntry) { 507 // The first entry in a jar file has an extra field of 508 // four bytes that you can't get rid of; any extra 509 // data you specify in the JarEntry is appended to 510 // these forced four bytes. This is JAR_MAGIC in 511 // JarOutputStream; the bytes are 0xfeca0000. 512 offset += 4; 513 firstEntry = false; 514 } 515 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 516 if (alignment > 0 && (offset % alignment != 0)) { 517 // Set the "extra data" of the entry to between 1 and 518 // alignment-1 bytes, to make the file data begin at 519 // an aligned offset. 520 int needed = alignment - (int)(offset % alignment); 521 outEntry.setExtra(new byte[needed]); 522 offset += needed; 523 } 524 525 out.putNextEntry(outEntry); 526 527 InputStream data = in.getInputStream(inEntry); 528 while ((num = data.read(buffer)) > 0) { 529 out.write(buffer, 0, num); 530 offset += num; 531 } 532 out.flush(); 533 } 534 535 // Copy all the non-STORED entries. We don't attempt to 536 // maintain the 'offset' variable past this point; we don't do 537 // alignment on these entries. 538 539 for (String name : names) { 540 JarEntry inEntry = in.getJarEntry(name); 541 JarEntry outEntry = null; 542 if (inEntry.getMethod() == JarEntry.STORED) continue; 543 // Create a new entry so that the compressed len is recomputed. 544 outEntry = new JarEntry(name); 545 outEntry.setTime(timestamp); 546 out.putNextEntry(outEntry); 547 548 InputStream data = in.getInputStream(inEntry); 549 while ((num = data.read(buffer)) > 0) { 550 out.write(buffer, 0, num); 551 } 552 out.flush(); 553 } 554 } 555 556 /** 557 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 558 * relative to start of file or {@code 0} if alignment of this entry's data is not important. 559 */ 560 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 561 if (defaultAlignment <= 0) { 562 return 0; 563 } 564 565 if (entryName.endsWith(".so")) { 566 // Align .so contents to memory page boundary to enable memory-mapped 567 // execution. 568 return 4096; 569 } else { 570 return defaultAlignment; 571 } 572 } 573 574 private static class WholeFileSignerOutputStream extends FilterOutputStream { 575 private boolean closing = false; 576 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 577 private OutputStream tee; 578 579 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 580 super(out); 581 this.tee = tee; 582 } 583 584 public void notifyClosing() { 585 closing = true; 586 } 587 588 public void finish() throws IOException { 589 closing = false; 590 591 byte[] data = footer.toByteArray(); 592 if (data.length < 2) 593 throw new IOException("Less than two bytes written to footer"); 594 write(data, 0, data.length - 2); 595 } 596 597 public byte[] getTail() { 598 return footer.toByteArray(); 599 } 600 601 @Override 602 public void write(byte[] b) throws IOException { 603 write(b, 0, b.length); 604 } 605 606 @Override 607 public void write(byte[] b, int off, int len) throws IOException { 608 if (closing) { 609 // if the jar is about to close, save the footer that will be written 610 footer.write(b, off, len); 611 } 612 else { 613 // write to both output streams. out is the CMSTypedData signer and tee is the file. 614 out.write(b, off, len); 615 tee.write(b, off, len); 616 } 617 } 618 619 @Override 620 public void write(int b) throws IOException { 621 if (closing) { 622 // if the jar is about to close, save the footer that will be written 623 footer.write(b); 624 } 625 else { 626 // write to both output streams. out is the CMSTypedData signer and tee is the file. 627 out.write(b); 628 tee.write(b); 629 } 630 } 631 } 632 633 private static class CMSSigner implements CMSTypedData { 634 private JarFile inputJar; 635 private File publicKeyFile; 636 private X509Certificate publicKey; 637 private PrivateKey privateKey; 638 private OutputStream outputStream; 639 private final ASN1ObjectIdentifier type; 640 private WholeFileSignerOutputStream signer; 641 642 public CMSSigner(JarFile inputJar, File publicKeyFile, 643 X509Certificate publicKey, PrivateKey privateKey, 644 OutputStream outputStream) { 645 this.inputJar = inputJar; 646 this.publicKeyFile = publicKeyFile; 647 this.publicKey = publicKey; 648 this.privateKey = privateKey; 649 this.outputStream = outputStream; 650 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 651 } 652 653 /** 654 * This should actually return byte[] or something similar, but nothing 655 * actually checks it currently. 656 */ 657 @Override 658 public Object getContent() { 659 return this; 660 } 661 662 @Override 663 public ASN1ObjectIdentifier getContentType() { 664 return type; 665 } 666 667 @Override 668 public void write(OutputStream out) throws IOException { 669 try { 670 signer = new WholeFileSignerOutputStream(out, outputStream); 671 JarOutputStream outputJar = new JarOutputStream(signer); 672 673 int hash = getDigestAlgorithm(publicKey); 674 675 // Assume the certificate is valid for at least an hour. 676 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 677 678 Manifest manifest = addDigestsToManifest(inputJar, hash); 679 copyFiles(manifest, inputJar, outputJar, timestamp, 0); 680 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash); 681 682 signFile(manifest, 683 new X509Certificate[]{ publicKey }, 684 new PrivateKey[]{ privateKey }, 685 outputJar); 686 687 signer.notifyClosing(); 688 outputJar.close(); 689 signer.finish(); 690 } 691 catch (Exception e) { 692 throw new IOException(e); 693 } 694 } 695 696 public void writeSignatureBlock(ByteArrayOutputStream temp) 697 throws IOException, 698 CertificateEncodingException, 699 OperatorCreationException, 700 CMSException { 701 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp); 702 } 703 704 public WholeFileSignerOutputStream getSigner() { 705 return signer; 706 } 707 } 708 709 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 710 X509Certificate publicKey, PrivateKey privateKey, 711 OutputStream outputStream) throws Exception { 712 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 713 publicKey, privateKey, outputStream); 714 715 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 716 717 // put a readable message and a null char at the start of the 718 // archive comment, so that tools that display the comment 719 // (hopefully) show something sensible. 720 // TODO: anything more useful we can put in this message? 721 byte[] message = "signed by SignApk".getBytes("UTF-8"); 722 temp.write(message); 723 temp.write(0); 724 725 cmsOut.writeSignatureBlock(temp); 726 727 byte[] zipData = cmsOut.getSigner().getTail(); 728 729 // For a zip with no archive comment, the 730 // end-of-central-directory record will be 22 bytes long, so 731 // we expect to find the EOCD marker 22 bytes from the end. 732 if (zipData[zipData.length-22] != 0x50 || 733 zipData[zipData.length-21] != 0x4b || 734 zipData[zipData.length-20] != 0x05 || 735 zipData[zipData.length-19] != 0x06) { 736 throw new IllegalArgumentException("zip data already has an archive comment"); 737 } 738 739 int total_size = temp.size() + 6; 740 if (total_size > 0xffff) { 741 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 742 } 743 // signature starts this many bytes from the end of the file 744 int signature_start = total_size - message.length - 1; 745 temp.write(signature_start & 0xff); 746 temp.write((signature_start >> 8) & 0xff); 747 // Why the 0xff bytes? In a zip file with no archive comment, 748 // bytes [-6:-2] of the file are the little-endian offset from 749 // the start of the file to the central directory. So for the 750 // two high bytes to be 0xff 0xff, the archive would have to 751 // be nearly 4GB in size. So it's unlikely that a real 752 // commentless archive would have 0xffs here, and lets us tell 753 // an old signed archive from a new one. 754 temp.write(0xff); 755 temp.write(0xff); 756 temp.write(total_size & 0xff); 757 temp.write((total_size >> 8) & 0xff); 758 temp.flush(); 759 760 // Signature verification checks that the EOCD header is the 761 // last such sequence in the file (to avoid minzip finding a 762 // fake EOCD appended after the signature in its scan). The 763 // odds of producing this sequence by chance are very low, but 764 // let's catch it here if it does. 765 byte[] b = temp.toByteArray(); 766 for (int i = 0; i < b.length-3; ++i) { 767 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 768 throw new IllegalArgumentException("found spurious EOCD header at " + i); 769 } 770 } 771 772 outputStream.write(total_size & 0xff); 773 outputStream.write((total_size >> 8) & 0xff); 774 temp.writeTo(outputStream); 775 } 776 777 private static void signFile(Manifest manifest, 778 X509Certificate[] publicKey, PrivateKey[] privateKey, 779 JarOutputStream outputJar) 780 throws Exception { 781 // Assume the certificate is valid for at least an hour. 782 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 783 784 // MANIFEST.MF 785 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); 786 je.setTime(timestamp); 787 outputJar.putNextEntry(je); 788 manifest.write(outputJar); 789 790 int numKeys = publicKey.length; 791 for (int k = 0; k < numKeys; ++k) { 792 // CERT.SF / CERT#.SF 793 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : 794 (String.format(CERT_SF_MULTI_NAME, k))); 795 je.setTime(timestamp); 796 outputJar.putNextEntry(je); 797 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 798 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k])); 799 byte[] signedData = baos.toByteArray(); 800 outputJar.write(signedData); 801 802 // CERT.{EC,RSA} / CERT#.{EC,RSA} 803 final String keyType = publicKey[k].getPublicKey().getAlgorithm(); 804 je = new JarEntry(numKeys == 1 ? 805 (String.format(CERT_SIG_NAME, keyType)) : 806 (String.format(CERT_SIG_MULTI_NAME, k, keyType))); 807 je.setTime(timestamp); 808 outputJar.putNextEntry(je); 809 writeSignatureBlock(new CMSProcessableByteArray(signedData), 810 publicKey[k], privateKey[k], outputJar); 811 } 812 } 813 814 /** 815 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 816 * types that might be stored in PKCS#11-like storage. 817 */ 818 private static void loadProviderIfNecessary(String providerClassName) { 819 if (providerClassName == null) { 820 return; 821 } 822 823 final Class<?> klass; 824 try { 825 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 826 if (sysLoader != null) { 827 klass = sysLoader.loadClass(providerClassName); 828 } else { 829 klass = Class.forName(providerClassName); 830 } 831 } catch (ClassNotFoundException e) { 832 e.printStackTrace(); 833 System.exit(1); 834 return; 835 } 836 837 Constructor<?> constructor = null; 838 for (Constructor<?> c : klass.getConstructors()) { 839 if (c.getParameterTypes().length == 0) { 840 constructor = c; 841 break; 842 } 843 } 844 if (constructor == null) { 845 System.err.println("No zero-arg constructor found for " + providerClassName); 846 System.exit(1); 847 return; 848 } 849 850 final Object o; 851 try { 852 o = constructor.newInstance(); 853 } catch (Exception e) { 854 e.printStackTrace(); 855 System.exit(1); 856 return; 857 } 858 if (!(o instanceof Provider)) { 859 System.err.println("Not a Provider class: " + providerClassName); 860 System.exit(1); 861 } 862 863 Security.insertProviderAt((Provider) o, 1); 864 } 865 866 private static void usage() { 867 System.err.println("Usage: signapk [-w] " + 868 "[-a <alignment>] " + 869 "[-providerClass <className>] " + 870 "publickey.x509[.pem] privatekey.pk8 " + 871 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 872 "input.jar output.jar"); 873 System.exit(2); 874 } 875 876 public static void main(String[] args) { 877 if (args.length < 4) usage(); 878 879 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than 880 // the standard or Bouncy Castle ones. 881 Security.insertProviderAt(new OpenSSLProvider(), 1); 882 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer 883 // DSA which may still be needed. 884 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. 885 Security.addProvider(new BouncyCastleProvider()); 886 887 boolean signWholeFile = false; 888 String providerClass = null; 889 int alignment = 4; 890 891 int argstart = 0; 892 while (argstart < args.length && args[argstart].startsWith("-")) { 893 if ("-w".equals(args[argstart])) { 894 signWholeFile = true; 895 ++argstart; 896 } else if ("-providerClass".equals(args[argstart])) { 897 if (argstart + 1 >= args.length) { 898 usage(); 899 } 900 providerClass = args[++argstart]; 901 ++argstart; 902 } else if ("-a".equals(args[argstart])) { 903 alignment = Integer.parseInt(args[++argstart]); 904 ++argstart; 905 } else { 906 usage(); 907 } 908 } 909 910 if ((args.length - argstart) % 2 == 1) usage(); 911 int numKeys = ((args.length - argstart) / 2) - 1; 912 if (signWholeFile && numKeys > 1) { 913 System.err.println("Only one key may be used with -w."); 914 System.exit(2); 915 } 916 917 loadProviderIfNecessary(providerClass); 918 919 String inputFilename = args[args.length-2]; 920 String outputFilename = args[args.length-1]; 921 922 JarFile inputJar = null; 923 FileOutputStream outputFile = null; 924 int hashes = 0; 925 926 try { 927 File firstPublicKeyFile = new File(args[argstart+0]); 928 929 X509Certificate[] publicKey = new X509Certificate[numKeys]; 930 try { 931 for (int i = 0; i < numKeys; ++i) { 932 int argNum = argstart + i*2; 933 publicKey[i] = readPublicKey(new File(args[argNum])); 934 hashes |= getDigestAlgorithm(publicKey[i]); 935 } 936 } catch (IllegalArgumentException e) { 937 System.err.println(e); 938 System.exit(1); 939 } 940 941 // Set the ZIP file timestamp to the starting valid time 942 // of the 0th certificate plus one hour (to match what 943 // we've historically done). 944 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; 945 946 PrivateKey[] privateKey = new PrivateKey[numKeys]; 947 for (int i = 0; i < numKeys; ++i) { 948 int argNum = argstart + i*2 + 1; 949 privateKey[i] = readPrivateKey(new File(args[argNum])); 950 } 951 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 952 953 outputFile = new FileOutputStream(outputFilename); 954 955 956 if (signWholeFile) { 957 SignApk.signWholeFile(inputJar, firstPublicKeyFile, 958 publicKey[0], privateKey[0], outputFile); 959 } else { 960 JarOutputStream outputJar = new JarOutputStream(outputFile); 961 962 // For signing .apks, use the maximum compression to make 963 // them as small as possible (since they live forever on 964 // the system partition). For OTA packages, use the 965 // default compression level, which is much much faster 966 // and produces output that is only a tiny bit larger 967 // (~0.1% on full OTA packages I tested). 968 outputJar.setLevel(9); 969 970 Manifest manifest = addDigestsToManifest(inputJar, hashes); 971 copyFiles(manifest, inputJar, outputJar, timestamp, alignment); 972 signFile(manifest, publicKey, privateKey, outputJar); 973 outputJar.close(); 974 } 975 } catch (Exception e) { 976 e.printStackTrace(); 977 System.exit(1); 978 } finally { 979 try { 980 if (inputJar != null) inputJar.close(); 981 if (outputFile != null) outputFile.close(); 982 } catch (IOException e) { 983 e.printStackTrace(); 984 System.exit(1); 985 } 986 } 987 } 988} 989