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 // Files matching this pattern are not copied to the output. 80 private static Pattern stripPattern = 81 Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$"); 82 readPublicKey(File file)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 */ readPassword(File keyFile)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 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)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. */ readPrivateKey(File file)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. */ addDigestsToManifest(JarFile jar)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 private int mCount; 224 SignatureOutputStream(OutputStream out, Signature sig)225 public SignatureOutputStream(OutputStream out, Signature sig) { 226 super(out); 227 mSignature = sig; 228 mCount = 0; 229 } 230 231 @Override write(int b)232 public void write(int b) throws IOException { 233 try { 234 mSignature.update((byte) b); 235 } catch (SignatureException e) { 236 throw new IOException("SignatureException: " + e); 237 } 238 super.write(b); 239 mCount++; 240 } 241 242 @Override write(byte[] b, int off, int len)243 public void write(byte[] b, int off, int len) throws IOException { 244 try { 245 mSignature.update(b, off, len); 246 } catch (SignatureException e) { 247 throw new IOException("SignatureException: " + e); 248 } 249 super.write(b, off, len); 250 mCount += len; 251 } 252 size()253 public int size() { 254 return mCount; 255 } 256 } 257 258 /** Write a .SF file with a digest of the specified manifest. */ writeSignatureFile(Manifest manifest, SignatureOutputStream out)259 private static void writeSignatureFile(Manifest manifest, SignatureOutputStream out) 260 throws IOException, GeneralSecurityException { 261 Manifest sf = new Manifest(); 262 Attributes main = sf.getMainAttributes(); 263 main.putValue("Signature-Version", "1.0"); 264 main.putValue("Created-By", "1.0 (Android SignApk)"); 265 266 BASE64Encoder base64 = new BASE64Encoder(); 267 MessageDigest md = MessageDigest.getInstance("SHA1"); 268 PrintStream print = new PrintStream( 269 new DigestOutputStream(new ByteArrayOutputStream(), md), 270 true, "UTF-8"); 271 272 // Digest of the entire manifest 273 manifest.write(print); 274 print.flush(); 275 main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest())); 276 277 Map<String, Attributes> entries = manifest.getEntries(); 278 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 279 // Digest of the manifest stanza for this entry. 280 print.print("Name: " + entry.getKey() + "\r\n"); 281 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 282 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 283 } 284 print.print("\r\n"); 285 print.flush(); 286 287 Attributes sfAttr = new Attributes(); 288 sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); 289 sf.getEntries().put(entry.getKey(), sfAttr); 290 } 291 292 sf.write(out); 293 294 // A bug in the java.util.jar implementation of Android platforms 295 // up to version 1.6 will cause a spurious IOException to be thrown 296 // if the length of the signature file is a multiple of 1024 bytes. 297 // As a workaround, add an extra CRLF in this case. 298 if ((out.size() % 1024) == 0) { 299 out.write('\r'); 300 out.write('\n'); 301 } 302 } 303 304 /** Write a .RSA file with a digital signature. */ writeSignatureBlock( Signature signature, X509Certificate publicKey, OutputStream out)305 private static void writeSignatureBlock( 306 Signature signature, X509Certificate publicKey, OutputStream out) 307 throws IOException, GeneralSecurityException { 308 SignerInfo signerInfo = new SignerInfo( 309 new X500Name(publicKey.getIssuerX500Principal().getName()), 310 publicKey.getSerialNumber(), 311 AlgorithmId.get("SHA1"), 312 AlgorithmId.get("RSA"), 313 signature.sign()); 314 315 PKCS7 pkcs7 = new PKCS7( 316 new AlgorithmId[] { AlgorithmId.get("SHA1") }, 317 new ContentInfo(ContentInfo.DATA_OID, null), 318 new X509Certificate[] { publicKey }, 319 new SignerInfo[] { signerInfo }); 320 321 pkcs7.encodeSignedData(out); 322 } 323 signWholeOutputFile(byte[] zipData, OutputStream outputStream, X509Certificate publicKey, PrivateKey privateKey)324 private static void signWholeOutputFile(byte[] zipData, 325 OutputStream outputStream, 326 X509Certificate publicKey, 327 PrivateKey privateKey) 328 throws IOException, GeneralSecurityException { 329 330 // For a zip with no archive comment, the 331 // end-of-central-directory record will be 22 bytes long, so 332 // we expect to find the EOCD marker 22 bytes from the end. 333 if (zipData[zipData.length-22] != 0x50 || 334 zipData[zipData.length-21] != 0x4b || 335 zipData[zipData.length-20] != 0x05 || 336 zipData[zipData.length-19] != 0x06) { 337 throw new IllegalArgumentException("zip data already has an archive comment"); 338 } 339 340 Signature signature = Signature.getInstance("SHA1withRSA"); 341 signature.initSign(privateKey); 342 signature.update(zipData, 0, zipData.length-2); 343 344 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 345 346 // put a readable message and a null char at the start of the 347 // archive comment, so that tools that display the comment 348 // (hopefully) show something sensible. 349 // TODO: anything more useful we can put in this message? 350 byte[] message = "signed by SignApk".getBytes("UTF-8"); 351 temp.write(message); 352 temp.write(0); 353 writeSignatureBlock(signature, publicKey, temp); 354 int total_size = temp.size() + 6; 355 if (total_size > 0xffff) { 356 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 357 } 358 // signature starts this many bytes from the end of the file 359 int signature_start = total_size - message.length - 1; 360 temp.write(signature_start & 0xff); 361 temp.write((signature_start >> 8) & 0xff); 362 // Why the 0xff bytes? In a zip file with no archive comment, 363 // bytes [-6:-2] of the file are the little-endian offset from 364 // the start of the file to the central directory. So for the 365 // two high bytes to be 0xff 0xff, the archive would have to 366 // be nearly 4GB in side. So it's unlikely that a real 367 // commentless archive would have 0xffs here, and lets us tell 368 // an old signed archive from a new one. 369 temp.write(0xff); 370 temp.write(0xff); 371 temp.write(total_size & 0xff); 372 temp.write((total_size >> 8) & 0xff); 373 temp.flush(); 374 375 // Signature verification checks that the EOCD header is the 376 // last such sequence in the file (to avoid minzip finding a 377 // fake EOCD appended after the signature in its scan). The 378 // odds of producing this sequence by chance are very low, but 379 // let's catch it here if it does. 380 byte[] b = temp.toByteArray(); 381 for (int i = 0; i < b.length-3; ++i) { 382 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 383 throw new IllegalArgumentException("found spurious EOCD header at " + i); 384 } 385 } 386 387 outputStream.write(zipData, 0, zipData.length-2); 388 outputStream.write(total_size & 0xff); 389 outputStream.write((total_size >> 8) & 0xff); 390 temp.writeTo(outputStream); 391 } 392 393 /** 394 * Copy all the files in a manifest from input to output. We set 395 * the modification times in the output to a fixed time, so as to 396 * reduce variation in the output file and make incremental OTAs 397 * more efficient. 398 */ copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp)399 private static void copyFiles(Manifest manifest, 400 JarFile in, JarOutputStream out, long timestamp) throws IOException { 401 byte[] buffer = new byte[4096]; 402 int num; 403 404 Map<String, Attributes> entries = manifest.getEntries(); 405 List<String> names = new ArrayList(entries.keySet()); 406 Collections.sort(names); 407 for (String name : names) { 408 JarEntry inEntry = in.getJarEntry(name); 409 JarEntry outEntry = null; 410 if (inEntry.getMethod() == JarEntry.STORED) { 411 // Preserve the STORED method of the input entry. 412 outEntry = new JarEntry(inEntry); 413 } else { 414 // Create a new entry so that the compressed len is recomputed. 415 outEntry = new JarEntry(name); 416 } 417 outEntry.setTime(timestamp); 418 out.putNextEntry(outEntry); 419 420 InputStream data = in.getInputStream(inEntry); 421 while ((num = data.read(buffer)) > 0) { 422 out.write(buffer, 0, num); 423 } 424 out.flush(); 425 } 426 } 427 main(String[] args)428 public static void main(String[] args) { 429 if (args.length != 4 && args.length != 5) { 430 System.err.println("Usage: signapk [-w] " + 431 "publickey.x509[.pem] privatekey.pk8 " + 432 "input.jar output.jar"); 433 System.exit(2); 434 } 435 436 boolean signWholeFile = false; 437 int argstart = 0; 438 if (args[0].equals("-w")) { 439 signWholeFile = true; 440 argstart = 1; 441 } 442 443 JarFile inputJar = null; 444 JarOutputStream outputJar = null; 445 FileOutputStream outputFile = null; 446 447 try { 448 X509Certificate publicKey = readPublicKey(new File(args[argstart+0])); 449 450 // Assume the certificate is valid for at least an hour. 451 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 452 453 PrivateKey privateKey = readPrivateKey(new File(args[argstart+1])); 454 inputJar = new JarFile(new File(args[argstart+2]), false); // Don't verify. 455 456 OutputStream outputStream = null; 457 if (signWholeFile) { 458 outputStream = new ByteArrayOutputStream(); 459 } else { 460 outputStream = outputFile = new FileOutputStream(args[argstart+3]); 461 } 462 outputJar = new JarOutputStream(outputStream); 463 outputJar.setLevel(9); 464 465 JarEntry je; 466 467 // MANIFEST.MF 468 Manifest manifest = addDigestsToManifest(inputJar); 469 je = new JarEntry(JarFile.MANIFEST_NAME); 470 je.setTime(timestamp); 471 outputJar.putNextEntry(je); 472 manifest.write(outputJar); 473 474 // CERT.SF 475 Signature signature = Signature.getInstance("SHA1withRSA"); 476 signature.initSign(privateKey); 477 je = new JarEntry(CERT_SF_NAME); 478 je.setTime(timestamp); 479 outputJar.putNextEntry(je); 480 writeSignatureFile(manifest, 481 new SignatureOutputStream(outputJar, signature)); 482 483 // CERT.RSA 484 je = new JarEntry(CERT_RSA_NAME); 485 je.setTime(timestamp); 486 outputJar.putNextEntry(je); 487 writeSignatureBlock(signature, publicKey, outputJar); 488 489 // Everything else 490 copyFiles(manifest, inputJar, outputJar, timestamp); 491 492 outputJar.close(); 493 outputJar = null; 494 outputStream.flush(); 495 496 if (signWholeFile) { 497 outputFile = new FileOutputStream(args[argstart+3]); 498 signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(), 499 outputFile, publicKey, privateKey); 500 } 501 } catch (Exception e) { 502 e.printStackTrace(); 503 System.exit(1); 504 } finally { 505 try { 506 if (inputJar != null) inputJar.close(); 507 if (outputFile != null) outputFile.close(); 508 } catch (IOException e) { 509 e.printStackTrace(); 510 System.exit(1); 511 } 512 } 513 } 514 } 515