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