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 sun.misc.BASE64Encoder; 20 import sun.security.pkcs.ContentInfo; 21 import sun.security.pkcs.PKCS7; 22 import sun.security.pkcs.SignerInfo; 23 import sun.security.x509.AlgorithmId; 24 import sun.security.x509.X500Name; 25 26 import java.io.BufferedReader; 27 import java.io.ByteArrayOutputStream; 28 import java.io.DataInputStream; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.FilterOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.InputStreamReader; 36 import java.io.OutputStream; 37 import java.io.PrintStream; 38 import java.security.AlgorithmParameters; 39 import java.security.DigestOutputStream; 40 import java.security.GeneralSecurityException; 41 import java.security.Key; 42 import java.security.KeyFactory; 43 import java.security.MessageDigest; 44 import java.security.PrivateKey; 45 import java.security.Signature; 46 import java.security.SignatureException; 47 import java.security.cert.Certificate; 48 import java.security.cert.CertificateFactory; 49 import java.security.cert.X509Certificate; 50 import java.security.spec.InvalidKeySpecException; 51 import java.security.spec.KeySpec; 52 import java.security.spec.PKCS8EncodedKeySpec; 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.Date; 56 import java.util.Enumeration; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.TreeMap; 60 import java.util.jar.Attributes; 61 import java.util.jar.JarEntry; 62 import java.util.jar.JarFile; 63 import java.util.jar.JarOutputStream; 64 import java.util.jar.Manifest; 65 import java.util.regex.Pattern; 66 import javax.crypto.Cipher; 67 import javax.crypto.EncryptedPrivateKeyInfo; 68 import javax.crypto.SecretKeyFactory; 69 import javax.crypto.spec.PBEKeySpec; 70 71 /** 72 * Command line tool to sign JAR files (including APKs and OTA updates) in 73 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. 74 */ 75 class SignApk { 76 private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 77 private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; 78 79 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 80 81 // Files matching this pattern are not copied to the output. 82 private static Pattern stripPattern = 83 Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$"); 84 readPublicKey(File file)85 private static X509Certificate readPublicKey(File file) 86 throws IOException, GeneralSecurityException { 87 FileInputStream input = new FileInputStream(file); 88 try { 89 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 90 return (X509Certificate) cf.generateCertificate(input); 91 } finally { 92 input.close(); 93 } 94 } 95 96 /** 97 * Reads the password from stdin and returns it as a string. 98 * 99 * @param keyFile The file containing the private key. Used to prompt the user. 100 */ readPassword(File keyFile)101 private static String readPassword(File keyFile) { 102 // TODO: use Console.readPassword() when it's available. 103 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 104 System.out.flush(); 105 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 106 try { 107 return stdin.readLine(); 108 } catch (IOException ex) { 109 return null; 110 } 111 } 112 113 /** 114 * Decrypt an encrypted PKCS 8 format private key. 115 * 116 * Based on ghstark's post on Aug 6, 2006 at 117 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 118 * 119 * @param encryptedPrivateKey The raw data of the private key 120 * @param keyFile The file containing the private key 121 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)122 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 123 throws GeneralSecurityException { 124 EncryptedPrivateKeyInfo epkInfo; 125 try { 126 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 127 } catch (IOException ex) { 128 // Probably not an encrypted key. 129 return null; 130 } 131 132 char[] password = readPassword(keyFile).toCharArray(); 133 134 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 135 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 136 137 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 138 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 139 140 try { 141 return epkInfo.getKeySpec(cipher); 142 } catch (InvalidKeySpecException ex) { 143 System.err.println("signapk: Password for " + keyFile + " may be bad."); 144 throw ex; 145 } 146 } 147 148 /** Read a PKCS 8 format private key. */ readPrivateKey(File file)149 private static PrivateKey readPrivateKey(File file) 150 throws IOException, GeneralSecurityException { 151 DataInputStream input = new DataInputStream(new FileInputStream(file)); 152 try { 153 byte[] bytes = new byte[(int) file.length()]; 154 input.read(bytes); 155 156 KeySpec spec = decryptPrivateKey(bytes, file); 157 if (spec == null) { 158 spec = new PKCS8EncodedKeySpec(bytes); 159 } 160 161 try { 162 return KeyFactory.getInstance("RSA").generatePrivate(spec); 163 } catch (InvalidKeySpecException ex) { 164 return KeyFactory.getInstance("DSA").generatePrivate(spec); 165 } 166 } finally { 167 input.close(); 168 } 169 } 170 171 /** Add the SHA1 of every file to the manifest, creating it if necessary. */ addDigestsToManifest(JarFile jar)172 private static Manifest addDigestsToManifest(JarFile jar) 173 throws IOException, GeneralSecurityException { 174 Manifest input = jar.getManifest(); 175 Manifest output = new Manifest(); 176 Attributes main = output.getMainAttributes(); 177 if (input != null) { 178 main.putAll(input.getMainAttributes()); 179 } else { 180 main.putValue("Manifest-Version", "1.0"); 181 main.putValue("Created-By", "1.0 (Android SignApk)"); 182 } 183 184 BASE64Encoder base64 = new BASE64Encoder(); 185 MessageDigest md = MessageDigest.getInstance("SHA1"); 186 byte[] buffer = new byte[4096]; 187 int num; 188 189 // We sort the input entries by name, and add them to the 190 // output manifest in sorted order. We expect that the output 191 // map will be deterministic. 192 193 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); 194 195 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { 196 JarEntry entry = e.nextElement(); 197 byName.put(entry.getName(), entry); 198 } 199 200 for (JarEntry entry: byName.values()) { 201 String name = entry.getName(); 202 if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && 203 !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) && 204 !name.equals(OTACERT_NAME) && 205 (stripPattern == null || 206 !stripPattern.matcher(name).matches())) { 207 InputStream data = jar.getInputStream(entry); 208 while ((num = data.read(buffer)) > 0) { 209 md.update(buffer, 0, num); 210 } 211 212 Attributes attr = null; 213 if (input != null) attr = input.getAttributes(name); 214 attr = attr != null ? new Attributes(attr) : new Attributes(); 215 attr.putValue("SHA1-Digest", base64.encode(md.digest())); 216 output.getEntries().put(name, attr); 217 } 218 } 219 220 return output; 221 } 222 223 /** 224 * Add a copy of the public key to the archive; this should 225 * exactly match one of the files in 226 * /system/etc/security/otacerts.zip on the device. (The same 227 * cert can be extracted from the CERT.RSA file but this is much 228 * easier to get at.) 229 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest)230 private static void addOtacert(JarOutputStream outputJar, 231 File publicKeyFile, 232 long timestamp, 233 Manifest manifest) 234 throws IOException, GeneralSecurityException { 235 BASE64Encoder base64 = new BASE64Encoder(); 236 MessageDigest md = MessageDigest.getInstance("SHA1"); 237 238 JarEntry je = new JarEntry(OTACERT_NAME); 239 je.setTime(timestamp); 240 outputJar.putNextEntry(je); 241 FileInputStream input = new FileInputStream(publicKeyFile); 242 byte[] b = new byte[4096]; 243 int read; 244 while ((read = input.read(b)) != -1) { 245 outputJar.write(b, 0, read); 246 md.update(b, 0, read); 247 } 248 input.close(); 249 250 Attributes attr = new Attributes(); 251 attr.putValue("SHA1-Digest", base64.encode(md.digest())); 252 manifest.getEntries().put(OTACERT_NAME, attr); 253 } 254 255 256 /** Write to another stream and also feed it to the Signature object. */ 257 private static class SignatureOutputStream extends FilterOutputStream { 258 private Signature mSignature; 259 private int mCount; 260 SignatureOutputStream(OutputStream out, Signature sig)261 public SignatureOutputStream(OutputStream out, Signature sig) { 262 super(out); 263 mSignature = sig; 264 mCount = 0; 265 } 266 267 @Override write(int b)268 public void write(int b) throws IOException { 269 try { 270 mSignature.update((byte) b); 271 } catch (SignatureException e) { 272 throw new IOException("SignatureException: " + e); 273 } 274 super.write(b); 275 mCount++; 276 } 277 278 @Override write(byte[] b, int off, int len)279 public void write(byte[] b, int off, int len) throws IOException { 280 try { 281 mSignature.update(b, off, len); 282 } catch (SignatureException e) { 283 throw new IOException("SignatureException: " + e); 284 } 285 super.write(b, off, len); 286 mCount += len; 287 } 288 size()289 public int size() { 290 return mCount; 291 } 292 } 293 294 /** Write a .SF file with a digest of the specified manifest. */ writeSignatureFile(Manifest manifest, SignatureOutputStream out)295 private static void writeSignatureFile(Manifest manifest, SignatureOutputStream out) 296 throws IOException, GeneralSecurityException { 297 Manifest sf = new Manifest(); 298 Attributes main = sf.getMainAttributes(); 299 main.putValue("Signature-Version", "1.0"); 300 main.putValue("Created-By", "1.0 (Android SignApk)"); 301 302 BASE64Encoder base64 = new BASE64Encoder(); 303 MessageDigest md = MessageDigest.getInstance("SHA1"); 304 PrintStream print = new PrintStream( 305 new DigestOutputStream(new ByteArrayOutputStream(), md), 306 true, "UTF-8"); 307 308 // Digest of the entire manifest 309 manifest.write(print); 310 print.flush(); 311 main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest())); 312 313 Map<String, Attributes> entries = manifest.getEntries(); 314 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 315 // Digest of the manifest stanza for this entry. 316 print.print("Name: " + entry.getKey() + "\r\n"); 317 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 318 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 319 } 320 print.print("\r\n"); 321 print.flush(); 322 323 Attributes sfAttr = new Attributes(); 324 sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); 325 sf.getEntries().put(entry.getKey(), sfAttr); 326 } 327 328 sf.write(out); 329 330 // A bug in the java.util.jar implementation of Android platforms 331 // up to version 1.6 will cause a spurious IOException to be thrown 332 // if the length of the signature file is a multiple of 1024 bytes. 333 // As a workaround, add an extra CRLF in this case. 334 if ((out.size() % 1024) == 0) { 335 out.write('\r'); 336 out.write('\n'); 337 } 338 } 339 340 /** Write a .RSA file with a digital signature. */ writeSignatureBlock( Signature signature, X509Certificate publicKey, OutputStream out)341 private static void writeSignatureBlock( 342 Signature signature, X509Certificate publicKey, OutputStream out) 343 throws IOException, GeneralSecurityException { 344 SignerInfo signerInfo = new SignerInfo( 345 new X500Name(publicKey.getIssuerX500Principal().getName()), 346 publicKey.getSerialNumber(), 347 AlgorithmId.get("SHA1"), 348 AlgorithmId.get("RSA"), 349 signature.sign()); 350 351 PKCS7 pkcs7 = new PKCS7( 352 new AlgorithmId[] { AlgorithmId.get("SHA1") }, 353 new ContentInfo(ContentInfo.DATA_OID, null), 354 new X509Certificate[] { publicKey }, 355 new SignerInfo[] { signerInfo }); 356 357 pkcs7.encodeSignedData(out); 358 } 359 signWholeOutputFile(byte[] zipData, OutputStream outputStream, X509Certificate publicKey, PrivateKey privateKey)360 private static void signWholeOutputFile(byte[] zipData, 361 OutputStream outputStream, 362 X509Certificate publicKey, 363 PrivateKey privateKey) 364 throws IOException, GeneralSecurityException { 365 366 // For a zip with no archive comment, the 367 // end-of-central-directory record will be 22 bytes long, so 368 // we expect to find the EOCD marker 22 bytes from the end. 369 if (zipData[zipData.length-22] != 0x50 || 370 zipData[zipData.length-21] != 0x4b || 371 zipData[zipData.length-20] != 0x05 || 372 zipData[zipData.length-19] != 0x06) { 373 throw new IllegalArgumentException("zip data already has an archive comment"); 374 } 375 376 Signature signature = Signature.getInstance("SHA1withRSA"); 377 signature.initSign(privateKey); 378 signature.update(zipData, 0, zipData.length-2); 379 380 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 381 382 // put a readable message and a null char at the start of the 383 // archive comment, so that tools that display the comment 384 // (hopefully) show something sensible. 385 // TODO: anything more useful we can put in this message? 386 byte[] message = "signed by SignApk".getBytes("UTF-8"); 387 temp.write(message); 388 temp.write(0); 389 writeSignatureBlock(signature, publicKey, temp); 390 int total_size = temp.size() + 6; 391 if (total_size > 0xffff) { 392 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 393 } 394 // signature starts this many bytes from the end of the file 395 int signature_start = total_size - message.length - 1; 396 temp.write(signature_start & 0xff); 397 temp.write((signature_start >> 8) & 0xff); 398 // Why the 0xff bytes? In a zip file with no archive comment, 399 // bytes [-6:-2] of the file are the little-endian offset from 400 // the start of the file to the central directory. So for the 401 // two high bytes to be 0xff 0xff, the archive would have to 402 // be nearly 4GB in side. So it's unlikely that a real 403 // commentless archive would have 0xffs here, and lets us tell 404 // an old signed archive from a new one. 405 temp.write(0xff); 406 temp.write(0xff); 407 temp.write(total_size & 0xff); 408 temp.write((total_size >> 8) & 0xff); 409 temp.flush(); 410 411 // Signature verification checks that the EOCD header is the 412 // last such sequence in the file (to avoid minzip finding a 413 // fake EOCD appended after the signature in its scan). The 414 // odds of producing this sequence by chance are very low, but 415 // let's catch it here if it does. 416 byte[] b = temp.toByteArray(); 417 for (int i = 0; i < b.length-3; ++i) { 418 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 419 throw new IllegalArgumentException("found spurious EOCD header at " + i); 420 } 421 } 422 423 outputStream.write(zipData, 0, zipData.length-2); 424 outputStream.write(total_size & 0xff); 425 outputStream.write((total_size >> 8) & 0xff); 426 temp.writeTo(outputStream); 427 } 428 429 /** 430 * Copy all the files in a manifest from input to output. We set 431 * the modification times in the output to a fixed time, so as to 432 * reduce variation in the output file and make incremental OTAs 433 * more efficient. 434 */ copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp)435 private static void copyFiles(Manifest manifest, 436 JarFile in, JarOutputStream out, long timestamp) throws IOException { 437 byte[] buffer = new byte[4096]; 438 int num; 439 440 Map<String, Attributes> entries = manifest.getEntries(); 441 List<String> names = new ArrayList(entries.keySet()); 442 Collections.sort(names); 443 for (String name : names) { 444 JarEntry inEntry = in.getJarEntry(name); 445 JarEntry outEntry = null; 446 if (inEntry.getMethod() == JarEntry.STORED) { 447 // Preserve the STORED method of the input entry. 448 outEntry = new JarEntry(inEntry); 449 } else { 450 // Create a new entry so that the compressed len is recomputed. 451 outEntry = new JarEntry(name); 452 } 453 outEntry.setTime(timestamp); 454 out.putNextEntry(outEntry); 455 456 InputStream data = in.getInputStream(inEntry); 457 while ((num = data.read(buffer)) > 0) { 458 out.write(buffer, 0, num); 459 } 460 out.flush(); 461 } 462 } 463 main(String[] args)464 public static void main(String[] args) { 465 if (args.length != 4 && args.length != 5) { 466 System.err.println("Usage: signapk [-w] " + 467 "publickey.x509[.pem] privatekey.pk8 " + 468 "input.jar output.jar"); 469 System.exit(2); 470 } 471 472 boolean signWholeFile = false; 473 int argstart = 0; 474 if (args[0].equals("-w")) { 475 signWholeFile = true; 476 argstart = 1; 477 } 478 479 JarFile inputJar = null; 480 JarOutputStream outputJar = null; 481 FileOutputStream outputFile = null; 482 483 try { 484 File publicKeyFile = new File(args[argstart+0]); 485 X509Certificate publicKey = readPublicKey(publicKeyFile); 486 487 // Assume the certificate is valid for at least an hour. 488 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 489 490 PrivateKey privateKey = readPrivateKey(new File(args[argstart+1])); 491 inputJar = new JarFile(new File(args[argstart+2]), false); // Don't verify. 492 493 OutputStream outputStream = null; 494 if (signWholeFile) { 495 outputStream = new ByteArrayOutputStream(); 496 } else { 497 outputStream = outputFile = new FileOutputStream(args[argstart+3]); 498 } 499 outputJar = new JarOutputStream(outputStream); 500 outputJar.setLevel(9); 501 502 JarEntry je; 503 504 Manifest manifest = addDigestsToManifest(inputJar); 505 506 // Everything else 507 copyFiles(manifest, inputJar, outputJar, timestamp); 508 509 // otacert 510 if (signWholeFile) { 511 addOtacert(outputJar, publicKeyFile, timestamp, manifest); 512 } 513 514 // MANIFEST.MF 515 je = new JarEntry(JarFile.MANIFEST_NAME); 516 je.setTime(timestamp); 517 outputJar.putNextEntry(je); 518 manifest.write(outputJar); 519 520 // CERT.SF 521 Signature signature = Signature.getInstance("SHA1withRSA"); 522 signature.initSign(privateKey); 523 je = new JarEntry(CERT_SF_NAME); 524 je.setTime(timestamp); 525 outputJar.putNextEntry(je); 526 writeSignatureFile(manifest, 527 new SignatureOutputStream(outputJar, signature)); 528 529 // CERT.RSA 530 je = new JarEntry(CERT_RSA_NAME); 531 je.setTime(timestamp); 532 outputJar.putNextEntry(je); 533 writeSignatureBlock(signature, publicKey, outputJar); 534 535 outputJar.close(); 536 outputJar = null; 537 outputStream.flush(); 538 539 if (signWholeFile) { 540 outputFile = new FileOutputStream(args[argstart+3]); 541 signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(), 542 outputFile, publicKey, privateKey); 543 } 544 } catch (Exception e) { 545 e.printStackTrace(); 546 System.exit(1); 547 } finally { 548 try { 549 if (inputJar != null) inputJar.close(); 550 if (outputFile != null) outputFile.close(); 551 } catch (IOException e) { 552 e.printStackTrace(); 553 System.exit(1); 554 } 555 } 556 } 557 } 558