blob: afa66505260b50415541b4e9a20d7abc4b2a8d9c [file] [log] [blame]
The Android Open Source Projectb6c1cf62008-10-21 07:00:00 -07001/*
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}