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.StandardCharsets; 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(StandardCharsets.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('.')) + ".SF"; 280 byte[] sfBytes = metaEntries.get(signatureFile); 281 if (sfBytes == null) { 282 return; 283 } 284 285 byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME); 286 // Manifest entry is required for any verifications. 287 if (manifest == null) { 288 return; 289 } 290 291 byte[] sBlockBytes = metaEntries.get(certFile); 292 try { 293 Certificate[] signerCertChain = JarUtils.verifySignature( 294 new ByteArrayInputStream(sfBytes), 295 new ByteArrayInputStream(sBlockBytes)); 296 /* 297 * Recursive call in loading security provider related class which 298 * is in a signed JAR. 299 */ 300 if (metaEntries == null) { 301 return; 302 } 303 if (signerCertChain != null) { 304 certificates.put(signatureFile, signerCertChain); 305 } 306 } catch (IOException e) { 307 return; 308 } catch (GeneralSecurityException e) { 309 throw failedVerification(jarName, signatureFile); 310 } 311 312 // Verify manifest hash in .sf file 313 Attributes attributes = new Attributes(); 314 HashMap<String, Attributes> entries = new HashMap<String, Attributes>(); 315 try { 316 ManifestReader im = new ManifestReader(sfBytes, attributes); 317 im.readEntries(entries, null); 318 } catch (IOException e) { 319 return; 320 } 321 322 // Do we actually have any signatures to look at? 323 if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) { 324 return; 325 } 326 327 boolean createdBySigntool = false; 328 String createdBy = attributes.getValue("Created-By"); 329 if (createdBy != null) { 330 createdBySigntool = createdBy.indexOf("signtool") != -1; 331 } 332 333 // Use .SF to verify the mainAttributes of the manifest 334 // If there is no -Digest-Manifest-Main-Attributes entry in .SF 335 // file, such as those created before java 1.5, then we ignore 336 // such verification. 337 if (mainAttributesEnd > 0 && !createdBySigntool) { 338 String digestAttribute = "-Digest-Manifest-Main-Attributes"; 339 if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) { 340 throw failedVerification(jarName, signatureFile); 341 } 342 } 343 344 // Use .SF to verify the whole manifest. 345 String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest"; 346 if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, false, false)) { 347 Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator(); 348 while (it.hasNext()) { 349 Map.Entry<String, Attributes> entry = it.next(); 350 Manifest.Chunk chunk = man.getChunk(entry.getKey()); 351 if (chunk == null) { 352 return; 353 } 354 if (!verify(entry.getValue(), "-Digest", manifest, 355 chunk.start, chunk.end, createdBySigntool, false)) { 356 throw invalidDigest(signatureFile, entry.getKey(), jarName); 357 } 358 } 359 } 360 metaEntries.put(signatureFile, null); 361 signatures.put(signatureFile, entries); 362 } 363 364 /** 365 * Associate this verifier with the specified {@link Manifest} object. 366 * 367 * @param mf 368 * a {@code java.util.jar.Manifest} object. 369 */ 370 void setManifest(Manifest mf) { 371 man = mf; 372 } 373 374 /** 375 * Returns a <code>boolean</code> indication of whether or not the 376 * associated jar file is signed. 377 * 378 * @return {@code true} if the JAR is signed, {@code false} 379 * otherwise. 380 */ 381 boolean isSignedJar() { 382 return certificates.size() > 0; 383 } 384 385 private boolean verify(Attributes attributes, String entry, byte[] data, 386 int start, int end, boolean ignoreSecondEndline, boolean ignorable) { 387 for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) { 388 String algorithm = DIGEST_ALGORITHMS[i]; 389 String hash = attributes.getValue(algorithm + entry); 390 if (hash == null) { 391 continue; 392 } 393 394 MessageDigest md; 395 try { 396 md = MessageDigest.getInstance(algorithm); 397 } catch (NoSuchAlgorithmException e) { 398 continue; 399 } 400 if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') { 401 md.update(data, start, end - 1 - start); 402 } else { 403 md.update(data, start, end - start); 404 } 405 byte[] b = md.digest(); 406 byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1); 407 return MessageDigest.isEqual(b, Base64.decode(hashBytes)); 408 } 409 return ignorable; 410 } 411 412 /** 413 * Returns all of the {@link java.security.cert.Certificate} instances that 414 * were used to verify the signature on the JAR entry called 415 * {@code name}. 416 * 417 * @param name 418 * the name of a JAR entry. 419 * @return an array of {@link java.security.cert.Certificate}. 420 */ 421 Certificate[] getCertificates(String name) { 422 Certificate[] verifiedCerts = verifiedEntries.get(name); 423 if (verifiedCerts == null) { 424 return null; 425 } 426 return verifiedCerts.clone(); 427 } 428 429 /** 430 * Remove all entries from the internal collection of data held about each 431 * JAR entry in the {@code META-INF} directory. 432 * 433 * @see #addMetaEntry(String, byte[]) 434 */ 435 void removeMetaEntries() { 436 metaEntries = null; 437 } 438 439 /** 440 * Returns a {@code Vector} of all of the 441 * {@link java.security.cert.Certificate}s that are associated with the 442 * signing of the named signature file. 443 * 444 * @param signatureFileName 445 * the name of a signature file. 446 * @param certificates 447 * a {@code Map} of all of the certificate chains discovered so 448 * far while attempting to verify the JAR that contains the 449 * signature file {@code signatureFileName}. This object is 450 * previously set in the course of one or more calls to 451 * {@link #verifyJarSignatureFile(String, String, String, Map, Map)} 452 * where it was passed as the last argument. 453 * @return all of the {@code Certificate} entries for the signer of the JAR 454 * whose actions led to the creation of the named signature file. 455 */ 456 public static Vector<Certificate> getSignerCertificates( 457 String signatureFileName, Map<String, Certificate[]> certificates) { 458 Vector<Certificate> result = new Vector<Certificate>(); 459 Certificate[] certChain = certificates.get(signatureFileName); 460 if (certChain != null) { 461 for (Certificate element : certChain) { 462 result.add(element); 463 } 464 } 465 return result; 466 } 467 } 468