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