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
26package sun.security.util;
27
28import java.io.ByteArrayInputStream;
29import java.io.IOException;
30import java.security.CodeSigner;
31import java.security.CryptoPrimitive;
32import java.security.MessageDigest;
33import java.security.NoSuchAlgorithmException;
34import java.security.SignatureException;
35import java.security.cert.CertPath;
36import java.security.cert.X509Certificate;
37import java.security.cert.CertificateException;
38import java.security.cert.CertificateFactory;
39import java.util.ArrayList;
40import java.util.Base64;
41import java.util.Collections;
42import java.util.EnumSet;
43import java.util.HashMap;
44import java.util.Hashtable;
45import java.util.Iterator;
46import java.util.List;
47import java.util.Locale;
48import java.util.Map;
49import java.util.Set;
50import java.util.jar.Attributes;
51import java.util.jar.JarException;
52import java.util.jar.JarFile;
53import java.util.jar.Manifest;
54
55import sun.security.jca.Providers;
56import sun.security.pkcs.PKCS7;
57import sun.security.pkcs.SignerInfo;
58
59public 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