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