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