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 private final String jarName; 56 57 private Manifest man; 58 59 private HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(5); 60 61 private final Hashtable<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>( 62 5); 63 64 private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>( 65 5); 66 67 private final Hashtable<String, Certificate[]> verifiedEntries = new Hashtable<String, Certificate[]>(); 68 69 int mainAttributesEnd; 70 71 /** 72 * Stores and a hash and a message digest and verifies that massage digest 73 * matches the hash. 74 */ 75 class VerifierEntry extends OutputStream { 76 77 private String name; 78 79 private MessageDigest digest; 80 81 private byte[] hash; 82 83 private Certificate[] certificates; 84 85 VerifierEntry(String name, MessageDigest digest, byte[] hash, 86 Certificate[] certificates) { 87 this.name = name; 88 this.digest = digest; 89 this.hash = hash; 90 this.certificates = certificates; 91 } 92 93 /** 94 * Updates a digest with one byte. 95 */ 96 @Override 97 public void write(int value) { 98 digest.update((byte) value); 99 } 100 101 /** 102 * Updates a digest with byte array. 103 */ 104 @Override 105 public void write(byte[] buf, int off, int nbytes) { 106 digest.update(buf, off, nbytes); 107 } 108 109 /** 110 * Verifies that the digests stored in the manifest match the decrypted 111 * digests from the .SF file. This indicates the validity of the 112 * signing, not the integrity of the file, as it's digest must be 113 * calculated and verified when its contents are read. 114 * 115 * @throws SecurityException 116 * if the digest value stored in the manifest does <i>not</i> 117 * agree with the decrypted digest as recovered from the 118 * <code>.SF</code> file. 119 */ 120 void verify() { 121 byte[] d = digest.digest(); 122 if (!MessageDigest.isEqual(d, Base64.decode(hash))) { 123 throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName); 124 } 125 verifiedEntries.put(name, certificates); 126 } 127 128 } 129 130 private SecurityException invalidDigest(String signatureFile, String name, String jarName) { 131 throw new SecurityException(signatureFile + " has invalid digest for " + name + 132 " in " + jarName); 133 } 134 135 private SecurityException failedVerification(String jarName, String signatureFile) { 136 throw new SecurityException(jarName + " failed verification of " + signatureFile); 137 } 138 139 /** 140 * Constructs and returns a new instance of {@code JarVerifier}. 141 * 142 * @param name 143 * the name of the JAR file being verified. 144 */ 145 JarVerifier(String name) { 146 jarName = name; 147 } 148 149 /** 150 * Invoked for each new JAR entry read operation from the input 151 * stream. This method constructs and returns a new {@link VerifierEntry} 152 * which contains the certificates used to sign the entry and its hash value 153 * as specified in the JAR MANIFEST format. 154 * 155 * @param name 156 * the name of an entry in a JAR file which is <b>not</b> in the 157 * {@code META-INF} directory. 158 * @return a new instance of {@link VerifierEntry} which can be used by 159 * callers as an {@link OutputStream}. 160 */ 161 VerifierEntry initEntry(String name) { 162 // If no manifest is present by the time an entry is found, 163 // verification cannot occur. If no signature files have 164 // been found, do not verify. 165 if (man == null || signatures.size() == 0) { 166 return null; 167 } 168 169 Attributes attributes = man.getAttributes(name); 170 // entry has no digest 171 if (attributes == null) { 172 return null; 173 } 174 175 ArrayList<Certificate> certs = new ArrayList<Certificate>(); 176 Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.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 certs.addAll(getSignerCertificates(signatureFile, certificates)); 184 } 185 } 186 187 // entry is not signed 188 if (certs.isEmpty()) { 189 return null; 190 } 191 Certificate[] certificatesArray = certs.toArray(new Certificate[certs.size()]); 192 193 String algorithms = attributes.getValue("Digest-Algorithms"); 194 if (algorithms == null) { 195 algorithms = "SHA SHA1"; 196 } 197 StringTokenizer tokens = new StringTokenizer(algorithms); 198 while (tokens.hasMoreTokens()) { 199 String algorithm = tokens.nextToken(); 200 String hash = attributes.getValue(algorithm + "-Digest"); 201 if (hash == null) { 202 continue; 203 } 204 byte[] hashBytes = hash.getBytes(Charsets.ISO_8859_1); 205 206 try { 207 return new VerifierEntry(name, MessageDigest 208 .getInstance(algorithm), hashBytes, certificatesArray); 209 } catch (NoSuchAlgorithmException e) { 210 // ignored 211 } 212 } 213 return null; 214 } 215 216 /** 217 * Add a new meta entry to the internal collection of data held on each JAR 218 * entry in the {@code META-INF} directory including the manifest 219 * file itself. Files associated with the signing of a JAR would also be 220 * added to this collection. 221 * 222 * @param name 223 * the name of the file located in the {@code META-INF} 224 * directory. 225 * @param buf 226 * the file bytes for the file called {@code name}. 227 * @see #removeMetaEntries() 228 */ 229 void addMetaEntry(String name, byte[] buf) { 230 metaEntries.put(name.toUpperCase(Locale.US), buf); 231 } 232 233 /** 234 * If the associated JAR file is signed, check on the validity of all of the 235 * known signatures. 236 * 237 * @return {@code true} if the associated JAR is signed and an internal 238 * check verifies the validity of the signature(s). {@code false} if 239 * the associated JAR file has no entries at all in its {@code 240 * META-INF} directory. This situation is indicative of an invalid 241 * JAR file. 242 * <p> 243 * Will also return {@code true} if the JAR file is <i>not</i> 244 * signed. 245 * @throws SecurityException 246 * if the JAR file is signed and it is determined that a 247 * signature block file contains an invalid signature for the 248 * corresponding signature file. 249 */ 250 synchronized boolean readCertificates() { 251 if (metaEntries == null) { 252 return false; 253 } 254 Iterator<String> it = metaEntries.keySet().iterator(); 255 while (it.hasNext()) { 256 String key = it.next(); 257 if (key.endsWith(".DSA") || key.endsWith(".RSA")) { 258 verifyCertificate(key); 259 // Check for recursive class load 260 if (metaEntries == null) { 261 return false; 262 } 263 it.remove(); 264 } 265 } 266 return true; 267 } 268 269 /** 270 * @param certFile 271 */ 272 private void verifyCertificate(String certFile) { 273 // Found Digital Sig, .SF should already have been read 274 String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) 275 + ".SF"; 276 byte[] sfBytes = metaEntries.get(signatureFile); 277 if (sfBytes == null) { 278 return; 279 } 280 281 byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME); 282 // Manifest entry is required for any verifications. 283 if (manifest == null) { 284 return; 285 } 286 287 byte[] sBlockBytes = metaEntries.get(certFile); 288 try { 289 Certificate[] signerCertChain = JarUtils.verifySignature( 290 new ByteArrayInputStream(sfBytes), 291 new ByteArrayInputStream(sBlockBytes)); 292 /* 293 * Recursive call in loading security provider related class which 294 * is in a signed JAR. 295 */ 296 if (metaEntries == null) { 297 return; 298 } 299 if (signerCertChain != null) { 300 certificates.put(signatureFile, signerCertChain); 301 } 302 } catch (IOException e) { 303 return; 304 } catch (GeneralSecurityException e) { 305 throw failedVerification(jarName, signatureFile); 306 } 307 308 // Verify manifest hash in .sf file 309 Attributes attributes = new Attributes(); 310 HashMap<String, Attributes> entries = new HashMap<String, Attributes>(); 311 try { 312 InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION); 313 im.initEntries(entries, null); 314 } catch (IOException e) { 315 return; 316 } 317 318 boolean createdBySigntool = false; 319 String createdBy = attributes.getValue("Created-By"); 320 if (createdBy != null) { 321 createdBySigntool = createdBy.indexOf("signtool") != -1; 322 } 323 324 // Use .SF to verify the mainAttributes of the manifest 325 // If there is no -Digest-Manifest-Main-Attributes entry in .SF 326 // file, such as those created before java 1.5, then we ignore 327 // such verification. 328 if (mainAttributesEnd > 0 && !createdBySigntool) { 329 String digestAttribute = "-Digest-Manifest-Main-Attributes"; 330 if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) { 331 throw failedVerification(jarName, signatureFile); 332 } 333 } 334 335 // Use .SF to verify the whole manifest. 336 String digestAttribute = createdBySigntool ? "-Digest" 337 : "-Digest-Manifest"; 338 if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, 339 false, false)) { 340 Iterator<Map.Entry<String, Attributes>> it = entries.entrySet() 341 .iterator(); 342 while (it.hasNext()) { 343 Map.Entry<String, Attributes> entry = it.next(); 344 Manifest.Chunk chunk = man.getChunk(entry.getKey()); 345 if (chunk == null) { 346 return; 347 } 348 if (!verify(entry.getValue(), "-Digest", manifest, 349 chunk.start, chunk.end, createdBySigntool, false)) { 350 throw invalidDigest(signatureFile, entry.getKey(), jarName); 351 } 352 } 353 } 354 metaEntries.put(signatureFile, null); 355 signatures.put(signatureFile, entries); 356 } 357 358 /** 359 * Associate this verifier with the specified {@link Manifest} object. 360 * 361 * @param mf 362 * a {@code java.util.jar.Manifest} object. 363 */ 364 void setManifest(Manifest mf) { 365 man = mf; 366 } 367 368 /** 369 * Returns a <code>boolean</code> indication of whether or not the 370 * associated jar file is signed. 371 * 372 * @return {@code true} if the JAR is signed, {@code false} 373 * otherwise. 374 */ 375 boolean isSignedJar() { 376 return certificates.size() > 0; 377 } 378 379 private boolean verify(Attributes attributes, String entry, byte[] data, 380 int start, int end, boolean ignoreSecondEndline, boolean ignorable) { 381 String algorithms = attributes.getValue("Digest-Algorithms"); 382 if (algorithms == null) { 383 algorithms = "SHA SHA1"; 384 } 385 StringTokenizer tokens = new StringTokenizer(algorithms); 386 while (tokens.hasMoreTokens()) { 387 String algorithm = tokens.nextToken(); 388 String hash = attributes.getValue(algorithm + entry); 389 if (hash == null) { 390 continue; 391 } 392 393 MessageDigest md; 394 try { 395 md = MessageDigest.getInstance(algorithm); 396 } catch (NoSuchAlgorithmException e) { 397 continue; 398 } 399 if (ignoreSecondEndline && data[end - 1] == '\n' 400 && 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(Charsets.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