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