1 /*
   2  * Copyright (c) 2011, 2015, 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 com.sun.javafx.tools.packager;
  27 
  28 import java.io.ByteArrayOutputStream;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.UnsupportedEncodingException;
  32 import java.math.BigInteger;
  33 import java.security.CodeSigner;
  34 import java.security.InvalidKeyException;
  35 import java.security.NoSuchAlgorithmException;
  36 import java.security.Principal;
  37 import java.security.PrivateKey;
  38 import java.security.PublicKey;
  39 import java.security.Signature;
  40 import java.security.SignatureException;
  41 import java.security.Timestamp;
  42 import java.security.cert.CertPath;
  43 import java.security.cert.CertificateException;
  44 import java.security.cert.CertificateFactory;
  45 import java.security.cert.X509Certificate;
  46 import java.util.ArrayList;
  47 import java.util.Locale;
  48 import java.util.jar.JarFile;
  49 import java.util.zip.ZipEntry;
  50 import java.util.zip.ZipInputStream;
  51 import java.util.zip.ZipOutputStream;
  52 import sun.security.pkcs.ContentInfo;
  53 import sun.security.pkcs.PKCS7;
  54 import sun.security.pkcs.PKCS9Attribute;
  55 import sun.security.pkcs.PKCS9Attributes;
  56 import sun.security.pkcs.ParsingException;
  57 import sun.security.pkcs.SignerInfo;
  58 import sun.security.timestamp.TimestampToken;
  59 import sun.security.x509.AlgorithmId;
  60 import sun.security.x509.X500Name;
  61 
  62 /**
  63  * (Source copied from:
  64  *      com.sun.deploy.security.JarSignature in Deploy workspace)
  65  *
  66  * This class is an abstraction of signature that is currently used
  67  * for implementation of jar signing as BLOBs.
  68  *
  69  * There are 2 modes of use for this class - signing and validation
  70  * and same instance of JarSignature object can not be reused.
  71  *
  72  * Signing mode:
  73  *   - create new instance using JarSignature.create()
  74  *   - add entries you want to include into the signature using
  75  *      updateWithEntry(). (Note the order is important.)
  76  *   - use getEncoded() to get bytes for the result signature
  77  *
  78  * Validation mode:
  79  *   - create new instance using JarSignature.load()
  80  *   - add entries using updateWithEntry()
  81  *   - use isValid() to validate result
  82  *   - use getCodeSigners() to get list of code signers used
  83  */
  84 public class JarSignature {
  85     //name of jar manifest attribute that contains signature
  86     public static final String BLOB_SIGNATURE = "META-INF/SIGNATURE.BSF";
  87 
  88     private final Signature sig;
  89     private final X509Certificate certChain[]; // for singing scenarios only
  90     private final CodeSigner codeSigners[];    // for validation only
  91     private final SignerInfo signerInfos[];        // for validation only
  92 
  93     /**
  94      * Loads jar signature from given byte array.
  95      * If signature could not be reconstructed then exceptions are thrown.
  96      */
  97     public static JarSignature load(byte[] rawSignature) throws ParsingException,
  98             CertificateException, IOException, NoSuchAlgorithmException,
  99             InvalidKeyException, SignatureException
 100     {
 101         PKCS7 pkcs7 = new PKCS7(rawSignature);
 102         SignerInfo[] infos = pkcs7.getSignerInfos();
 103         if (infos == null || infos.length != 1) {
 104             throw new IllegalArgumentException(
 105                     "BLOB signature currently only support single signer.");
 106         }
 107         X509Certificate cert = infos[0].getCertificate(pkcs7);
 108         PublicKey publicKey = cert.getPublicKey();
 109         CodeSigner[] signers = extractCodeSigners(infos, pkcs7);
 110         Signature sig = getSignature(infos[0]);
 111         sig.initVerify(publicKey);
 112 
 113         return new JarSignature(sig, infos, signers);
 114     }
 115 
 116     /**
 117      * Creates new signature for signing.
 118      *
 119      * @param privateKey Key to be used for signing.
 120      * @return
 121      * @throws NoSuchAlgorithmException
 122      * @throws InvalidKeyException
 123      */
 124     public static JarSignature create(
 125                                 PrivateKey privateKey, X509Certificate chain[])
 126             throws NoSuchAlgorithmException, InvalidKeyException
 127     {
 128         Signature signature = getSignature(privateKey.getAlgorithm());
 129         signature.initSign(privateKey);
 130         return new JarSignature(signature, chain);
 131     }
 132 
 133     private JarSignature(Signature signature, X509Certificate chain[]) {
 134         certChain = chain;
 135         signerInfos = null;
 136         codeSigners = null;
 137         sig = signature;
 138     }
 139 
 140     private JarSignature(Signature signature, SignerInfo[] infos, CodeSigner[] signers) {
 141         certChain = null;
 142         signerInfos = infos;
 143         codeSigners = signers;
 144         sig = signature;
 145     }
 146 
 147     public boolean isValidationMode() {
 148         return certChain == null;
 149     }
 150 
 151     /**
 152      * Get default signature object base on default signature algorithm derived
 153      * from given key algorithm for use in encoding usage.
 154      */
 155     private static Signature getSignature(String keyAlgorithm)
 156             throws NoSuchAlgorithmException
 157     {
 158         if (keyAlgorithm.equalsIgnoreCase("DSA")) {
 159             return Signature.getInstance("SHA1withDSA");
 160         } else if (keyAlgorithm.equalsIgnoreCase("RSA")) {
 161             return Signature.getInstance("SHA256withRSA");
 162         } else if (keyAlgorithm.equalsIgnoreCase("EC")) {
 163             return Signature.getInstance("SHA256withECDSA");
 164         }
 165         throw new IllegalArgumentException(
 166                             "Key algorithm should be either DSA, RSA or EC");
 167     }
 168 
 169     /**
 170      * Derive Signature from signer info for use in validation.
 171      */
 172     private static Signature getSignature(SignerInfo info)
 173             throws NoSuchAlgorithmException
 174     {
 175         String digestAlgorithm = info.getDigestAlgorithmId().getName();
 176         String keyAlgorithm = info.getDigestEncryptionAlgorithmId().getName();
 177         String signatureAlgorithm = makeSigAlg(
 178                 digestAlgorithm, keyAlgorithm);
 179         return Signature.getInstance(signatureAlgorithm);
 180     }
 181 
 182     String getSignatureAlgorithm() throws NoSuchAlgorithmException {
 183         return sig.getAlgorithm();
 184     }
 185 
 186     AlgorithmId getDigestAlgorithm() throws NoSuchAlgorithmException {
 187         String name = getDigAlgFromSigAlg(sig.getAlgorithm());
 188         return AlgorithmId.get(name);
 189     }
 190 
 191     AlgorithmId getKeyAlgorithm() throws NoSuchAlgorithmException {
 192         String name = getEncAlgFromSigAlg(sig.getAlgorithm());
 193         return AlgorithmId.get(name);
 194     }
 195 
 196     private static String makeSigAlg(String digAlg, String encAlg) {
 197         digAlg = digAlg.replace("-", "").toUpperCase(Locale.ENGLISH);
 198         if (digAlg.equalsIgnoreCase("SHA")) digAlg = "SHA1";
 199 
 200         encAlg = encAlg.toUpperCase(Locale.ENGLISH);
 201         if (encAlg.equals("EC")) encAlg = "ECDSA";
 202 
 203         return digAlg + "with" + encAlg;
 204     }
 205 
 206     private static String getDigAlgFromSigAlg(String signatureAlgorithm) {
 207         signatureAlgorithm = signatureAlgorithm.toUpperCase(Locale.ENGLISH);
 208         int with = signatureAlgorithm.indexOf("WITH");
 209         if (with > 0) {
 210             return signatureAlgorithm.substring(0, with);
 211         }
 212         return null;
 213     }
 214 
 215     private static String getEncAlgFromSigAlg(String signatureAlgorithm) {
 216         signatureAlgorithm = signatureAlgorithm.toUpperCase(Locale.ENGLISH);
 217         int with = signatureAlgorithm.indexOf("WITH");
 218         String keyAlgorithm = null;
 219         if (with > 0) {
 220             int and = signatureAlgorithm.indexOf("AND", with + 4);
 221             if (and > 0) {
 222                 keyAlgorithm = signatureAlgorithm.substring(with + 4, and);
 223             } else {
 224                 keyAlgorithm = signatureAlgorithm.substring(with + 4);
 225             }
 226             if (keyAlgorithm.equalsIgnoreCase("ECDSA")) {
 227                 keyAlgorithm = "EC";
 228             }
 229         }
 230         return keyAlgorithm;
 231     }
 232 
 233     /**
 234      * Returns encoded representation of signature.
 235      * @throws UnsupportedOperationException if called in validation mode.
 236      */
 237     public byte[] getEncoded()
 238             throws NoSuchAlgorithmException, SignatureException, IOException {
 239         if (isValidationMode()) {
 240             throw new UnsupportedOperationException(
 241                     "Method is not for validation mode.");
 242         }
 243 
 244         AlgorithmId digestAlgId = getDigestAlgorithm();
 245         AlgorithmId[] digestAlgIds = {digestAlgId};
 246         ContentInfo contentInfo = new ContentInfo(ContentInfo.DATA_OID, null);
 247         Principal issuerName = certChain[0].getIssuerDN();
 248         BigInteger serialNumber = certChain[0].getSerialNumber();
 249         byte[] signature = sig.sign();
 250         SignerInfo signerInfo =
 251                 new SignerInfo((X500Name) issuerName, serialNumber, digestAlgId,
 252                                 getKeyAlgorithm(), signature);
 253 
 254         SignerInfo[] signerInfos = {signerInfo};
 255         PKCS7 pkcs7 = new PKCS7(digestAlgIds, contentInfo, certChain,
 256                 signerInfos);
 257         ByteArrayOutputStream bos = new ByteArrayOutputStream(8192);
 258         pkcs7.encodeSignedData(bos);
 259         return bos.toByteArray();
 260     }
 261 
 262     private class ValidationStream extends InputStream {
 263         InputStream dataStream = null;
 264 
 265         public ValidationStream(
 266                 InputStream is) {
 267             dataStream = is;
 268         }
 269 
 270         public int read() throws IOException {
 271             int v = dataStream.read();
 272             if (v > -1) {
 273                 try {
 274                     JarSignature.this.sig.update((byte) v);
 275                 } catch (SignatureException ex) {
 276                     throw new RuntimeException(ex);
 277                 }
 278             }
 279             return v;
 280         }
 281 
 282         public int read(byte[] b, int off, int len) throws IOException {
 283             len = dataStream.read(b, off, len);
 284             if (len > 0) {
 285                 try {
 286                     JarSignature.this.sig.update(b, off, len);
 287                 } catch (SignatureException ex) {
 288                     throw new RuntimeException(ex);
 289                 }
 290             }
 291             return len;
 292         }
 293 
 294         public void close() throws IOException {
 295             dataStream.close();
 296         }
 297 
 298     }
 299 
 300     /**
 301      * Performs partial validation of zip/jar entry with given name and
 302      * data stream. Return wrapped version of input stream to complete validation.
 303      * Stream need to be read fully for validation to be complete.
 304      *
 305      * @throws SignatureException
 306      */
 307     public InputStream updateWithZipEntry(String name, InputStream is)
 308             throws SignatureException {
 309         try {
 310             sig.update(name.getBytes("UTF-8"));
 311         } catch(UnsupportedEncodingException e) {
 312             throw new SignatureException(e);
 313         }
 314         return new ValidationStream(is);
 315     }
 316 
 317     public void update(byte[] v) throws SignatureException {
 318         sig.update(v);
 319     }
 320 
 321     public boolean isValid() {
 322         try {
 323             return sig.verify(signerInfos[0].getEncryptedDigest());
 324         } catch(Exception e) {
 325             return false;
 326         }
 327     }
 328 
 329     public CodeSigner[] getCodeSigners() {
 330         CodeSigner[] result = new CodeSigner[codeSigners.length];
 331         System.arraycopy(codeSigners, 0, result, 0, result.length);
 332         return result;
 333     }
 334 
 335     /*
 336      * Implementation is mostly borrowed from
 337      *   sun.security.util.SignatureFileVerifier
 338      */
 339     private static CodeSigner[] extractCodeSigners(SignerInfo infos[], PKCS7 block)
 340             throws IOException, NoSuchAlgorithmException, SignatureException,
 341             CertificateException {
 342         ArrayList s = new ArrayList();
 343 
 344         CertificateFactory certificateFactory =
 345                                     CertificateFactory.getInstance("X509");
 346         for (int i = 0; i < infos.length; i++) {
 347             SignerInfo info = infos[i];
 348             ArrayList chain = info.getCertificateChain(block);
 349             CertPath certPath = certificateFactory.generateCertPath(chain);
 350             // Append the new code signer
 351             CodeSigner signer =
 352                 new CodeSigner(certPath, getTimestamp(info, certificateFactory));
 353 //                if (block.getCRLs() != null) {
 354 //                    SharedSecrets.getJavaSecurityCodeSignerAccess().setCRLs(
 355 //                            signer, block.getCRLs());
 356 //                }
 357             s.add(signer);
 358         }
 359 
 360         return (CodeSigner[]) s.toArray(new CodeSigner[s.size()]);
 361     }
 362 
 363     /*
 364      * Examines a signature timestamp token to generate a timestamp object.
 365      *
 366      * Examines the signer's unsigned attributes for a
 367      * <tt>signatureTimestampToken</tt> attribute. If present,
 368      * then it is parsed to extract the date and time at which the
 369      * timestamp was generated.
 370      *
 371      * @param info A signer information element of a PKCS 7 block.
 372      *
 373      * @return A timestamp token or null if none is present.
 374      * @throws IOException if an error is encountered while parsing the
 375      *         PKCS7 data.
 376      * @throws NoSuchAlgorithmException if an error is encountered while
 377      *         verifying the PKCS7 object.
 378      * @throws SignatureException if an error is encountered while
 379      *         verifying the PKCS7 object.
 380      * @throws CertificateException if an error is encountered while generating
 381      *         the TSA's certpath.
 382      */
 383     private static Timestamp getTimestamp(SignerInfo info,
 384             CertificateFactory certificateFactory) throws IOException,
 385             NoSuchAlgorithmException, SignatureException, CertificateException
 386     {
 387         Timestamp timestamp = null;
 388 
 389         // Extract the signer's unsigned attributes
 390         PKCS9Attributes unsignedAttrs = info.getUnauthenticatedAttributes();
 391         if (unsignedAttrs != null) {
 392             PKCS9Attribute timestampTokenAttr =
 393                     unsignedAttrs.getAttribute("signatureTimestampToken");
 394             if (timestampTokenAttr != null) {
 395                 PKCS7 timestampToken =
 396                         new PKCS7((byte[]) timestampTokenAttr.getValue());
 397                 // Extract the content (an encoded timestamp token info)
 398                 byte[] encodedTimestampTokenInfo =
 399                         timestampToken.getContentInfo().getData();
 400                 // Extract the signer (the Timestamping Authority)
 401                 // while verifying the content
 402                 SignerInfo[] tsa =
 403                         timestampToken.verify(encodedTimestampTokenInfo);
 404                 // Expect only one signer
 405                 ArrayList chain = tsa[0].getCertificateChain(timestampToken);
 406                 CertPath tsaChain = certificateFactory.generateCertPath(
 407                         chain);
 408                 // Create a timestamp token info object
 409                 TimestampToken timestampTokenInfo =
 410                         new TimestampToken(encodedTimestampTokenInfo);
 411                 // Create a timestamp object
 412                 timestamp =
 413                         new Timestamp(timestampTokenInfo.getDate(), tsaChain);
 414             }
 415         }
 416         return timestamp;
 417     }
 418 
 419     public interface InputStreamSource {
 420         InputStream getInputStream() throws IOException;
 421     }
 422 
 423     public void signJarAsBLOB(InputStreamSource input, ZipOutputStream jos)
 424             throws IOException, SignatureException, NoSuchAlgorithmException
 425     {
 426         byte copyBuf[] = new byte[8000];
 427         int n;
 428         ZipEntry e;
 429         ZipInputStream jis = new ZipInputStream(input.getInputStream());
 430 
 431         try {
 432             //calculate signature in the first pass
 433             //consider all entries except directories and old signature file (if any)
 434             boolean hasManifest = false;
 435             boolean hasMetaInf = false;
 436             while ((e = jis.getNextEntry()) != null) {
 437                 if (JarFile.MANIFEST_NAME.equals(
 438                         e.getName().toUpperCase(Locale.ENGLISH))) {
 439                     hasManifest = true;
 440                 }
 441                 if ("META-INF/".equals(
 442                         e.getName().toUpperCase(Locale.ENGLISH))) {
 443                     hasMetaInf = true;
 444                 }
 445                 if (!BLOB_SIGNATURE.equals(e.getName()) &&
 446                     !e.getName().endsWith("/")) {
 447                     readFully(updateWithZipEntry(e.getName(), jis));
 448                 }
 449             }
 450 
 451             byte[] signature = getEncoded();
 452 
 453             //2 pass: save manifest and other entries
 454 
 455             //reopen input file
 456             jis.close();
 457             jis = new ZipInputStream(input.getInputStream());
 458             while ((e = jis.getNextEntry()) != null) {
 459                 String name = e.getName();
 460 
 461                 //special case - jar has no manifest and possibly no META-INF
 462                 // => add META-INF entry once
 463                 //Then output manifest
 464                 if (!hasMetaInf) {
 465                     ZipEntry ze = new ZipEntry("META-INF/");
 466                     ze.setTime(System.currentTimeMillis());
 467                     //NOTE: Do we need CRC32? for this entry?
 468                     jos.putNextEntry(ze);
 469                     jos.closeEntry();
 470 
 471                     hasMetaInf = true;
 472                 }
 473                 if (!hasManifest) {
 474                     addSignatureEntry(signature, jos);
 475                     hasManifest = true;
 476                 }
 477 
 478 
 479                 //copy entry unless it is old signature file
 480                 if (!BLOB_SIGNATURE.equals(name)) {
 481                     // do our own compression
 482                     ZipEntry e2 = new ZipEntry(name);
 483                     e2.setMethod(e.getMethod());
 484                     e2.setTime(e.getTime());
 485                     e2.setComment(e.getComment());
 486                     e2.setExtra(e.getExtra());
 487                     if (e.getMethod() == ZipEntry.STORED) {
 488                         e2.setSize(e.getSize());
 489                         e2.setCrc(e.getCrc());
 490                     }
 491                     jos.putNextEntry(e2);
 492 
 493                     while ((n = jis.read(copyBuf)) != -1) {
 494                         jos.write(copyBuf, 0, n);
 495                     }
 496                     jos.closeEntry();
 497                 }
 498 
 499                 String upperName = name.toUpperCase(Locale.ENGLISH);
 500                 boolean isManifestEntry = JarFile.MANIFEST_NAME.equals(upperName);
 501 
 502                 //output signature after manifest
 503                 if (isManifestEntry) {
 504                     addSignatureEntry(signature, jos);
 505                 }
 506             }
 507         } finally {
 508             jis.close();
 509             jos.close();
 510         }
 511     }
 512 
 513     private void addSignatureEntry(byte[] signature, ZipOutputStream jos) throws IOException {
 514         ZipEntry ze = new ZipEntry(BLOB_SIGNATURE);
 515         ze.setSize(signature.length);
 516         ze.setTime(System.currentTimeMillis());
 517         //NOTE: Do we need a CRC32?
 518         jos.putNextEntry(ze);
 519         jos.write(signature);
 520         jos.closeEntry();
 521 
 522     }
 523 
 524     private static void readFully(InputStream is) throws IOException {
 525         byte buf[] = new byte[10000];
 526         while (is.read(buf) != -1) {}
 527     }
 528 
 529 }