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