1 /*
   2  * Copyright (c) 1996, 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.tools.jar;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 import java.security.*;
  31 
  32 import sun.net.www.MessageHeader;
  33 import java.util.Base64;
  34 
  35 
  36 import sun.security.pkcs.*;
  37 import sun.security.x509.AlgorithmId;
  38 
  39 /**
  40  * <p>A signature file as defined in the <a
  41  * href="manifest.html">Manifest and Signature Format</a>. It has
  42  * essentially the same structure as a Manifest file in that it is a
  43  * set of RFC 822 headers (sections). The first section contains meta
  44  * data relevant to the entire file (i.e "Signature-Version:1.0") and
  45  * each subsequent section contains data relevant to specific entries:
  46  * entry sections.
  47  *
  48  * <p>Each entry section contains the name of an entry (which must
  49  * have a counterpart in the manifest). Like the manifest it contains
  50  * a hash, the hash of the manifest section corresponding to the
  51  * name. Since the manifest entry contains the hash of the data, this
  52  * is equivalent to a signature of the data, plus the attributes of
  53  * the manifest entry.
  54  *
  55  * <p>This signature file format deal with PKCS7 encoded DSA signature
  56  * block. It should be straightforward to extent to support other
  57  * algorithms.
  58  *
  59  * @author      David Brown
  60  * @author      Benjamin Renaud */
  61 
  62 public class SignatureFile {
  63 
  64     /* Are we debugging? */
  65     static final boolean debug = false;
  66 
  67     /* list of headers that all pertain to a particular file in the
  68      * archive */
  69     private Vector<MessageHeader> entries = new Vector<>();
  70 
  71     /* Right now we only support SHA hashes */
  72     static final String[] hashes = {"SHA"};
  73 
  74     static final void debug(String s) {
  75         if (debug)
  76             System.out.println("sig> " + s);
  77     }
  78 
  79     /*
  80      * The manifest we're working with.  */
  81     private Manifest manifest;
  82 
  83     /*
  84      * The file name for the file. This is the raw name, i.e. the
  85      * extention-less 8 character name (such as MYSIGN) which wil be
  86      * used to build the signature filename (MYSIGN.SF) and the block
  87      * filename (MYSIGN.DSA) */
  88     private String rawName;
  89 
  90     /* The digital signature block corresponding to this signature
  91      * file.  */
  92     private PKCS7 signatureBlock;
  93 
  94 
  95     /**
  96      * Private constructor which takes a name a given signature
  97      * file. The name must be extension-less and less or equal to 8
  98      * character in length.  */
  99     private SignatureFile(String name) throws JarException {
 100 
 101         entries = new Vector<>();
 102 
 103         if (name != null) {
 104             if (name.length() > 8 || name.indexOf('.') != -1) {
 105                 throw new JarException("invalid file name");
 106             }
 107             rawName = name.toUpperCase(Locale.ENGLISH);
 108         }
 109     }
 110 
 111     /**
 112      * Private constructor which takes a name a given signature file
 113      * and a new file predicate. If it is a new file, a main header
 114      * will be added. */
 115     private SignatureFile(String name, boolean newFile)
 116     throws JarException {
 117 
 118         this(name);
 119 
 120         if (newFile) {
 121             MessageHeader globals = new MessageHeader();
 122             globals.set("Signature-Version", "1.0");
 123             entries.addElement(globals);
 124         }
 125     }
 126 
 127     /**
 128      * Constructs a new Signature file corresponding to a given
 129      * Manifest. All entries in the manifest are signed.
 130      *
 131      * @param manifest the manifest to use.
 132      *
 133      * @param name for this signature file. This should
 134      * be less than 8 characters, and without a suffix (i.e.
 135      * without a period in it.
 136      *
 137      * @exception JarException if an invalid name is passed in.
 138      */
 139     public SignatureFile(Manifest manifest, String name)
 140     throws JarException {
 141 
 142         this(name, true);
 143 
 144         this.manifest = manifest;
 145         Enumeration<MessageHeader> enum_ = manifest.entries();
 146         while (enum_.hasMoreElements()) {
 147             MessageHeader mh = enum_.nextElement();
 148             String entryName = mh.findValue("Name");
 149             if (entryName != null) {
 150                 add(entryName);
 151             }
 152         }
 153     }
 154 
 155     /**
 156      * Constructs a new Signature file corresponding to a given
 157      * Manifest. Specific entries in the manifest are signed.
 158      *
 159      * @param manifest the manifest to use.
 160      *
 161      * @param entries the entries to sign.
 162      *
 163      * @param filename for this signature file. This should
 164      * be less than 8 characters, and without a suffix (i.e.
 165      * without a period in it.
 166      *
 167      * @exception JarException if an invalid name is passed in.
 168      */
 169     public SignatureFile(Manifest manifest, String[] entries,
 170                          String filename)
 171     throws JarException {
 172         this(filename, true);
 173         this.manifest = manifest;
 174         add(entries);
 175     }
 176 
 177     /**
 178      * Construct a Signature file from an input stream.
 179      *
 180      * @exception IOException if an invalid name is passed in or if a
 181      * stream exception occurs.
 182      */
 183     public SignatureFile(InputStream is, String filename)
 184     throws IOException {
 185         this(filename);
 186         while (is.available() > 0) {
 187             MessageHeader m = new MessageHeader(is);
 188             entries.addElement(m);
 189         }
 190     }
 191 
 192    /**
 193      * Construct a Signature file from an input stream.
 194      *
 195      * @exception IOException if an invalid name is passed in or if a
 196      * stream exception occurs.
 197      */
 198     public SignatureFile(InputStream is) throws IOException {
 199         this(is, null);
 200     }
 201 
 202     public SignatureFile(byte[] bytes) throws IOException {
 203         this(new ByteArrayInputStream(bytes));
 204     }
 205 
 206     /**
 207      * Returns the name of the signature file, ending with a ".SF"
 208      * suffix */
 209     public String getName() {
 210         return "META-INF/" + rawName + ".SF";
 211     }
 212 
 213     /**
 214      * Returns the name of the block file, ending with a block suffix
 215      * such as ".DSA". */
 216     public String getBlockName() {
 217         String suffix = "DSA";
 218         if (signatureBlock != null) {
 219             SignerInfo info = signatureBlock.getSignerInfos()[0];
 220             suffix = info.getDigestEncryptionAlgorithmId().getName();
 221             String temp = AlgorithmId.getEncAlgFromSigAlg(suffix);
 222             if (temp != null) suffix = temp;
 223         }
 224         return "META-INF/" + rawName + "." + suffix;
 225     }
 226 
 227     /**
 228      * Returns the signature block associated with this file.
 229      */
 230     public PKCS7 getBlock() {
 231         return signatureBlock;
 232     }
 233 
 234     /**
 235      * Sets the signature block associated with this file.
 236      */
 237     public void setBlock(PKCS7 block) {
 238         this.signatureBlock = block;
 239     }
 240 
 241     /**
 242      * Add a set of entries from the current manifest.
 243      */
 244     public void add(String[] entries) throws JarException {
 245         for (int i = 0; i < entries.length; i++) {
 246             add (entries[i]);
 247         }
 248     }
 249 
 250     /**
 251      * Add a specific entry from the current manifest.
 252      */
 253     public void add(String entry) throws JarException {
 254         MessageHeader mh = manifest.getEntry(entry);
 255         if (mh == null) {
 256             throw new JarException("entry " + entry + " not in manifest");
 257         }
 258         MessageHeader smh;
 259         try {
 260             smh = computeEntry(mh);
 261         } catch (IOException e) {
 262             throw new JarException(e.getMessage());
 263         }
 264         entries.addElement(smh);
 265     }
 266 
 267     /**
 268      * Get the entry corresponding to a given name. Returns null if
 269      *the entry does not exist.
 270      */
 271     public MessageHeader getEntry(String name) {
 272         Enumeration<MessageHeader> enum_ = entries();
 273         while(enum_.hasMoreElements()) {
 274             MessageHeader mh = enum_.nextElement();
 275             if (name.equals(mh.findValue("Name"))) {
 276                 return mh;
 277             }
 278         }
 279         return null;
 280     }
 281 
 282     /**
 283      * Returns the n-th entry. The global header is a entry 0.  */
 284     public MessageHeader entryAt(int n) {
 285         return entries.elementAt(n);
 286     }
 287 
 288     /**
 289      * Returns an enumeration of the entries.
 290      */
 291     public Enumeration<MessageHeader> entries() {
 292         return entries.elements();
 293     }
 294 
 295     /**
 296      * Given a manifest entry, computes the signature entry for this
 297      * manifest entry.
 298      */
 299     private MessageHeader computeEntry(MessageHeader mh) throws IOException {
 300         MessageHeader smh = new MessageHeader();
 301 
 302         String name = mh.findValue("Name");
 303         if (name == null) {
 304             return null;
 305         }
 306         smh.set("Name", name);
 307 
 308         try {
 309             for (int i = 0; i < hashes.length; ++i) {
 310                 MessageDigest dig = getDigest(hashes[i]);
 311                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
 312                 PrintStream ps = new PrintStream(baos);
 313                 mh.print(ps);
 314                 byte[] headerBytes = baos.toByteArray();
 315                 byte[] digest = dig.digest(headerBytes);
 316                 smh.set(hashes[i] + "-Digest", Base64.getMimeEncoder().encodeToString(digest));
 317             }
 318             return smh;
 319         } catch (NoSuchAlgorithmException e) {
 320             throw new JarException(e.getMessage());
 321         }
 322     }
 323 
 324     private Hashtable<String, MessageDigest> digests = new Hashtable<>();
 325 
 326     private MessageDigest getDigest(String algorithm)
 327     throws NoSuchAlgorithmException {
 328         MessageDigest dig = digests.get(algorithm);
 329         if (dig == null) {
 330             dig = MessageDigest.getInstance(algorithm);
 331             digests.put(algorithm, dig);
 332         }
 333         dig.reset();
 334         return dig;
 335     }
 336 
 337 
 338     /**
 339      * Add a signature file at current position in a stream
 340      */
 341     public void stream(OutputStream os) throws IOException {
 342 
 343         /* the first header in the file should be the global one.
 344          * It should say "SignatureFile-Version: x.x"; barf if not
 345          */
 346         MessageHeader globals = entries.elementAt(0);
 347         if (globals.findValue("Signature-Version") == null) {
 348             throw new JarException("Signature file requires " +
 349                             "Signature-Version: 1.0 in 1st header");
 350         }
 351 
 352         PrintStream ps = new PrintStream(os);
 353         globals.print(ps);
 354 
 355         for (int i = 1; i < entries.size(); ++i) {
 356             MessageHeader mh = entries.elementAt(i);
 357             mh.print(ps);
 358         }
 359     }
 360 }