SignApk.java revision b6c1cf6de79035f58b512f4400db458c8401379a
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.KeyFactory; 42import java.security.MessageDigest; 43import java.security.PrivateKey; 44import java.security.Signature; 45import java.security.SignatureException; 46import java.security.cert.Certificate; 47import java.security.cert.CertificateFactory; 48import java.security.cert.X509Certificate; 49import java.security.Key; 50import java.security.spec.InvalidKeySpecException; 51import java.security.spec.KeySpec; 52import java.security.spec.PKCS8EncodedKeySpec; 53import java.util.Enumeration; 54import java.util.Map; 55import java.util.jar.Attributes; 56import java.util.jar.JarEntry; 57import java.util.jar.JarFile; 58import java.util.jar.JarOutputStream; 59import java.util.jar.Manifest; 60import javax.crypto.Cipher; 61import javax.crypto.EncryptedPrivateKeyInfo; 62import javax.crypto.SecretKeyFactory; 63import javax.crypto.spec.PBEKeySpec; 64 65/** 66 * Command line tool to sign JAR files (including APKs and OTA updates) in 67 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. 68 */ 69class SignApk { 70 private static X509Certificate readPublicKey(File file) 71 throws IOException, GeneralSecurityException { 72 FileInputStream input = new FileInputStream(file); 73 try { 74 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 75 return (X509Certificate) cf.generateCertificate(input); 76 } finally { 77 input.close(); 78 } 79 } 80 81 /** 82 * Reads the password from stdin and returns it as a string. 83 * 84 * @param keyFile The file containing the private key. Used to prompt the user. 85 */ 86 private static String readPassword(File keyFile) { 87 // TODO: use Console.readPassword() when it's available. 88 System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 89 System.out.flush(); 90 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 91 try { 92 return stdin.readLine(); 93 } catch (IOException ex) { 94 return null; 95 } 96 } 97 98 /** 99 * Decrypt an encrypted PKCS 8 format private key. 100 * 101 * Based on ghstark's post on Aug 6, 2006 at 102 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 103 * 104 * @param encryptedPrivateKey The raw data of the private key 105 * @param keyFile The file containing the private key 106 */ 107 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 108 throws GeneralSecurityException { 109 EncryptedPrivateKeyInfo epkInfo; 110 try { 111 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 112 } catch (IOException ex) { 113 // Probably not an encrypted key. 114 return null; 115 } 116 117 char[] password = readPassword(keyFile).toCharArray(); 118 119 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 120 Key key = skFactory.generateSecret(new PBEKeySpec(password)); 121 122 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 123 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 124 125 try { 126 return epkInfo.getKeySpec(cipher); 127 } catch (InvalidKeySpecException ex) { 128 System.err.println("signapk: Password for " + keyFile + " may be bad."); 129 throw ex; 130 } 131 } 132 133 /** Read a PKCS 8 format private key. */ 134 private static PrivateKey readPrivateKey(File file) 135 throws IOException, GeneralSecurityException { 136 DataInputStream input = new DataInputStream(new FileInputStream(file)); 137 try { 138 byte[] bytes = new byte[(int) file.length()]; 139 input.read(bytes); 140 141 KeySpec spec = decryptPrivateKey(bytes, file); 142 if (spec == null) { 143 spec = new PKCS8EncodedKeySpec(bytes); 144 } 145 146 try { 147 return KeyFactory.getInstance("RSA").generatePrivate(spec); 148 } catch (InvalidKeySpecException ex) { 149 return KeyFactory.getInstance("DSA").generatePrivate(spec); 150 } 151 } finally { 152 input.close(); 153 } 154 } 155 156 /** Add the SHA1 of every file to the manifest, creating it if necessary. */ 157 private static Manifest addDigestsToManifest(JarFile jar) 158 throws IOException, GeneralSecurityException { 159 Manifest input = jar.getManifest(); 160 Manifest output = new Manifest(); 161 Attributes main = output.getMainAttributes(); 162 if (input != null) { 163 main.putAll(input.getMainAttributes()); 164 } else { 165 main.putValue("Manifest-Version", "1.0"); 166 main.putValue("Created-By", "1.0 (Android SignApk)"); 167 } 168 169 BASE64Encoder base64 = new BASE64Encoder(); 170 MessageDigest md = MessageDigest.getInstance("SHA1"); 171 byte[] buffer = new byte[4096]; 172 int num; 173 174 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { 175 JarEntry entry = e.nextElement(); 176 String name = entry.getName(); 177 if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME)) { 178 InputStream data = jar.getInputStream(entry); 179 while ((num = data.read(buffer)) > 0) { 180 md.update(buffer, 0, num); 181 } 182 183 Attributes attr = null; 184 if (input != null) attr = input.getAttributes(name); 185 attr = attr != null ? new Attributes(attr) : new Attributes(); 186 attr.putValue("SHA1-Digest", base64.encode(md.digest())); 187 output.getEntries().put(name, attr); 188 } 189 } 190 191 return output; 192 } 193 194 /** Write to another stream and also feed it to the Signature object. */ 195 private static class SignatureOutputStream extends FilterOutputStream { 196 private Signature mSignature; 197 198 public SignatureOutputStream(OutputStream out, Signature sig) { 199 super(out); 200 mSignature = sig; 201 } 202 203 @Override 204 public void write(int b) throws IOException { 205 try { 206 mSignature.update((byte) b); 207 } catch (SignatureException e) { 208 throw new IOException("SignatureException: " + e); 209 } 210 super.write(b); 211 } 212 213 @Override 214 public void write(byte[] b, int off, int len) throws IOException { 215 try { 216 mSignature.update(b, off, len); 217 } catch (SignatureException e) { 218 throw new IOException("SignatureException: " + e); 219 } 220 super.write(b, off, len); 221 } 222 } 223 224 /** Write a .SF file with a digest the specified manifest. */ 225 private static void writeSignatureFile(Manifest manifest, OutputStream out) 226 throws IOException, GeneralSecurityException { 227 Manifest sf = new Manifest(); 228 Attributes main = sf.getMainAttributes(); 229 main.putValue("Signature-Version", "1.0"); 230 main.putValue("Created-By", "1.0 (Android SignApk)"); 231 232 BASE64Encoder base64 = new BASE64Encoder(); 233 MessageDigest md = MessageDigest.getInstance("SHA1"); 234 PrintStream print = new PrintStream( 235 new DigestOutputStream(new ByteArrayOutputStream(), md), 236 true, "UTF-8"); 237 238 // Digest of the entire manifest 239 manifest.write(print); 240 print.flush(); 241 main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest())); 242 243 Map<String, Attributes> entries = manifest.getEntries(); 244 for (Map.Entry<String, Attributes> entry : entries.entrySet()) { 245 // Digest of the manifest stanza for this entry. 246 print.print("Name: " + entry.getKey() + "\r\n"); 247 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { 248 print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 249 } 250 print.print("\r\n"); 251 print.flush(); 252 253 Attributes sfAttr = new Attributes(); 254 sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); 255 sf.getEntries().put(entry.getKey(), sfAttr); 256 } 257 258 sf.write(out); 259 } 260 261 /** Write a .RSA file with a digital signature. */ 262 private static void writeSignatureBlock( 263 Signature signature, X509Certificate publicKey, OutputStream out) 264 throws IOException, GeneralSecurityException { 265 SignerInfo signerInfo = new SignerInfo( 266 new X500Name(publicKey.getIssuerX500Principal().getName()), 267 publicKey.getSerialNumber(), 268 AlgorithmId.get("SHA1"), 269 AlgorithmId.get("RSA"), 270 signature.sign()); 271 272 PKCS7 pkcs7 = new PKCS7( 273 new AlgorithmId[] { AlgorithmId.get("SHA1") }, 274 new ContentInfo(ContentInfo.DATA_OID, null), 275 new X509Certificate[] { publicKey }, 276 new SignerInfo[] { signerInfo }); 277 278 pkcs7.encodeSignedData(out); 279 } 280 281 /** Copy all the files in a manifest from input to output. */ 282 private static void copyFiles(Manifest manifest, 283 JarFile in, JarOutputStream out) throws IOException { 284 byte[] buffer = new byte[4096]; 285 int num; 286 287 Map<String, Attributes> entries = manifest.getEntries(); 288 for (String name : entries.keySet()) { 289 JarEntry inEntry = in.getJarEntry(name); 290 if (inEntry.getMethod() == JarEntry.STORED) { 291 // Preserve the STORED method of the input entry. 292 out.putNextEntry(new JarEntry(inEntry)); 293 } else { 294 // Create a new entry so that the compressed len is recomputed. 295 out.putNextEntry(new JarEntry(name)); 296 } 297 298 InputStream data = in.getInputStream(inEntry); 299 while ((num = data.read(buffer)) > 0) { 300 out.write(buffer, 0, num); 301 } 302 out.flush(); 303 } 304 } 305 306 public static void main(String[] args) { 307 if (args.length != 4) { 308 System.err.println("Usage: signapk " + 309 "publickey.x509[.pem] privatekey.pk8 " + 310 "input.jar output.jar"); 311 System.exit(2); 312 } 313 314 JarFile inputJar = null; 315 JarOutputStream outputJar = null; 316 317 try { 318 X509Certificate publicKey = readPublicKey(new File(args[0])); 319 PrivateKey privateKey = readPrivateKey(new File(args[1])); 320 inputJar = new JarFile(new File(args[2]), false); // Don't verify. 321 outputJar = new JarOutputStream(new FileOutputStream(args[3])); 322 outputJar.setLevel(9); 323 324 // MANIFEST.MF 325 Manifest manifest = addDigestsToManifest(inputJar); 326 manifest.getEntries().remove("META-INF/CERT.SF"); 327 manifest.getEntries().remove("META-INF/CERT.RSA"); 328 outputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); 329 manifest.write(outputJar); 330 331 // CERT.SF 332 Signature signature = Signature.getInstance("SHA1withRSA"); 333 signature.initSign(privateKey); 334 outputJar.putNextEntry(new JarEntry("META-INF/CERT.SF")); 335 writeSignatureFile(manifest, 336 new SignatureOutputStream(outputJar, signature)); 337 338 // CERT.RSA 339 outputJar.putNextEntry(new JarEntry("META-INF/CERT.RSA")); 340 writeSignatureBlock(signature, publicKey, outputJar); 341 342 // Everything else 343 copyFiles(manifest, inputJar, outputJar); 344 } catch (Exception e) { 345 e.printStackTrace(); 346 System.exit(1); 347 } finally { 348 try { 349 if (inputJar != null) inputJar.close(); 350 if (outputJar != null) outputJar.close(); 351 } catch (IOException e) { 352 e.printStackTrace(); 353 System.exit(1); 354 } 355 } 356 } 357} 358