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