JarVerifier.java revision b1433b3bd4dfc05426e5d9c3100b5fbaa198d8a0
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.util.jar; 19 20import java.io.ByteArrayInputStream; 21import java.io.IOException; 22import java.io.OutputStream; 23import java.io.UnsupportedEncodingException; 24import java.security.GeneralSecurityException; 25import java.security.MessageDigest; 26import java.security.NoSuchAlgorithmException; 27import java.security.cert.Certificate; 28import java.util.HashMap; 29import java.util.Hashtable; 30import java.util.Iterator; 31import java.util.Map; 32import java.util.StringTokenizer; 33import java.util.Vector; 34 35import org.apache.harmony.archive.internal.nls.Messages; 36import org.apache.harmony.luni.util.Base64; 37import org.apache.harmony.security.utils.JarUtils; 38 39import org.apache.harmony.luni.util.Util; 40 41// BEGIN android-added 42import org.apache.harmony.xnet.provider.jsse.OpenSSLMessageDigestJDK; 43// END android-added 44 45/** 46 * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage 47 * the verification of signed JARs. {@code JarFile} and {@code JarInputStream} 48 * objects are expected to have a {@code JarVerifier} instance member which 49 * can be used to carry out the tasks associated with verifying a signed JAR. 50 * These tasks would typically include: 51 * <ul> 52 * <li>verification of all signed signature files 53 * <li>confirmation that all signed data was signed only by the party or parties 54 * specified in the signature block data 55 * <li>verification that the contents of all signature files (i.e. {@code .SF} 56 * files) agree with the JAR entries information found in the JAR manifest. 57 * </ul> 58 */ 59class JarVerifier { 60 61 private final String jarName; 62 63 private Manifest man; 64 65 private HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(5); 66 67 private final Hashtable<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>( 68 5); 69 70 private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>( 71 5); 72 73 private final Hashtable<String, Certificate[]> verifiedEntries = new Hashtable<String, Certificate[]>(); 74 75 int mainAttributesEnd; 76 77 /** 78 * Stores and a hash and a message digest and verifies that massage digest 79 * matches the hash. 80 */ 81 class VerifierEntry extends OutputStream { 82 83 private String name; 84 85 private MessageDigest digest; 86 87 private byte[] hash; 88 89 private Certificate[] certificates; 90 91 VerifierEntry(String name, MessageDigest digest, byte[] hash, 92 Certificate[] certificates) { 93 this.name = name; 94 this.digest = digest; 95 this.hash = hash; 96 this.certificates = certificates; 97 } 98 99 /** 100 * Updates a digest with one byte. 101 */ 102 @Override 103 public void write(int value) { 104 digest.update((byte) value); 105 } 106 107 /** 108 * Updates a digest with byte array. 109 */ 110 @Override 111 public void write(byte[] buf, int off, int nbytes) { 112 digest.update(buf, off, nbytes); 113 } 114 115 /** 116 * Verifies that the digests stored in the manifest match the decrypted 117 * digests from the .SF file. This indicates the validity of the 118 * signing, not the integrity of the file, as it's digest must be 119 * calculated and verified when its contents are read. 120 * 121 * @throws SecurityException 122 * if the digest value stored in the manifest does <i>not</i> 123 * agree with the decrypted digest as recovered from the 124 * <code>.SF</code> file. 125 */ 126 void verify() { 127 byte[] d = digest.digest(); 128 if (!MessageDigest.isEqual(d, Base64.decode(hash))) { 129 throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName); 130 } 131 verifiedEntries.put(name, certificates); 132 } 133 134 } 135 136 private SecurityException invalidDigest(String signatureFile, String name, String jarName) { 137 throw new SecurityException(signatureFile + " has invalid digest for " + name + 138 " in " + jarName); 139 } 140 141 private SecurityException failedVerification(String jarName, String signatureFile) { 142 throw new SecurityException(jarName + " failed verification of " + signatureFile); 143 } 144 145 /** 146 * Constructs and returns a new instance of {@code JarVerifier}. 147 * 148 * @param name 149 * the name of the JAR file being verified. 150 */ 151 JarVerifier(String name) { 152 jarName = name; 153 } 154 155 /** 156 * Invoked for each new JAR entry read operation from the input 157 * stream. This method constructs and returns a new {@link VerifierEntry} 158 * which contains the certificates used to sign the entry and its hash value 159 * as specified in the JAR MANIFEST format. 160 * 161 * @param name 162 * the name of an entry in a JAR file which is <b>not</b> in the 163 * {@code META-INF} directory. 164 * @return a new instance of {@link VerifierEntry} which can be used by 165 * callers as an {@link OutputStream}. 166 */ 167 VerifierEntry initEntry(String name) { 168 // If no manifest is present by the time an entry is found, 169 // verification cannot occur. If no signature files have 170 // been found, do not verify. 171 if (man == null || signatures.size() == 0) { 172 return null; 173 } 174 175 Attributes attributes = man.getAttributes(name); 176 // entry has no digest 177 if (attributes == null) { 178 return null; 179 } 180 181 Vector<Certificate> certs = new Vector<Certificate>(); 182 Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures 183 .entrySet().iterator(); 184 while (it.hasNext()) { 185 Map.Entry<String, HashMap<String, Attributes>> entry = it.next(); 186 HashMap<String, Attributes> hm = entry.getValue(); 187 if (hm.get(name) != null) { 188 // Found an entry for entry name in .SF file 189 String signatureFile = entry.getKey(); 190 191 Vector<Certificate> newCerts = getSignerCertificates( 192 signatureFile, certificates); 193 Iterator<Certificate> iter = newCerts.iterator(); 194 while (iter.hasNext()) { 195 certs.add(iter.next()); 196 } 197 } 198 } 199 200 // entry is not signed 201 if (certs.size() == 0) { 202 return null; 203 } 204 Certificate[] certificatesArray = new Certificate[certs.size()]; 205 certs.toArray(certificatesArray); 206 207 String algorithms = attributes.getValue("Digest-Algorithms"); 208 if (algorithms == null) { 209 algorithms = "SHA SHA1"; 210 } 211 StringTokenizer tokens = new StringTokenizer(algorithms); 212 while (tokens.hasMoreTokens()) { 213 String algorithm = tokens.nextToken(); 214 String hash = attributes.getValue(algorithm + "-Digest"); 215 if (hash == null) { 216 continue; 217 } 218 byte[] hashBytes; 219 try { 220 hashBytes = hash.getBytes("ISO-8859-1"); 221 } catch (UnsupportedEncodingException e) { 222 throw new RuntimeException(e.toString()); 223 } 224 225 try { 226 // BEGIN android-changed 227 return new VerifierEntry(name, OpenSSLMessageDigestJDK.getInstance(algorithm), 228 hashBytes, certificatesArray); 229 // END android-changed 230 } catch (NoSuchAlgorithmException e) { 231 // ignored 232 } 233 } 234 return null; 235 } 236 237 /** 238 * Add a new meta entry to the internal collection of data held on each JAR 239 * entry in the {@code META-INF} directory including the manifest 240 * file itself. Files associated with the signing of a JAR would also be 241 * added to this collection. 242 * 243 * @param name 244 * the name of the file located in the {@code META-INF} 245 * directory. 246 * @param buf 247 * the file bytes for the file called {@code name}. 248 * @see #removeMetaEntries() 249 */ 250 void addMetaEntry(String name, byte[] buf) { 251 metaEntries.put(Util.toASCIIUpperCase(name), buf); 252 } 253 254 /** 255 * If the associated JAR file is signed, check on the validity of all of the 256 * known signatures. 257 * 258 * @return {@code true} if the associated JAR is signed and an internal 259 * check verifies the validity of the signature(s). {@code false} if 260 * the associated JAR file has no entries at all in its {@code 261 * META-INF} directory. This situation is indicative of an invalid 262 * JAR file. 263 * <p> 264 * Will also return {@code true} if the JAR file is <i>not</i> 265 * signed. 266 * @throws SecurityException 267 * if the JAR file is signed and it is determined that a 268 * signature block file contains an invalid signature for the 269 * corresponding signature file. 270 */ 271 synchronized boolean readCertificates() { 272 if (metaEntries == null) { 273 return false; 274 } 275 Iterator<String> it = metaEntries.keySet().iterator(); 276 while (it.hasNext()) { 277 String key = it.next(); 278 if (key.endsWith(".DSA") || key.endsWith(".RSA")) { 279 verifyCertificate(key); 280 // Check for recursive class load 281 if (metaEntries == null) { 282 return false; 283 } 284 it.remove(); 285 } 286 } 287 return true; 288 } 289 290 /** 291 * @param certFile 292 */ 293 private void verifyCertificate(String certFile) { 294 // Found Digital Sig, .SF should already have been read 295 String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) 296 + ".SF"; 297 byte[] sfBytes = metaEntries.get(signatureFile); 298 if (sfBytes == null) { 299 return; 300 } 301 302 byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME); 303 // Manifest entry is required for any verifications. 304 if (manifest == null) { 305 return; 306 } 307 308 byte[] sBlockBytes = metaEntries.get(certFile); 309 try { 310 Certificate[] signerCertChain = JarUtils.verifySignature( 311 new ByteArrayInputStream(sfBytes), 312 new ByteArrayInputStream(sBlockBytes)); 313 /* 314 * Recursive call in loading security provider related class which 315 * is in a signed JAR. 316 */ 317 if (null == metaEntries) { 318 return; 319 } 320 if (signerCertChain != null) { 321 certificates.put(signatureFile, signerCertChain); 322 } 323 } catch (IOException e) { 324 return; 325 } catch (GeneralSecurityException e) { 326 throw failedVerification(jarName, signatureFile); 327 } 328 329 // Verify manifest hash in .sf file 330 Attributes attributes = new Attributes(); 331 HashMap<String, Attributes> entries = new HashMap<String, Attributes>(); 332 try { 333 InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION); 334 im.initEntries(entries, null); 335 } catch (IOException e) { 336 return; 337 } 338 339 boolean createdBySigntool = false; 340 String createdBy = attributes.getValue("Created-By"); 341 if (createdBy != null) { 342 createdBySigntool = createdBy.indexOf("signtool") != -1; 343 } 344 345 // Use .SF to verify the mainAttributes of the manifest 346 // If there is no -Digest-Manifest-Main-Attributes entry in .SF 347 // file, such as those created before java 1.5, then we ignore 348 // such verification. 349 if (mainAttributesEnd > 0 && !createdBySigntool) { 350 String digestAttribute = "-Digest-Manifest-Main-Attributes"; 351 if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) { 352 throw failedVerification(jarName, signatureFile); 353 } 354 } 355 356 // Use .SF to verify the whole manifest. 357 String digestAttribute = createdBySigntool ? "-Digest" 358 : "-Digest-Manifest"; 359 if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, 360 false, false)) { 361 Iterator<Map.Entry<String, Attributes>> it = entries.entrySet() 362 .iterator(); 363 while (it.hasNext()) { 364 Map.Entry<String, Attributes> entry = it.next(); 365 Manifest.Chunk chunk = man.getChunk(entry.getKey()); 366 if (chunk == null) { 367 return; 368 } 369 if (!verify(entry.getValue(), "-Digest", manifest, 370 chunk.start, chunk.end, createdBySigntool, false)) { 371 throw invalidDigest(signatureFile, entry.getKey(), jarName); 372 } 373 } 374 } 375 metaEntries.put(signatureFile, null); 376 signatures.put(signatureFile, entries); 377 } 378 379 /** 380 * Associate this verifier with the specified {@link Manifest} object. 381 * 382 * @param mf 383 * a {@code java.util.jar.Manifest} object. 384 */ 385 void setManifest(Manifest mf) { 386 man = mf; 387 } 388 389 /** 390 * Returns a <code>boolean</code> indication of whether or not the 391 * associated jar file is signed. 392 * 393 * @return {@code true} if the JAR is signed, {@code false} 394 * otherwise. 395 */ 396 boolean isSignedJar() { 397 return certificates.size() > 0; 398 } 399 400 private boolean verify(Attributes attributes, String entry, byte[] data, 401 int start, int end, boolean ignoreSecondEndline, boolean ignorable) { 402 String algorithms = attributes.getValue("Digest-Algorithms"); 403 if (algorithms == null) { 404 algorithms = "SHA SHA1"; 405 } 406 StringTokenizer tokens = new StringTokenizer(algorithms); 407 while (tokens.hasMoreTokens()) { 408 String algorithm = tokens.nextToken(); 409 String hash = attributes.getValue(algorithm + entry); 410 if (hash == null) { 411 continue; 412 } 413 414 MessageDigest md; 415 try { 416 // BEGIN android-changed 417 md = OpenSSLMessageDigestJDK.getInstance(algorithm); 418 // END android-changed 419 } catch (NoSuchAlgorithmException e) { 420 continue; 421 } 422 if (ignoreSecondEndline && data[end - 1] == '\n' 423 && data[end - 2] == '\n') { 424 md.update(data, start, end - 1 - start); 425 } else { 426 md.update(data, start, end - start); 427 } 428 byte[] b = md.digest(); 429 byte[] hashBytes; 430 try { 431 hashBytes = hash.getBytes("ISO-8859-1"); 432 } catch (UnsupportedEncodingException e) { 433 throw new RuntimeException(e.toString()); 434 } 435 return MessageDigest.isEqual(b, Base64.decode(hashBytes)); 436 } 437 return ignorable; 438 } 439 440 /** 441 * Returns all of the {@link java.security.cert.Certificate} instances that 442 * were used to verify the signature on the JAR entry called 443 * {@code name}. 444 * 445 * @param name 446 * the name of a JAR entry. 447 * @return an array of {@link java.security.cert.Certificate}. 448 */ 449 Certificate[] getCertificates(String name) { 450 Certificate[] verifiedCerts = verifiedEntries.get(name); 451 if (verifiedCerts == null) { 452 return null; 453 } 454 return verifiedCerts.clone(); 455 } 456 457 /** 458 * Remove all entries from the internal collection of data held about each 459 * JAR entry in the {@code META-INF} directory. 460 * 461 * @see #addMetaEntry(String, byte[]) 462 */ 463 void removeMetaEntries() { 464 metaEntries = null; 465 } 466 467 /** 468 * Returns a {@code Vector} of all of the 469 * {@link java.security.cert.Certificate}s that are associated with the 470 * signing of the named signature file. 471 * 472 * @param signatureFileName 473 * the name of a signature file. 474 * @param certificates 475 * a {@code Map} of all of the certificate chains discovered so 476 * far while attempting to verify the JAR that contains the 477 * signature file {@code signatureFileName}. This object is 478 * previously set in the course of one or more calls to 479 * {@link #verifyJarSignatureFile(String, String, String, Map, Map)} 480 * where it was passed as the last argument. 481 * @return all of the {@code Certificate} entries for the signer of the JAR 482 * whose actions led to the creation of the named signature file. 483 */ 484 public static Vector<Certificate> getSignerCertificates( 485 String signatureFileName, Map<String, Certificate[]> certificates) { 486 Vector<Certificate> result = new Vector<Certificate>(); 487 Certificate[] certChain = certificates.get(signatureFileName); 488 if (certChain != null) { 489 for (Certificate element : certChain) { 490 result.add(element); 491 } 492 } 493 return result; 494 } 495} 496