1 /*
  2  * Copyright (c) 1997, 2019, 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.GeneralSecurityException;
 32 import java.security.MessageDigest;
 33 import java.security.NoSuchAlgorithmException;
 34 import java.security.SignatureException;
 35 import java.security.Timestamp;
 36 import java.security.cert.CertPath;
 37 import java.security.cert.X509Certificate;
 38 import java.security.cert.CertificateException;
 39 import java.security.cert.CertificateFactory;
 40 import java.util.ArrayList;
 41 import java.util.Base64;
 42 import java.util.HashMap;
 43 import java.util.Hashtable;
 44 import java.util.Iterator;
 45 import java.util.List;
 46 import java.util.Locale;
 47 import java.util.Map;
 48 import java.util.jar.Attributes;
 49 import java.util.jar.JarException;
 50 import java.util.jar.JarFile;
 51 import java.util.jar.Manifest;
 52 
 53 import sun.security.jca.Providers;
 54 import sun.security.pkcs.PKCS7;
 55 import sun.security.pkcs.SignerInfo;
 56 
 57 public class SignatureFileVerifier {
 58 
 59     /* Are we debugging ? */
 60     private static final Debug debug = Debug.getInstance("jar");
 61 
 62     /**
 63      * Holder class to delay initialization of DisabledAlgorithmConstraints
 64      * until needed.
 65      */
 66     private static class ConfigurationHolder {
 67         static final DisabledAlgorithmConstraints JAR_DISABLED_CHECK =
 68             new DisabledAlgorithmConstraints(
 69                     DisabledAlgorithmConstraints.PROPERTY_JAR_DISABLED_ALGS);
 70     }
 71 
 72     private ArrayList<CodeSigner[]> signerCache;
 73 
 74     private static final String ATTR_DIGEST =
 75         "-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS.toUpperCase(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     /** Algorithms that have been checked if they are weak. */
