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 new SecurityException(Messages.getString( 130 "archive.32", new Object[] { //$NON-NLS-1$ 131 JarFile.MANIFEST_NAME, name, jarName })); 132 } 133 verifiedEntries.put(name, certificates); 134 } 135 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"); //$NON-NLS-1$ 201 if (algorithms == null) { 202 algorithms = "SHA SHA1"; //$NON-NLS-1$ 203 } 204 StringTokenizer tokens = new StringTokenizer(algorithms); 205 while (tokens.hasMoreTokens()) { 206 String algorithm = tokens.nextToken(); 207 String hash = attributes.getValue(algorithm + "-Digest"); //$NON-NLS-1$ 208 if (hash == null) { 209 continue; 210 } 211 byte[] hashBytes; 212 try { 213 hashBytes = hash.getBytes("ISO-8859-1"); //$NON-NLS-1$ 214 } catch (UnsupportedEncodingException e) { 215 throw new RuntimeException(e.toString()); 216 } 217 218 try { 219 // BEGIN android-changed 220 return new VerifierEntry(name, OpenSSLMessageDigestJDK.getInstance(algorithm), 221 hashBytes, certificatesArray); 222 // END android-changed 223 } catch (NoSuchAlgorithmException e) { 224 // ignored 225 } 226 } 227 return null; 228 } 229 230 /** 231 * Add a new meta entry to the internal collection of data held on each JAR 232 * entry in the {@code META-INF} directory including the manifest 233 * file itself. Files associated with the signing of a JAR would also be 234 * added to this collection. 235 * 236 * @param name 237 * the name of the file located in the {@code META-INF} 238 * directory. 239 * @param buf 240 * the file bytes for the file called {@code name}. 241 * @see #removeMetaEntries() 242 */ 243 void addMetaEntry(String name, byte[] buf) { 244 metaEntries.put(Util.toASCIIUpperCase(name), buf); 245 } 246 247 /** 248 * If the associated JAR file is signed, check on the validity of all of the 249 * known signatures. 250 * 251 * @return {@code true} if the associated JAR is signed and an internal 252 * check verifies the validity of the signature(s). {@code false} if 253 * the associated JAR file has no entries at all in its {@code 254 * META-INF} directory. This situation is indicative of an invalid 255 * JAR file. 256 * <p> 257 * Will also return {@code true} if the JAR file is <i>not</i> 258 * signed. 259 * @throws SecurityException 260 * if the JAR file is signed and it is determined that a 261 * signature block file contains an invalid signature for the 262 * corresponding signature file. 263 */ 264 synchronized boolean readCertificates() { 265 if (metaEntries == null) { 266 return false; 267 } 268 Iterator<String> it = metaEntries.keySet().iterator(); 269 while (it.hasNext()) { 270 String key = it.next(); 271 if (key.endsWith(".DSA") || key.endsWith(".RSA")) { //$NON-NLS-1$ //$NON-NLS-2$ 272 verifyCertificate(key); 273 // Check for recursive class load 274 if (metaEntries == null) { 275 return false; 276 } 277 it.remove(); 278 } 279 } 280 return true; 281 } 282 283 /** 284 * @param certFile 285 */ 286 private void verifyCertificate(String certFile) { 287 // Found Digital Sig, .SF should already have been read 288 String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) 289 + ".SF"; //$NON-NLS-1$ 290 byte[] sfBytes = metaEntries.get(signatureFile); 291 if (sfBytes == null) { 292 return; 293 } 294 295 byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME); 296 // Manifest entry is required for any verifications. 297 if (manifest == null) { 298 return; 299 } 300 301 byte[] sBlockBytes = metaEntries.get(certFile); 302 try { 303 Certificate[] signerCertChain = JarUtils.verifySignature( 304 new ByteArrayInputStream(sfBytes), 305 new ByteArrayInputStream(sBlockBytes)); 306 /* 307 * Recursive call in loading security provider related class which 308 * is in a signed JAR. 309 */ 310 if (null == metaEntries) { 311 return; 312 } 313 if (signerCertChain != null) { 314 certificates.put(signatureFile, signerCertChain); 315 } 316 } catch (IOException e) { 317 return; 318 } catch (GeneralSecurityException e) { 319 /* [MSG "archive.31", "{0} failed verification of {1}"] */ 320 throw new SecurityException(Messages.getString( 321 "archive.31", jarName, signatureFile)); //$NON-NLS-1$ 322 } 323 324 // Verify manifest hash in .sf file 325 Attributes attributes = new Attributes(); 326 HashMap<String, Attributes> entries = new HashMap<String, Attributes>(); 327 try { 328 InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION); 329 im.initEntries(entries, null); 330 } catch (IOException e) { 331 return; 332 } 333 334 boolean createdBySigntool = false; 335 String createdBy = attributes.getValue("Created-By"); //$NON-NLS-1$ 336 if (createdBy != null) { 337 createdBySigntool = createdBy.indexOf("signtool") != -1; //$NON-NLS-1$ 338 } 339 340 // Use .SF to verify the mainAttributes of the manifest 341 // If there is no -Digest-Manifest-Main-Attributes entry in .SF 342 // file, such as those created before java 1.5, then we ignore 343 // such verification. 344 if (mainAttributesEnd > 0 && !createdBySigntool) { 345 String digestAttribute = "-Digest-Manifest-Main-Attributes"; //$NON-NLS-1$ 346 if (!verify(attributes, digestAttribute, manifest, 0, 347 mainAttributesEnd, false, true)) { 348 /* [MSG "archive.31", "{0} failed verification of {1}"] */ 349 throw new SecurityException(Messages.getString( 350 "archive.31", jarName, signatureFile)); //$NON-NLS-1$ 351 } 352 } 353 354 // Use .SF to verify the whole manifest. 355 String digestAttribute = createdBySigntool ? "-Digest" //$NON-NLS-1$ 356 : "-Digest-Manifest"; //$NON-NLS-1$ 357 if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, 358 false, false)) { 359 Iterator<Map.Entry<String, Attributes>> it = entries.entrySet() 360 .iterator(); 361 while (it.hasNext()) { 362 Map.Entry<String, Attributes> entry = it.next(); 363 Manifest.Chunk chunk = man.getChunk(entry.getKey()); 364 if (chunk == null) { 365 return; 366 } 367 if (!verify(entry.getValue(), "-Digest", manifest, //$NON-NLS-1$ 368 chunk.start, chunk.end, createdBySigntool, false)) { 369 throw new SecurityException(Messages.getString( 370 "archive.32", //$NON-NLS-1$ 371 new Object[] { signatureFile, entry.getKey(), 372 jarName })); 373 } 374 } 375 } 376 metaEntries.put(signatureFile, null); 377 signatures.put(signatureFile, entries); 378 } 379 380 /** 381 * Associate this verifier with the specified {@link Manifest} object. 382 * 383 * @param mf 384 * a {@code java.util.jar.Manifest} object. 385 */ 386 void setManifest(Manifest mf) { 387 man = mf; 388 } 389 390 /** 391 * Returns a <code>boolean</code> indication of whether or not the 392 * associated jar file is signed. 393 * 394 * @return {@code true} if the JAR is signed, {@code false} 395 * otherwise. 396 */ 397 boolean isSignedJar() { 398 return certificates.size() > 0; 399 } 400 401 private boolean verify(Attributes attributes, String entry, byte[] data, 402 int start, int end, boolean ignoreSecondEndline, boolean ignorable) { 403 String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$ 404 if (algorithms == null) { 405 algorithms = "SHA SHA1"; //$NON-NLS-1$ 406 } 407 StringTokenizer tokens = new StringTokenizer(algorithms); 408 while (tokens.hasMoreTokens()) { 409 String algorithm = tokens.nextToken(); 410 String hash = attributes.getValue(algorithm + entry); 411 if (hash == null) { 412 continue; 413 } 414 415 MessageDigest md; 416 try { 417 // BEGIN android-changed 418 md = OpenSSLMessageDigestJDK.getInstance(algorithm); 419 // END android-changed 420 } catch (NoSuchAlgorithmException e) { 421 continue; 422 } 423 if (ignoreSecondEndline && data[end - 1] == '\n' 424 && data[end - 2] == '\n') { 425 md.update(data, start, end - 1 - start); 426 } else { 427 md.update(data, start, end - start); 428 } 429 byte[] b = md.digest(); 430 byte[] hashBytes; 431 try { 432 hashBytes = hash.getBytes("ISO-8859-1"); //$NON-NLS-1$ 433 } catch (UnsupportedEncodingException e) { 434 throw new RuntimeException(e.toString()); 435 } 436 return MessageDigest.isEqual(b, Base64.decode(hashBytes)); 437 } 438 return ignorable; 439 } 440 441 /** 442 * Returns all of the {@link java.security.cert.Certificate} instances that 443 * were used to verify the signature on the JAR entry called 444 * {@code name}. 445 * 446 * @param name 447 * the name of a JAR entry. 448 * @return an array of {@link java.security.cert.Certificate}. 449 */ 450 Certificate[] getCertificates(String name) { 451 Certificate[] verifiedCerts = verifiedEntries.get(name); 452 if (verifiedCerts == null) { 453 return null; 454 } 455 return verifiedCerts.clone(); 456 } 457 458 /** 459 * Remove all entries from the internal collection of data held about each 460 * JAR entry in the {@code META-INF} directory. 461 * 462 * @see #addMetaEntry(String, byte[]) 463 */ 464 void removeMetaEntries() { 465 metaEntries = null; 466 } 467 468 /** 469 * Returns a {@code Vector} of all of the 470 * {@link java.security.cert.Certificate}s that are associated with the 471 * signing of the named signature file. 472 * 473 * @param signatureFileName 474 * the name of a signature file. 475 * @param certificates 476 * a {@code Map} of all of the certificate chains discovered so 477 * far while attempting to verify the JAR that contains the 478 * signature file {@code signatureFileName}. This object is 479 * previously set in the course of one or more calls to 480 * {@link #verifyJarSignatureFile(String, String, String, Map, Map)} 481 * where it was passed as the last argument. 482 * @return all of the {@code Certificate} entries for the signer of the JAR 483 * whose actions led to the creation of the named signature file. 484 */ 485 public static Vector<Certificate> getSignerCertificates( 486 String signatureFileName, Map<String, Certificate[]> certificates) { 487 Vector<Certificate> result = new Vector<Certificate>(); 488 Certificate[] certChain = certificates.get(signatureFileName); 489 if (certChain != null) { 490 for (Certificate element : certChain) { 491 result.add(element); 492 } 493 } 494 return result; 495 } 496} 497