1 /* 2 * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.security.util; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.IOException; 30 import java.security.CodeSigner; 31 import java.security.CryptoPrimitive; 32 import java.security.MessageDigest; 33 import java.security.NoSuchAlgorithmException; 34 import java.security.SignatureException; 35 import java.security.cert.CertPath; 36 import java.security.cert.X509Certificate; 37 import java.security.cert.CertificateException; 38 import java.security.cert.CertificateFactory; 39 import java.util.ArrayList; 40 import java.util.Base64; 41 import java.util.Collections; 42 import java.util.EnumSet; 43 import java.util.HashMap; 44 import java.util.Hashtable; 45 import java.util.Iterator; 46 import java.util.List; 47 import java.util.Locale; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.jar.Attributes; 51 import java.util.jar.JarException; 52 import java.util.jar.JarFile; 53 import java.util.jar.Manifest; 54 55 import sun.security.jca.Providers; 56 import sun.security.pkcs.PKCS7; 57 import sun.security.pkcs.SignerInfo; 58 59 public class SignatureFileVerifier { 60 61 /* Are we debugging ? */ 62 private static final Debug debug = Debug.getInstance("jar"); 63 64 private static final Set<CryptoPrimitive> DIGEST_PRIMITIVE_SET = 65 Collections.unmodifiableSet(EnumSet.of(CryptoPrimitive.MESSAGE_DIGEST)); 66 67 private static final DisabledAlgorithmConstraints JAR_DISABLED_CHECK = 68 new DisabledAlgorithmConstraints( 69 DisabledAlgorithmConstraints.PROPERTY_JAR_DISABLED_ALGS); 70 71 private ArrayList<CodeSigner[]> signerCache; 72 73 private static final String ATTR_DIGEST = 74 ("-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS).toUpperCase 75 (Locale.ENGLISH); 76 77 /** the PKCS7 block for this .DSA/.RSA/.EC file */ 78 private PKCS7 block; 79 80 /** the raw bytes of the .SF file */ 81 private byte sfBytes[]; 82 83 /** the name of the signature block file, uppercased and without 84 * the extension (.DSA/.RSA/.EC) 85 */ 86 private String name; 87 88 /** the ManifestDigester */ 89 private ManifestDigester md; 90 91 /** cache of created MessageDigest objects */ 92 private HashMap<String, MessageDigest> createdDigests; 93 94 /* workaround for parsing Netscape jars */ 95 private boolean workaround = false; 96 97 /* for generating certpath objects */ 98 private CertificateFactory certificateFactory = null; 99 100 /** 101 * Create the named SignatureFileVerifier. 102 * 103 * @param name the name of the signature block file (.DSA/.RSA/.EC) 104 * 105 * @param rawBytes the raw bytes of the signature block file 106 */ 107 public SignatureFileVerifier(ArrayList<CodeSigner[]> signerCache, 108 ManifestDigester md, 109 String name, 110 byte rawBytes[]) 111 throws IOException, CertificateException 112 { 113 // new PKCS7() calls CertificateFactory.getInstance() 114 // need to use local providers here, see Providers class 115 Object obj = null; 116 try { 117 obj = Providers.startJarVerification(); 118 block = new PKCS7(rawBytes); 119 sfBytes = block.getContentInfo().getData(); 120 certificateFactory = CertificateFactory.getInstance("X509"); 121 } finally { 122 Providers.stopJarVerification(obj); 123 } 124 this.name = name.substring(0, name.lastIndexOf(".")) 125 .toUpperCase(Locale.ENGLISH); 126 this.md = md; 127 this.signerCache = signerCache; 128 } 129 130 /** 131 * returns true if we need the .SF file 132 */ 133 public boolean needSignatureFileBytes() 134 { 135 136 return sfBytes == null; 137 } 138 139 140 /** 141 * returns true if we need this .SF file. 142 * 143 * @param name the name of the .SF file without the extension 144 * 145 */ 146 public boolean needSignatureFile(String name) 147 { 148 return this.name.equalsIgnoreCase(name); 149 } 150 151 /** 152 * used to set the raw bytes of the .SF file when it 153 * is external to the signature block file. 154 */ 155 public void setSignatureFile(byte sfBytes[]) 156 { 157 this.sfBytes = sfBytes; 158 } 159 160 /** 161 * Utility method used by JarVerifier and JarSigner 162 * to determine the signature file names and PKCS7 block 163 * files names that are supported 164 * 165 * @param s file name 166 * @return true if the input file name is a supported 167 * Signature File or PKCS7 block file name 168 */ 169 public static boolean isBlockOrSF(String s) { 170 // we currently only support DSA and RSA PKCS7 blocks 171 if (s.endsWith(".SF") || s.endsWith(".DSA") || 172 s.endsWith(".RSA") || s.endsWith(".EC")) { 173 return true; 174 } 175 return false; 176 } 177 178 /** 179 * Yet another utility method used by JarVerifier and JarSigner 180 * to determine what files are signature related, which includes 181 * the MANIFEST, SF files, known signature block files, and other 182 * unknown signature related files (those starting with SIG- with 183 * an optional [A-Z0-9]{1,3} extension right inside META-INF). 184 * 185 * @param s file name 186 * @return true if the input file name is signature related 187 */ 188 public static boolean isSigningRelated(String name) { 189 name = name.toUpperCase(Locale.ENGLISH); 190 if (!name.startsWith("META-INF/")) { 191 return false; 192 } 193 name = name.substring(9); 194 if (name.indexOf('/') != -1) { 195 return false; 196 } 197 if (isBlockOrSF(name) || name.equals("MANIFEST.MF")) { 198 return true; 199 } else if (name.startsWith("SIG-")) { 200 // check filename extension 201 // see https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#Digital_Signatures 202 // for what filename extensions are legal 203 int extIndex = name.lastIndexOf('.'); 204 if (extIndex != -1) { 205 String ext = name.substring(extIndex + 1); 206 // validate length first 207 if (ext.length() > 3 || ext.length() < 1) { 208 return false; 209 } 210 // then check chars, must be in [a-zA-Z0-9] per the jar spec 211 for (int index = 0; index < ext.length(); index++) { 212 char cc = ext.charAt(index); 213 // chars are promoted to uppercase so skip lowercase checks 214 if ((cc < 'A' || cc > 'Z') && (cc < '0' || cc > '9')) { 215 return false; 216 } 217 } 218 } 219 return true; // no extension is OK 220 } 221 return false; 222 } 223 224 /** get digest from cache */ 225 226 private MessageDigest getDigest(String algorithm) throws SignatureException { 227 // check that algorithm is not restricted 228 if (!JAR_DISABLED_CHECK.permits(DIGEST_PRIMITIVE_SET, algorithm, null)) { 229 SignatureException e = 230 new SignatureException("SignatureFile check failed. " + 231 "Disabled algorithm used: " + algorithm); 232 throw e; 233 } 234 235 if (createdDigests == null) 236 createdDigests = new HashMap<String, MessageDigest>(); 237 238 MessageDigest digest = createdDigests.get(algorithm); 239 240 if (digest == null) { 241 try { 242 digest = MessageDigest.getInstance(algorithm); 243 createdDigests.put(algorithm, digest); 244 } catch (NoSuchAlgorithmException nsae) { 245 // ignore 246 } 247 } 248 return digest; 249 } 250 251 /** 252 * process the signature block file. Goes through the .SF file 253 * and adds code signers for each section where the .SF section 254 * hash was verified against the Manifest section. 255 * 256 * 257 */ 258 public void process(Hashtable<String, CodeSigner[]> signers, 259 List<Object> manifestDigests) 260 throws IOException, SignatureException, NoSuchAlgorithmException, 261 JarException, CertificateException 262 { 263 // calls Signature.getInstance() and MessageDigest.getInstance() 264 // need to use local providers here, see Providers class 265 Object obj = null; 266 try { 267 obj = Providers.startJarVerification(); 268 processImpl(signers, manifestDigests); 269 } finally { 270 Providers.stopJarVerification(obj); 271 } 272 273 } 274 275 private void processImpl(Hashtable<String, CodeSigner[]> signers, 276 List<Object> manifestDigests) 277 throws IOException, SignatureException, NoSuchAlgorithmException, 278 JarException, CertificateException 279 { 280 Manifest sf = new Manifest(); 281 sf.read(new ByteArrayInputStream(sfBytes)); 282 283 String version = 284 sf.getMainAttributes().getValue(Attributes.Name.SIGNATURE_VERSION); 285 286 if ((version == null) || !(version.equalsIgnoreCase("1.0"))) { 287 // XXX: should this be an exception? 288 // for now we just ignore this signature file 289 return; 290 } 291 292 SignerInfo[] infos = block.verify(sfBytes); 293 294 if (infos == null) { 295 throw new SecurityException("cannot verify signature block file " + 296 name); 297 } 298 299 300 CodeSigner[] newSigners = getSigners(infos, block); 301 302 // make sure we have something to do all this work for... 303 if (newSigners == null) 304 return; 305 306 Iterator<Map.Entry<String,Attributes>> entries = 307 sf.getEntries().entrySet().iterator(); 308 309 // see if we can verify the whole manifest first 310 boolean manifestSigned = verifyManifestHash(sf, md, manifestDigests); 311 312 // verify manifest main attributes 313 if (!manifestSigned && !verifyManifestMainAttrs(sf, md)) { 314 throw new SecurityException 315 ("Invalid signature file digest for Manifest main attributes"); 316 } 317 318 // go through each section in the signature file 319 while(entries.hasNext()) { 320 321 Map.Entry<String,Attributes> e = entries.next(); 322 String name = e.getKey(); 323 324 if (manifestSigned || 325 (verifySection(e.getValue(), name, md))) { 326 327 if (name.startsWith("./")) 328 name = name.substring(2); 329 330 if (name.startsWith("/")) 331 name = name.substring(1); 332 333 updateSigners(newSigners, signers, name); 334 335 if (debug != null) { 336 debug.println("processSignature signed name = "+name); 337 } 338 339 } else if (debug != null) { 340 debug.println("processSignature unsigned name = "+name); 341 } 342 } 343 344 // MANIFEST.MF is always regarded as signed 345 updateSigners(newSigners, signers, JarFile.MANIFEST_NAME); 346 } 347 348 /** 349 * See if the whole manifest was signed. 350 */ 351 private boolean verifyManifestHash(Manifest sf, 352 ManifestDigester md, 353 List<Object> manifestDigests) 354 throws IOException, SignatureException 355 { 356 Attributes mattr = sf.getMainAttributes(); 357 boolean manifestSigned = false; 358 359 // go through all the attributes and process *-Digest-Manifest entries 360 for (Map.Entry<Object,Object> se : mattr.entrySet()) { 361 362 String key = se.getKey().toString(); 363 364 if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST-MANIFEST")) { 365 // 16 is length of "-Digest-Manifest" 366 String algorithm = key.substring(0, key.length()-16); 367 368 manifestDigests.add(key); 369 manifestDigests.add(se.getValue()); 370 MessageDigest digest = getDigest(algorithm); 371 if (digest != null) { 372 byte[] computedHash = md.manifestDigest(digest); 373 byte[] expectedHash = 374 Base64.getMimeDecoder().decode((String)se.getValue()); 375 376 if (debug != null) { 377 debug.println("Signature File: Manifest digest " + 378 digest.getAlgorithm()); 379 debug.println( " sigfile " + toHex(expectedHash)); 380 debug.println( " computed " + toHex(computedHash)); 381 debug.println(); 382 } 383 384 if (MessageDigest.isEqual(computedHash, 385 expectedHash)) { 386 manifestSigned = true; 387 } else { 388 //XXX: we will continue and verify each section 389 } 390 } 391 } 392 } 393 return manifestSigned; 394 } 395 396 private boolean verifyManifestMainAttrs(Manifest sf, 397 ManifestDigester md) 398 throws IOException, SignatureException 399 { 400 Attributes mattr = sf.getMainAttributes(); 401 boolean attrsVerified = true; 402 403 // go through all the attributes and process 404 // digest entries for the manifest main attributes 405 for (Map.Entry<Object,Object> se : mattr.entrySet()) { 406 String key = se.getKey().toString(); 407 408 if (key.toUpperCase(Locale.ENGLISH).endsWith(ATTR_DIGEST)) { 409 String algorithm = 410 key.substring(0, key.length() - ATTR_DIGEST.length()); 411 412 MessageDigest digest = getDigest(algorithm); 413 if (digest != null) { 414 ManifestDigester.Entry mde = 415 md.get(ManifestDigester.MF_MAIN_ATTRS, false); 416 byte[] computedHash = mde.digest(digest); 417 byte[] expectedHash = 418 Base64.getMimeDecoder().decode((String)se.getValue()); 419 420 if (debug != null) { 421 debug.println("Signature File: " + 422 "Manifest Main Attributes digest " + 423 digest.getAlgorithm()); 424 debug.println( " sigfile " + toHex(expectedHash)); 425 debug.println( " computed " + toHex(computedHash)); 426 debug.println(); 427 } 428 429 if (MessageDigest.isEqual(computedHash, 430 expectedHash)) { 431 // good 432 } else { 433 // we will *not* continue and verify each section 434 attrsVerified = false; 435 if (debug != null) { 436 debug.println("Verification of " + 437 "Manifest main attributes failed"); 438 debug.println(); 439 } 440 break; 441 } 442 } 443 } 444 } 445 446 // this method returns 'true' if either: 447 // . manifest main attributes were not signed, or 448 // . manifest main attributes were signed and verified 449 return attrsVerified; 450 } 451 452 /** 453 * given the .SF digest header, and the data from the 454 * section in the manifest, see if the hashes match. 455 * if not, throw a SecurityException. 456 * 457 * @return true if all the -Digest headers verified 458 * @exception SecurityException if the hash was not equal 459 */ 460 461 private boolean verifySection(Attributes sfAttr, 462 String name, 463 ManifestDigester md) 464 throws IOException, SignatureException 465 { 466 boolean oneDigestVerified = false; 467 ManifestDigester.Entry mde = md.get(name,block.isOldStyle()); 468 469 if (mde == null) { 470 throw new SecurityException( 471 "no manifest section for signature file entry "+name); 472 } 473 474 if (sfAttr != null) { 475 476 //sun.misc.HexDumpEncoder hex = new sun.misc.HexDumpEncoder(); 477 //hex.encodeBuffer(data, System.out); 478 479 // go through all the attributes and process *-Digest entries 480 for (Map.Entry<Object,Object> se : sfAttr.entrySet()) { 481 String key = se.getKey().toString(); 482 483 if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST")) { 484 // 7 is length of "-Digest" 485 String algorithm = key.substring(0, key.length()-7); 486 487 MessageDigest digest = getDigest(algorithm); 488 489 if (digest != null) { 490 boolean ok = false; 491 492 byte[] expected = 493 Base64.getMimeDecoder().decode((String)se.getValue()); 494 byte[] computed; 495 if (workaround) { 496 computed = mde.digestWorkaround(digest); 497 } else { 498 computed = mde.digest(digest); 499 } 500 501 if (debug != null) { 502 debug.println("Signature Block File: " + 503 name + " digest=" + digest.getAlgorithm()); 504 debug.println(" expected " + toHex(expected)); 505 debug.println(" computed " + toHex(computed)); 506 debug.println(); 507 } 508 509 if (MessageDigest.isEqual(computed, expected)) { 510 oneDigestVerified = true; 511 ok = true; 512 } else { 513 // attempt to fallback to the workaround 514 if (!workaround) { 515 computed = mde.digestWorkaround(digest); 516 if (MessageDigest.isEqual(computed, expected)) { 517 if (debug != null) { 518 debug.println(" re-computed " + toHex(computed)); 519 debug.println(); 520 } 521 workaround = true; 522 oneDigestVerified = true; 523 ok = true; 524 } 525 } 526 } 527 if (!ok){ 528 throw new SecurityException("invalid " + 529 digest.getAlgorithm() + 530 " signature file digest for " + name); 531 } 532 } 533 } 534 } 535 } 536 return oneDigestVerified; 537 } 538 539 /** 540 * Given the PKCS7 block and SignerInfo[], create an array of 541 * CodeSigner objects. We do this only *once* for a given 542 * signature block file. 543 */ 544 private CodeSigner[] getSigners(SignerInfo infos[], PKCS7 block) 545 throws IOException, NoSuchAlgorithmException, SignatureException, 546 CertificateException { 547 548 ArrayList<CodeSigner> signers = null; 549 550 for (int i = 0; i < infos.length; i++) { 551 552 SignerInfo info = infos[i]; 553 ArrayList<X509Certificate> chain = info.getCertificateChain(block); 554 CertPath certChain = certificateFactory.generateCertPath(chain); 555 if (signers == null) { 556 signers = new ArrayList<CodeSigner>(); 557 } 558 // Append the new code signer 559 signers.add(new CodeSigner(certChain, info.getTimestamp())); 560 561 if (debug != null) { 562 debug.println("Signature Block Certificate: " + 563 chain.get(0)); 564 } 565 } 566 567 if (signers != null) { 568 return signers.toArray(new CodeSigner[signers.size()]); 569 } else { 570 return null; 571 } 572 } 573 574 // for the toHex function 575 private static final char[] hexc = 576 {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; 577 /** 578 * convert a byte array to a hex string for debugging purposes 579 * @param data the binary data to be converted to a hex string 580 * @return an ASCII hex string 581 */ 582 583 static String toHex(byte[] data) { 584 585 StringBuffer sb = new StringBuffer(data.length*2); 586 587 for (int i=0; i<data.length; i++) { 588 sb.append(hexc[(data[i] >>4) & 0x0f]); 589 sb.append(hexc[data[i] & 0x0f]); 590 } 591 return sb.toString(); 592 } 593 594 // returns true if set contains signer 595 static boolean contains(CodeSigner[] set, CodeSigner signer) 596 { 597 for (int i = 0; i < set.length; i++) { 598 if (set[i].equals(signer)) 599 return true; 600 } 601 return false; 602 } 603 604 // returns true if subset is a subset of set 605 static boolean isSubSet(CodeSigner[] subset, CodeSigner[] set) 606 { 607 // check for the same object 608 if (set == subset) 609 return true; 610 611 boolean match; 612 for (int i = 0; i < subset.length; i++) { 613 if (!contains(set, subset[i])) 614 return false; 615 } 616 return true; 617 } 618 619 /** 620 * returns true if signer contains exactly the same code signers as 621 * oldSigner and newSigner, false otherwise. oldSigner 622 * is allowed to be null. 623 */ 624 static boolean matches(CodeSigner[] signers, CodeSigner[] oldSigners, 625 CodeSigner[] newSigners) { 626 627 // special case 628 if ((oldSigners == null) && (signers == newSigners)) 629 return true; 630 631 boolean match; 632 633 // make sure all oldSigners are in signers 634 if ((oldSigners != null) && !isSubSet(oldSigners, signers)) 635 return false; 636 637 // make sure all newSigners are in signers 638 if (!isSubSet(newSigners, signers)) { 639 return false; 640 } 641 642 // now make sure all the code signers in signers are 643 // also in oldSigners or newSigners 644 645 for (int i = 0; i < signers.length; i++) { 646 boolean found = 647 ((oldSigners != null) && contains(oldSigners, signers[i])) || 648 contains(newSigners, signers[i]); 649 if (!found) 650 return false; 651 } 652 return true; 653 } 654 655 void updateSigners(CodeSigner[] newSigners, 656 Hashtable<String, CodeSigner[]> signers, String name) { 657 658 CodeSigner[] oldSigners = signers.get(name); 659 660 // search through the cache for a match, go in reverse order 661 // as we are more likely to find a match with the last one 662 // added to the cache 663 664 CodeSigner[] cachedSigners; 665 for (int i = signerCache.size() - 1; i != -1; i--) { 666 cachedSigners = signerCache.get(i); 667 if (matches(cachedSigners, oldSigners, newSigners)) { 668 signers.put(name, cachedSigners); 669 return; 670 } 671 } 672 673 if (oldSigners == null) { 674 cachedSigners = newSigners; 675 } else { 676 cachedSigners = 677 new CodeSigner[oldSigners.length + newSigners.length]; 678 System.arraycopy(oldSigners, 0, cachedSigners, 0, 679 oldSigners.length); 680 System.arraycopy(newSigners, 0, cachedSigners, oldSigners.length, 681 newSigners.length); 682 } 683 signerCache.add(cachedSigners); 684 signers.put(name, cachedSigners); 685 } 686 } 687