101     private Map<String, Boolean> permittedAlgs= new HashMap<>();
102 
103     /** TSA timestamp of signed jar.  The newest timestamp is used.  If there
104      *  was no TSA timestamp used when signed, current time is used ("null").
105      */
106     private Timestamp timestamp = null;
107 
108     /**
109      * Create the named SignatureFileVerifier.
110      *
111      * @param name the name of the signature block file (.DSA/.RSA/.EC)
112      *
113      * @param rawBytes the raw bytes of the signature block file
114      */
115     public SignatureFileVerifier(ArrayList<CodeSigner[]> signerCache,
116                                  ManifestDigester md,
117                                  String name,
118                                  byte[] rawBytes)
119         throws IOException, CertificateException
120     {
121         // new PKCS7() calls CertificateFactory.getInstance()
122         // need to use local providers here, see Providers class
123         Object obj = null;
124         try {
125             obj = Providers.startJarVerification();
126             block = new PKCS7(rawBytes);
127             sfBytes = block.getContentInfo().getData();
128             certificateFactory = CertificateFactory.getInstance("X509");
129         } finally {
130             Providers.stopJarVerification(obj);
131         }
132         this.name = name.substring(0, name.lastIndexOf('.'))
133                                                    .toUpperCase(Locale.ENGLISH);
134         this.md = md;
135         this.signerCache = signerCache;
136     }
137 
138     /**
139      * returns true if we need the .SF file
140      */
141     public boolean needSignatureFileBytes()
142     {
143 
144         return sfBytes == null;
145     }
146 
147 
148     /**
149      * returns true if we need this .SF file.
150      *
151      * @param name the name of the .SF file without the extension
152      *
153      */
154     public boolean needSignatureFile(String name)
155     {
156         return this.name.equalsIgnoreCase(name);
157     }
158 
159     /**
160      * used to set the raw bytes of the .SF file when it
161      * is external to the signature block file.
162      */
163     public void setSignatureFile(byte[] sfBytes)
164     {
165         this.sfBytes = sfBytes;
166     }
167 
168     /**
169      * Utility method used by JarVerifier and JarSigner
170      * to determine the signature file names and PKCS7 block
171      * files names that are supported
172      *
173      * @param s file name
174      * @return true if the input file name is a supported
175      *          Signature File or PKCS7 block file name
176      */
177     public static boolean isBlockOrSF(String s) {
178         // Note: keep this in sync with j.u.z.ZipFile.Source#isSignatureRelated
179         // we currently only support DSA and RSA PKCS7 blocks
180         return s.endsWith(".SF")
181             || s.endsWith(".DSA")
182             || s.endsWith(".RSA")
183             || s.endsWith(".EC");
184     }
185 
186     /**
187      * Yet another utility method used by JarVerifier and JarSigner
188      * to determine what files are signature related, which includes
189      * the MANIFEST, SF files, known signature block files, and other
190      * unknown signature related files (those starting with SIG- with
191      * an optional [A-Z0-9]{1,3} extension right inside META-INF).
192      *
193      * @param name file name
194      * @return true if the input file name is signature related
195      */
196     public static boolean isSigningRelated(String name) {
197         name = name.toUpperCase(Locale.ENGLISH);
198         if (!name.startsWith("META-INF/")) {
199             return false;
200         }
201         name = name.substring(9);
202         if (name.indexOf('/') != -1) {
203             return false;
204         }
205         if (isBlockOrSF(name) || name.equals("MANIFEST.MF")) {
206             return true;
207         } else if (name.startsWith("SIG-")) {
208             // check filename extension
209             // see http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#Digital_Signatures
210             // for what filename extensions are legal
211             int extIndex = name.lastIndexOf('.');
212             if (extIndex != -1) {
213                 String ext = name.substring(extIndex + 1);
214                 // validate length first
215                 if (ext.length() > 3 || ext.length() < 1) {
216                     return false;
217                 }
218                 // then check chars, must be in [a-zA-Z0-9] per the jar spec
219                 for (int index = 0; index < ext.length(); index++) {
220                     char cc = ext.charAt(index);
221                     // chars are promoted to uppercase so skip lowercase checks
222                     if ((cc < 'A' || cc > 'Z') && (cc < '0' || cc > '9')) {
223                         return false;
224                     }
225                 }
226             }
227             return true; // no extension is OK
228         }
229         return false;
230     }
231 
232     /** get digest from cache */
233 
234     private MessageDigest getDigest(String algorithm)
235             throws SignatureException {
236         if (createdDigests == null)
237             createdDigests = new HashMap<>();
238 
239         MessageDigest digest = createdDigests.get(algorithm);
240 
241         if (digest == null) {
242             try {
243                 digest = MessageDigest.getInstance(algorithm);
244                 createdDigests.put(algorithm, digest);
245             } catch (NoSuchAlgorithmException nsae) {
246                 // ignore
247             }
248         }
249         return digest;
250     }
251 
252     /**
253      * process the signature block file. Goes through the .SF file
254      * and adds code signers for each section where the .SF section
255      * hash was verified against the Manifest section.
256      *
257      *
258      */
259     public void process(Hashtable<String, CodeSigner[]> signers,
260             List<Object> manifestDigests)
261         throws IOException, SignatureException, NoSuchAlgorithmException,
262             JarException, CertificateException
263     {
264         // calls Signature.getInstance() and MessageDigest.getInstance()
265         // need to use local providers here, see Providers class
266         Object obj = null;
267         try {
268             obj = Providers.startJarVerification();
269             processImpl(signers, manifestDigests);
270         } finally {
271             Providers.stopJarVerification(obj);
272         }
273 
274     }
275 
276     private void processImpl(Hashtable<String, CodeSigner[]> signers,
277             List<Object> manifestDigests)
278         throws IOException, SignatureException, NoSuchAlgorithmException,
279             JarException, CertificateException
280     {
281         Manifest sf = new Manifest();
282         sf.read(new ByteArrayInputStream(sfBytes));
283 
284         String version =
285             sf.getMainAttributes().getValue(Attributes.Name.SIGNATURE_VERSION);
286 
287         if ((version == null) || !(version.equalsIgnoreCase("1.0"))) {
288             // XXX: should this be an exception?
289             // for now we just ignore this signature file
290             return;
291         }
292 
293         SignerInfo[] infos = block.verify(sfBytes);
294 
295         if (infos == null) {
296             throw new SecurityException("cannot verify signature block file " +
297                                         name);
298         }
299 
300 
301         CodeSigner[] newSigners = getSigners(infos, block);
302 
303         // make sure we have something to do all this work for...
304         if (newSigners == null)
305             return;
306 
307         /*
308          * Look for the latest timestamp in the signature block.  If an entry
309          * has no timestamp, use current time (aka null).
310          */
311         for (CodeSigner s: newSigners) {
312             if (debug != null) {
313                 debug.println("Gathering timestamp for:  " + s.toString());
314             }
315             if (s.getTimestamp() == null) {
316                 timestamp = null;
317                 break;
318             } else if (timestamp == null) {
319                 timestamp = s.getTimestamp();
320             } else {
321                 if (timestamp.getTimestamp().before(
322                         s.getTimestamp().getTimestamp())) {
323                     timestamp = s.getTimestamp();
324                 }
325             }
326         }
327 
328         Iterator<Map.Entry<String,Attributes>> entries =
329                                 sf.getEntries().entrySet().iterator();
330 
331         // see if we can verify the whole manifest first
332         boolean manifestSigned = verifyManifestHash(sf, md, manifestDigests);
333 
334         // verify manifest main attributes
335         if (!manifestSigned && !verifyManifestMainAttrs(sf, md)) {
336             throw new SecurityException
337                 ("Invalid signature file digest for Manifest main attributes");
338         }
339 
340         // go through each section in the signature file
341         while(entries.hasNext()) {
342 
343             Map.Entry<String,Attributes> e = entries.next();
344             String name = e.getKey();
345 
346             if (manifestSigned ||
347                 (verifySection(e.getValue(), name, md))) {
348 
349                 if (name.startsWith("./"))
350                     name = name.substring(2);
351 
352                 if (name.startsWith("/"))
353                     name = name.substring(1);
354 
355                 updateSigners(newSigners, signers, name);
356 
357                 if (debug != null) {
358                     debug.println("processSignature signed name = "+name);
359                 }
360 
361             } else if (debug != null) {
362                 debug.println("processSignature unsigned name = "+name);
363             }
364         }
365 
366         // MANIFEST.MF is always regarded as signed
367         updateSigners(newSigners, signers, JarFile.MANIFEST_NAME);
368     }
369 
370     /**
371      * Check if algorithm is permitted using the permittedAlgs Map.
372      * If the algorithm is not in the map, check against disabled algorithms and
373      * store the result. If the algorithm is in the map use that result.
374      * False is returned for weak algorithm, true for good algorithms.
375      */
376     boolean permittedCheck(String key, String algorithm) {
377         Boolean permitted = permittedAlgs.get(algorithm);
378         if (permitted == null) {
379             try {
380                 ConfigurationHolder.JAR_DISABLED_CHECK.permits(algorithm,
381                         new ConstraintsParameters(timestamp));
382             } catch(GeneralSecurityException e) {
383                 permittedAlgs.put(algorithm, Boolean.FALSE);
384                 permittedAlgs.put(key.toUpperCase(), Boolean.FALSE);
385                 if (debug != null) {
386                     if (e.getMessage() != null) {
387                         debug.println(key + ":  " + e.getMessage());
388                     } else {
389                         debug.println("Debug info only. " +  key + ":  " +
390                             algorithm +
391                             " was disabled, no exception msg given.");
392                         e.printStackTrace();
393                     }
394                 }
395                 return false;
396             }
397 
398             permittedAlgs.put(algorithm, Boolean.TRUE);
399             return true;
400         }
401 
402         // Algorithm has already been checked, return the value from map.
403         return permitted.booleanValue();
404     }
405 
406     /**
407      * With a given header (*-DIGEST*), return a string that lists all the
408      * algorithms associated with the header.
409      * If there are none, return "Unknown Algorithm".
410      */
411     String getWeakAlgorithms(String header) {
412         String w = "";
413         try {
414             for (String key : permittedAlgs.keySet()) {
415                 if (key.endsWith(header)) {
416                     w += key.substring(0, key.length() - header.length()) + " ";
417                 }
418             }
419         } catch (RuntimeException e) {
420             w = "Unknown Algorithm(s).  Error processing " + header + ".  " +
421                     e.getMessage();
422         }
423 
424         // This means we have an error in finding weak algorithms, run in
425         // debug mode to see permittedAlgs map's values.
426         if (w.isEmpty()) {
427             return "Unknown Algorithm(s)";
428         }
429 
430         return w;
431     }
432 
433     /**
434      * See if the whole manifest was signed.
435      */
436     private boolean verifyManifestHash(Manifest sf,
437                                        ManifestDigester md,
438                                        List<Object> manifestDigests)
439          throws IOException, SignatureException
440     {
441         Attributes mattr = sf.getMainAttributes();
442         boolean manifestSigned = false;
443         // If only weak algorithms are used.
444         boolean weakAlgs = true;
445         // If a "*-DIGEST-MANIFEST" entry is found.
446         boolean validEntry = false;
447 
448         // go through all the attributes and process *-Digest-Manifest entries
449         for (Map.Entry<Object,Object> se : mattr.entrySet()) {
450 
451             String key = se.getKey().toString();
452 
453             if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST-MANIFEST")) {
454                 // 16 is length of "-Digest-Manifest"
455                 String algorithm = key.substring(0, key.length()-16);
456                 validEntry = true;
457 
458                 // Check if this algorithm is permitted, skip if false.
459                 if (!permittedCheck(key, algorithm)) {
460                     continue;
461                 }
462 
463                 // A non-weak algorithm was used, any weak algorithms found do
464                 // not need to be reported.
465                 weakAlgs = false;
466 
467                 manifestDigests.add(key);
468                 manifestDigests.add(se.getValue());
469                 MessageDigest digest = getDigest(algorithm);
470                 if (digest != null) {
471                     byte[] computedHash = md.manifestDigest(digest);
472                     byte[] expectedHash =
473                         Base64.getMimeDecoder().decode((String)se.getValue());
474 
475                     if (debug != null) {
476                         debug.println("Signature File: Manifest digest " +
477                                 algorithm);
478                         debug.println( "  sigfile  " + toHex(expectedHash));
479                         debug.println( "  computed " + toHex(computedHash));
480                         debug.println();
481                     }
482 
483                     if (MessageDigest.isEqual(computedHash, expectedHash)) {
484                         manifestSigned = true;
485                     } else {
486                         //XXX: we will continue and verify each section
487                     }
488                 }
489             }
490         }
491 
492         if (debug != null) {
493             debug.println("PermittedAlgs mapping: ");
494             for (String key : permittedAlgs.keySet()) {
495                 debug.println(key + " : " +
496                         permittedAlgs.get(key).toString());
497             }
498         }
499 
500         // If there were only weak algorithms entries used, throw an exception.
501         if (validEntry && weakAlgs) {
502             throw new SignatureException("Manifest hash check failed " +
503                     "(DIGEST-MANIFEST). Disabled algorithm(s) used: " +
504                     getWeakAlgorithms("-DIGEST-MANIFEST"));
505         }
506         return manifestSigned;
507     }
508 
509     private boolean verifyManifestMainAttrs(Manifest sf, ManifestDigester md)
510          throws IOException, SignatureException
511     {
512         Attributes mattr = sf.getMainAttributes();
513         boolean attrsVerified = true;
514         // If only weak algorithms are used.
515         boolean weakAlgs = true;
516         // If a ATTR_DIGEST entry is found.
517         boolean validEntry = false;
518 
519         // go through all the attributes and process
520         // digest entries for the manifest main attributes
521         for (Map.Entry<Object,Object> se : mattr.entrySet()) {
522             String key = se.getKey().toString();
523 
524             if (key.toUpperCase(Locale.ENGLISH).endsWith(ATTR_DIGEST)) {
525                 String algorithm =
526                         key.substring(0, key.length() - ATTR_DIGEST.length());
527                 validEntry = true;
528 
529                 // Check if this algorithm is permitted, skip if false.
530                 if (!permittedCheck(key, algorithm)) {
531                     continue;
532                 }
533 
534                 // A non-weak algorithm was used, any weak algorithms found do
535                 // not need to be reported.
536                 weakAlgs = false;
537 
538                 MessageDigest digest = getDigest(algorithm);
539                 if (digest != null) {
540                     ManifestDigester.Entry mde = md.getMainAttsEntry(false);
541                     byte[] computedHash = mde.digest(digest);
542                     byte[] expectedHash =
543                         Base64.getMimeDecoder().decode((String)se.getValue());
544 
545                     if (debug != null) {
546                      debug.println("Signature File: " +
547                                         "Manifest Main Attributes digest " +
548                                         digest.getAlgorithm());
549                      debug.println( "  sigfile  " + toHex(expectedHash));
550                      debug.println( "  computed " + toHex(computedHash));
551                      debug.println();
552                     }
553 
554                     if (MessageDigest.isEqual(computedHash, expectedHash)) {
555                         // good
556                     } else {
557                         // we will *not* continue and verify each section
558                         attrsVerified = false;
559                         if (debug != null) {
560                             debug.println("Verification of " +
561                                         "Manifest main attributes failed");
562                             debug.println();
563                         }
564                         break;
565                     }
566                 }
567             }
568         }
569 
570         if (debug != null) {
571             debug.println("PermittedAlgs mapping: ");
572             for (String key : permittedAlgs.keySet()) {
573                 debug.println(key + " : " +
574                         permittedAlgs.get(key).toString());
575             }
576         }
577 
578         // If there were only weak algorithms entries used, throw an exception.
579         if (validEntry && weakAlgs) {
580             throw new SignatureException("Manifest Main Attribute check " +
581                     "failed (" + ATTR_DIGEST + ").  " +
582                     "Disabled algorithm(s) used: " +
583                     getWeakAlgorithms(ATTR_DIGEST));
584         }
585 
586         // this method returns 'true' if either:
587         //      . manifest main attributes were not signed, or
588         //      . manifest main attributes were signed and verified
589         return attrsVerified;
590     }
591 
592     /**
593      * given the .SF digest header, and the data from the
594      * section in the manifest, see if the hashes match.
595      * if not, throw a SecurityException.
596      *
597      * @return true if all the -Digest headers verified
598      * @exception SecurityException if the hash was not equal
599      */
600 
601     private boolean verifySection(Attributes sfAttr,
602                                   String name,
603                                   ManifestDigester md)
604          throws IOException, SignatureException
605     {
606         boolean oneDigestVerified = false;
607         ManifestDigester.Entry mde = md.get(name,block.isOldStyle());
608         // If only weak algorithms are used.
609         boolean weakAlgs = true;
610         // If a "*-DIGEST" entry is found.
611         boolean validEntry = false;
612 
613         if (mde == null) {
614             throw new SecurityException(
615                   "no manifest section for signature file entry "+name);
616         }
617 
618         if (sfAttr != null) {
619             //sun.security.util.HexDumpEncoder hex = new sun.security.util.HexDumpEncoder();
620             //hex.encodeBuffer(data, System.out);
621 
622             // go through all the attributes and process *-Digest entries
623             for (Map.Entry<Object,Object> se : sfAttr.entrySet()) {
624                 String key = se.getKey().toString();
625 
626                 if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST")) {
627                     // 7 is length of "-Digest"
628                     String algorithm = key.substring(0, key.length()-7);
629                     validEntry = true;
630 
631                     // Check if this algorithm is permitted, skip if false.
632                     if (!permittedCheck(key, algorithm)) {
633                         continue;
634                     }
635 
636                     // A non-weak algorithm was used, any weak algorithms found do
637                     // not need to be reported.
638                     weakAlgs = false;
639 
640                     MessageDigest digest = getDigest(algorithm);
641 
642                     if (digest != null) {
643                         boolean ok = false;
644 
645                         byte[] expected =
646                             Base64.getMimeDecoder().decode((String)se.getValue());
647                         byte[] computed;
648                         if (workaround) {
649                             computed = mde.digestWorkaround(digest);
650                         } else {
651                             computed = mde.digest(digest);
652                         }
653 
654                         if (debug != null) {
655                           debug.println("Signature Block File: " +
656                                    name + " digest=" + digest.getAlgorithm());
657                           debug.println("  expected " + toHex(expected));
658                           debug.println("  computed " + toHex(computed));
659                           debug.println();
660                         }
661 
662                         if (MessageDigest.isEqual(computed, expected)) {
663                             oneDigestVerified = true;
664                             ok = true;
665                         } else {
666                             // attempt to fallback to the workaround
667                             if (!workaround) {
668                                computed = mde.digestWorkaround(digest);
669                                if (MessageDigest.isEqual(computed, expected)) {
670                                    if (debug != null) {
671                                        debug.println("  re-computed " + toHex(computed));
672                                        debug.println();
673                                    }
674                                    workaround = true;
675                                    oneDigestVerified = true;
676                                    ok = true;
677                                }
678                             }
679                         }
680                         if (!ok){
681                             throw new SecurityException("invalid " +
682                                        digest.getAlgorithm() +
683                                        " signature file digest for " + name);
684                         }
685                     }
686                 }
687             }
688         }
689 
690         if (debug != null) {
691             debug.println("PermittedAlgs mapping: ");
692             for (String key : permittedAlgs.keySet()) {
693                 debug.println(key + " : " +
694                         permittedAlgs.get(key).toString());
695             }
696         }
697 
698         // If there were only weak algorithms entries used, throw an exception.
699         if (validEntry && weakAlgs) {
700             throw new SignatureException("Manifest Main Attribute check " +
701                     "failed (DIGEST).  Disabled algorithm(s) used: " +
702                     getWeakAlgorithms("DIGEST"));
703         }
704 
705         return oneDigestVerified;
706     }
707 
708     /**
709      * Given the PKCS7 block and SignerInfo[], create an array of
710      * CodeSigner objects. We do this only *once* for a given
711      * signature block file.
712      */
713     private CodeSigner[] getSigners(SignerInfo[] infos, PKCS7 block)
714         throws IOException, NoSuchAlgorithmException, SignatureException,
715             CertificateException {
716 
717         ArrayList<CodeSigner> signers = null;
718 
719         for (int i = 0; i < infos.length; i++) {
720 
721             SignerInfo info = infos[i];
722             ArrayList<X509Certificate> chain = info.getCertificateChain(block);
723             CertPath certChain = certificateFactory.generateCertPath(chain);
724             if (signers == null) {
725                 signers = new ArrayList<>();
726             }
727             // Append the new code signer. If timestamp is invalid, this
728             // jar will be treated as unsigned.
729             signers.add(new CodeSigner(certChain, info.getTimestamp()));
730 
731             if (debug != null) {
732                 debug.println("Signature Block Certificate: " +
733                     chain.get(0));
734             }
735         }
736 
737         if (signers != null) {
738             return signers.toArray(new CodeSigner[signers.size()]);
739         } else {
740             return null;
741         }
742     }
743 
744     // for the toHex function
745     private static final char[] hexc =
746             {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
747     /**
748      * convert a byte array to a hex string for debugging purposes
749      * @param data the binary data to be converted to a hex string
750      * @return an ASCII hex string
751      */
752 
753     static String toHex(byte[] data) {
754 
755         StringBuilder sb = new StringBuilder(data.length*2);
756 
757         for (int i=0; i<data.length; i++) {
758             sb.append(hexc[(data[i] >>4) & 0x0f]);
759             sb.append(hexc[data[i] & 0x0f]);
760         }
761         return sb.toString();
762     }
763 
764     // returns true if set contains signer
765     static boolean contains(CodeSigner[] set, CodeSigner signer)
766     {
767         for (int i = 0; i < set.length; i++) {
768             if (set[i].equals(signer))
769                 return true;
770         }
771         return false;
772     }
773 
774     // returns true if subset is a subset of set
775     static boolean isSubSet(CodeSigner[] subset, CodeSigner[] set)
776     {
777         // check for the same object
778         if (set == subset)
779             return true;
780 
781         boolean match;
782         for (int i = 0; i < subset.length; i++) {
783             if (!contains(set, subset[i]))
784                 return false;
785         }
786         return true;
787     }
788 
789     /**
790      * returns true if signer contains exactly the same code signers as
791      * oldSigner and newSigner, false otherwise. oldSigner
792      * is allowed to be null.
793      */
794     static boolean matches(CodeSigner[] signers, CodeSigner[] oldSigners,
795         CodeSigner[] newSigners) {
796 
797         // special case
798         if ((oldSigners == null) && (signers == newSigners))
799             return true;
800 
801         boolean match;
802 
803         // make sure all oldSigners are in signers
804         if ((oldSigners != null) && !isSubSet(oldSigners, signers))
805             return false;
806 
807         // make sure all newSigners are in signers
808         if (!isSubSet(newSigners, signers)) {
809             return false;
810         }
811 
812         // now make sure all the code signers in signers are
813         // also in oldSigners or newSigners
814 
815         for (int i = 0; i < signers.length; i++) {
816             boolean found =
817                 ((oldSigners != null) && contains(oldSigners, signers[i])) ||
818                 contains(newSigners, signers[i]);
819             if (!found)
820                 return false;
821         }
822         return true;
823     }
824 
825     void updateSigners(CodeSigner[] newSigners,
826         Hashtable<String, CodeSigner[]> signers, String name) {
827 
828         CodeSigner[] oldSigners = signers.get(name);
829 
830         // search through the cache for a match, go in reverse order
831         // as we are more likely to find a match with the last one
832         // added to the cache
833 
834         CodeSigner[] cachedSigners;
835         for (int i = signerCache.size() - 1; i != -1; i--) {
836             cachedSigners = signerCache.get(i);
837             if (matches(cachedSigners, oldSigners, newSigners)) {
838                 signers.put(name, cachedSigners);
839                 return;
840             }
841         }
842 
843         if (oldSigners == null) {
844             cachedSigners = newSigners;
845         } else {
846             cachedSigners =
847                 new CodeSigner[oldSigners.length + newSigners.length];
848             System.arraycopy(oldSigners, 0, cachedSigners, 0,
849                 oldSigners.length);
850             System.arraycopy(newSigners, 0, cachedSigners, oldSigners.length,
851                 newSigners.length);
852         }
853         signerCache.add(cachedSigners);
854         signers.put(name, cachedSigners);
855     }
856 }