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