• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.jarutils;
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.ByteArrayOutputStream;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FilterOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.io.PrintStream;
34 import java.security.DigestOutputStream;
35 import java.security.GeneralSecurityException;
36 import java.security.MessageDigest;
37 import java.security.NoSuchAlgorithmException;
38 import java.security.PrivateKey;
39 import java.security.Signature;
40 import java.security.SignatureException;
41 import java.security.cert.X509Certificate;
42 import java.util.Map;
43 import java.util.jar.Attributes;
44 import java.util.jar.JarEntry;
45 import java.util.jar.JarFile;
46 import java.util.jar.JarOutputStream;
47 import java.util.jar.Manifest;
48 import java.util.zip.ZipEntry;
49 import java.util.zip.ZipInputStream;
50 
51 /**
52  * A Jar file builder with signature support.
53  */
54 public class SignedJarBuilder {
55     private static final String DIGEST_ALGORITHM = "SHA1";
56     private static final String DIGEST_ATTR = "SHA1-Digest";
57     private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest";
58 
59     /** Write to another stream and also feed it to the Signature object. */
60     private static class SignatureOutputStream extends FilterOutputStream {
61         private Signature mSignature;
62 
SignatureOutputStream(OutputStream out, Signature sig)63         public SignatureOutputStream(OutputStream out, Signature sig) {
64             super(out);
65             mSignature = sig;
66         }
67 
68         @Override
write(int b)69         public void write(int b) throws IOException {
70             try {
71                 mSignature.update((byte) b);
72             } catch (SignatureException e) {
73                 throw new IOException("SignatureException: " + e);
74             }
75             super.write(b);
76         }
77 
78         @Override
write(byte[] b, int off, int len)79         public void write(byte[] b, int off, int len) throws IOException {
80             try {
81                 mSignature.update(b, off, len);
82             } catch (SignatureException e) {
83                 throw new IOException("SignatureException: " + e);
84             }
85             super.write(b, off, len);
86         }
87     }
88 
89     private JarOutputStream mOutputJar;
90     private PrivateKey mKey;
91     private X509Certificate mCertificate;
92     private Manifest mManifest;
93     private BASE64Encoder mBase64Encoder;
94     private MessageDigest mMessageDigest;
95 
96     private byte[] mBuffer = new byte[4096];
97 
98     /**
99      * Classes which implement this interface provides a method to check whether a file should
100      * be added to a Jar file.
101      */
102     public interface IZipEntryFilter {
103         /**
104          * Checks a file for inclusion in a Jar archive.
105          * @param name the archive file path of the entry
106          * @return <code>true</code> if the file should be included.
107          */
checkEntry(String name)108         public boolean checkEntry(String name);
109     }
110 
111     /**
112      * Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
113      * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then
114      * the archive will not be signed.
115      * @param out the {@link OutputStream} where to write the Jar archive.
116      * @param key the {@link PrivateKey} used to sign the archive, or <code>null</code>.
117      * @param certificate the {@link X509Certificate} used to sign the archive, or
118      * <code>null</code>.
119      * @throws IOException
120      * @throws NoSuchAlgorithmException
121      */
SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)122     public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
123             throws IOException, NoSuchAlgorithmException {
124         mOutputJar = new JarOutputStream(out);
125         mOutputJar.setLevel(9);
126         mKey = key;
127         mCertificate = certificate;
128 
129         if (mKey != null && mCertificate != null) {
130             mManifest = new Manifest();
131             Attributes main = mManifest.getMainAttributes();
132             main.putValue("Manifest-Version", "1.0");
133             main.putValue("Created-By", "1.0 (Android)");
134 
135             mBase64Encoder = new BASE64Encoder();
136             mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
137         }
138     }
139 
140     /**
141      * Writes a new {@link File} into the archive.
142      * @param inputFile the {@link File} to write.
143      * @param jarPath the filepath inside the archive.
144      * @throws IOException
145      */
writeFile(File inputFile, String jarPath)146     public void writeFile(File inputFile, String jarPath) throws IOException {
147         // Get an input stream on the file.
148         FileInputStream fis = new FileInputStream(inputFile);
149         try {
150 
151             // create the zip entry
152             JarEntry entry = new JarEntry(jarPath);
153             entry.setTime(inputFile.lastModified());
154 
155             writeEntry(fis, entry);
156         } finally {
157             // close the file stream used to read the file
158             fis.close();
159         }
160     }
161 
162     /**
163      * Copies the content of a Jar/Zip archive into the receiver archive.
164      * <p/>An optional {@link IZipEntryFilter} allows to selectively choose which files
165      * to copy over.
166      * @param input the {@link InputStream} for the Jar/Zip to copy.
167      * @param filter the filter or <code>null</code>
168      * @throws IOException
169      */
writeZip(InputStream input, IZipEntryFilter filter)170     public void writeZip(InputStream input, IZipEntryFilter filter) throws IOException {
171         ZipInputStream zis = new ZipInputStream(input);
172 
173         try {
174             // loop on the entries of the intermediary package and put them in the final package.
175             ZipEntry entry;
176             while ((entry = zis.getNextEntry()) != null) {
177                 String name = entry.getName();
178 
179                 // do not take directories or anything inside a potential META-INF folder.
180                 if (entry.isDirectory() || name.startsWith("META-INF/")) {
181                     continue;
182                 }
183 
184                 // if we have a filter, we check the entry against it
185                 if (filter != null && filter.checkEntry(name) == false) {
186                     continue;
187                 }
188 
189                 JarEntry newEntry;
190 
191                 // Preserve the STORED method of the input entry.
192                 if (entry.getMethod() == JarEntry.STORED) {
193                     newEntry = new JarEntry(entry);
194                 } else {
195                     // Create a new entry so that the compressed len is recomputed.
196                     newEntry = new JarEntry(name);
197                 }
198 
199                 writeEntry(zis, newEntry);
200 
201                 zis.closeEntry();
202             }
203         } finally {
204             zis.close();
205         }
206     }
207 
208     /**
209      * Closes the Jar archive by creating the manifest, and signing the archive.
210      * @throws IOException
211      * @throws GeneralSecurityException
212      */
close()213     public void close() throws IOException, GeneralSecurityException {
214         if (mManifest != null) {
215             // write the manifest to the jar file
216             mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
217             mManifest.write(mOutputJar);
218 
219             // CERT.SF
220             Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm());
221             signature.initSign(mKey);
222             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
223             writeSignatureFile(new SignatureOutputStream(mOutputJar, signature));
224 
225             // CERT.*
226             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
227             writeSignatureBlock(signature, mCertificate, mKey);
228         }
229 
230         mOutputJar.close();
231     }
232 
233     /**
234      * Adds an entry to the output jar, and write its content from the {@link InputStream}
235      * @param input The input stream from where to write the entry content.
236      * @param entry the entry to write in the jar.
237      * @throws IOException
238      */
writeEntry(InputStream input, JarEntry entry)239     private void writeEntry(InputStream input, JarEntry entry) throws IOException {
240         // add the entry to the jar archive
241         mOutputJar.putNextEntry(entry);
242 
243         // read the content of the entry from the input stream, and write it into the archive.
244         int count;
245         while ((count = input.read(mBuffer)) != -1) {
246             mOutputJar.write(mBuffer, 0, count);
247 
248             // update the digest
249             if (mMessageDigest != null) {
250                 mMessageDigest.update(mBuffer, 0, count);
251             }
252         }
253 
254         // close the entry for this file
255         mOutputJar.closeEntry();
256 
257         if (mManifest != null) {
258             // update the manifest for this entry.
259             Attributes attr = mManifest.getAttributes(entry.getName());
260             if (attr == null) {
261                 attr = new Attributes();
262                 mManifest.getEntries().put(entry.getName(), attr);
263             }
264             attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
265         }
266     }
267 
268     /** Writes a .SF file with a digest to the manifest. */
writeSignatureFile(OutputStream out)269     private void writeSignatureFile(OutputStream out)
270             throws IOException, GeneralSecurityException {
271         Manifest sf = new Manifest();
272         Attributes main = sf.getMainAttributes();
273         main.putValue("Signature-Version", "1.0");
274         main.putValue("Created-By", "1.0 (Android)");
275 
276         BASE64Encoder base64 = new BASE64Encoder();
277         MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
278         PrintStream print = new PrintStream(
279                 new DigestOutputStream(new ByteArrayOutputStream(), md),
280                 true, "UTF-8");
281 
282         // Digest of the entire manifest
283         mManifest.write(print);
284         print.flush();
285         main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
286 
287         Map<String, Attributes> entries = mManifest.getEntries();
288         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
289             // Digest of the manifest stanza for this entry.
290             print.print("Name: " + entry.getKey() + "\r\n");
291             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
292                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
293             }
294             print.print("\r\n");
295             print.flush();
296 
297             Attributes sfAttr = new Attributes();
298             sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
299             sf.getEntries().put(entry.getKey(), sfAttr);
300         }
301 
302         sf.write(out);
303     }
304 
305     /** Write the certificate file with a digital signature. */
writeSignatureBlock(Signature signature, X509Certificate publicKey, PrivateKey privateKey)306     private void writeSignatureBlock(Signature signature, X509Certificate publicKey,
307             PrivateKey privateKey)
308             throws IOException, GeneralSecurityException {
309         SignerInfo signerInfo = new SignerInfo(
310                 new X500Name(publicKey.getIssuerX500Principal().getName()),
311                 publicKey.getSerialNumber(),
312                 AlgorithmId.get(DIGEST_ALGORITHM),
313                 AlgorithmId.get(privateKey.getAlgorithm()),
314                 signature.sign());
315 
316         PKCS7 pkcs7 = new PKCS7(
317                 new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) },
318                 new ContentInfo(ContentInfo.DATA_OID, null),
319                 new X509Certificate[] { publicKey },
320                 new SignerInfo[] { signerInfo });
321 
322         pkcs7.encodeSignedData(mOutputJar);
323     }
324 }
325