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