diff -r 4ebc2e2fb97c -r 71c04702a3d5 src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,1284 @@
+/*
+ * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.security.jarsigner;
+
+import com.sun.jarsigner.ContentSigner;
+import com.sun.jarsigner.ContentSignerParameters;
+import sun.security.tools.PathList;
+import sun.security.tools.jarsigner.TimestampedSigner;
+import sun.security.util.ManifestDigester;
+import sun.security.util.SignatureFileVerifier;
+import sun.security.x509.AlgorithmId;
+
+import java.io.*;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.security.*;
+import java.security.cert.CertPath;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * An immutable utility class to sign a jar file.
+ *
+ * A caller creates a {@code JarSigner.Builder} object, (optionally) sets
+ * some parameters, and calls {@link JarSigner.Builder#build build} to create
+ * a {@code JarSigner} object. This {@code JarSigner} object can then
+ * be used to sign a jar file.
+ *
+ * Unless otherwise stated, calling a method of {@code JarSigner} or
+ * {@code JarSigner.Builder} with a null argument will throw
+ * a {@link NullPointerException}.
+ *
handler;
+
+ // Implementation-specific properties:
+ String tSAPolicyID;
+ String tSADigestAlg;
+ boolean signManifest = true;
+ boolean externalSF = true;
+ String altSignerPath;
+ String altSigner;
+
+ /**
+ * Creates a {@code JarSigner.Builder} object with
+ * a {@link KeyStore.PrivateKeyEntry} object.
+ *
+ * @param entry the {@link KeyStore.PrivateKeyEntry} of the signer.
+ */
+ public Builder(KeyStore.PrivateKeyEntry entry) {
+ this.privateKey = entry.getPrivateKey();
+ try {
+ // called internally, no need to clone
+ Certificate[] certs = entry.getCertificateChain();
+ this.certChain = Arrays.copyOf(certs, certs.length,
+ X509Certificate[].class);
+ } catch (ArrayStoreException ase) {
+ // Wrong type, not X509Certificate. Won't document.
+ throw new IllegalArgumentException(
+ "Entry does not contain X509Certificate");
+ }
+ }
+
+ /**
+ * Creates a {@code JarSigner.Builder} object with a private key and
+ * a certification path.
+ *
+ * @param privateKey the private key of the signer.
+ * @param certPath the certification path of the signer.
+ * @throws IllegalArgumentException if {@code certPath} is empty, or
+ * the {@code privateKey} algorithm does not match the algorithm
+ * of the {@code PublicKey} in the end entity certificate
+ * (the first certificate in {@code certPath}).
+ */
+ public Builder(PrivateKey privateKey, CertPath certPath) {
+ List extends Certificate> certs = certPath.getCertificates();
+ if (certs.isEmpty()) {
+ throw new IllegalArgumentException("certPath cannot be empty");
+ }
+ if (!privateKey.getAlgorithm().equals
+ (certs.get(0).getPublicKey().getAlgorithm())) {
+ throw new IllegalArgumentException
+ ("private key algorithm does not match " +
+ "algorithm of public key in end entity " +
+ "certificate (the 1st in certPath)");
+ }
+ this.privateKey = privateKey;
+ try {
+ this.certChain = certs.toArray(new X509Certificate[certs.size()]);
+ } catch (ArrayStoreException ase) {
+ // Wrong type, not X509Certificate.
+ throw new IllegalArgumentException(
+ "Entry does not contain X509Certificate");
+ }
+ }
+
+ /**
+ * Sets the digest algorithm. If no digest algorithm is specified,
+ * the default algorithm returned by {@link #getDefaultDigestAlgorithm}
+ * will be used.
+ *
+ * @param algorithm the standard name of the algorithm. See
+ * the {@code MessageDigest} section in the
+ * Java Cryptography Architecture Standard Algorithm Name
+ * Documentation for information about standard algorithm names.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws NoSuchAlgorithmException if {@code algorithm} is not available.
+ */
+ public Builder digestAlgorithm(String algorithm) throws NoSuchAlgorithmException {
+ MessageDigest.getInstance(Objects.requireNonNull(algorithm));
+ this.digestalg = new String[]{algorithm};
+ this.digestProvider = null;
+ return this;
+ }
+
+ /**
+ * Sets the digest algorithm from the specified provider.
+ * If no digest algorithm is specified, the default algorithm
+ * returned by {@link #getDefaultDigestAlgorithm} will be used.
+ *
+ * @param algorithm the standard name of the algorithm. See
+ * the {@code MessageDigest} section in the
+ * Java Cryptography Architecture Standard Algorithm Name
+ * Documentation for information about standard algorithm names.
+ * @param provider the provider.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws NoSuchAlgorithmException if {@code algorithm} is not
+ * available in the specified provider.
+ */
+ public Builder digestAlgorithm(String algorithm, Provider provider)
+ throws NoSuchAlgorithmException {
+ MessageDigest.getInstance(
+ Objects.requireNonNull(algorithm),
+ Objects.requireNonNull(provider));
+ this.digestalg = new String[]{algorithm};
+ this.digestProvider = provider;
+ return this;
+ }
+
+ /**
+ * Sets the signature algorithm. If no signature algorithm
+ * is specified, the default signature algorithm returned by
+ * {@link #getDefaultSignatureAlgorithm} for the private key
+ * will be used.
+ *
+ * @param algorithm the standard name of the algorithm. See
+ * the {@code Signature} section in the
+ * Java Cryptography Architecture Standard Algorithm Name
+ * Documentation for information about standard algorithm names.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws NoSuchAlgorithmException if {@code algorithm} is not available.
+ * @throws IllegalArgumentException if {@code algorithm} is not
+ * compatible with the algorithm of the signer's private key.
+ */
+ public Builder signatureAlgorithm(String algorithm)
+ throws NoSuchAlgorithmException {
+ // Check availability
+ Signature.getInstance(Objects.requireNonNull(algorithm));
+ AlgorithmId.checkKeyAndSigAlgMatch(
+ privateKey.getAlgorithm(), algorithm);
+ this.sigalg = algorithm;
+ this.sigProvider = null;
+ return this;
+ }
+
+ /**
+ * Sets the signature algorithm from the specified provider. If no
+ * signature algorithm is specified, the default signature algorithm
+ * returned by {@link #getDefaultSignatureAlgorithm} for the private
+ * key will be used.
+ *
+ * @param algorithm the standard name of the algorithm. See
+ * the {@code Signature} section in the
+ * Java Cryptography Architecture Standard Algorithm Name
+ * Documentation for information about standard algorithm names.
+ * @param provider the provider.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws NoSuchAlgorithmException if {@code algorithm} is not
+ * available in the specified provider.
+ * @throws IllegalArgumentException if {@code algorithm} is not
+ * compatible with the algorithm of the signer's private key.
+ */
+ public Builder signatureAlgorithm(String algorithm, Provider provider)
+ throws NoSuchAlgorithmException {
+ // Check availability
+ Signature.getInstance(
+ Objects.requireNonNull(algorithm),
+ Objects.requireNonNull(provider));
+ AlgorithmId.checkKeyAndSigAlgMatch(
+ privateKey.getAlgorithm(), algorithm);
+ this.sigalg = algorithm;
+ this.sigProvider = provider;
+ return this;
+ }
+
+ /**
+ * Sets the URI of the Time Stamping Authority (TSA).
+ *
+ * @param uri the URI.
+ * @return the {@code JarSigner.Builder} itself.
+ */
+ public Builder tsa(URI uri) {
+ this.tsaUrl = Objects.requireNonNull(uri);
+ return this;
+ }
+
+ /**
+ * Sets the signer name. The name will be used as the base name for
+ * the signature files. All lowercase characters will be converted to
+ * uppercase for signature file names. If a signer name is not
+ * specified, the string "SIGNER" will be used.
+ *
+ * @param name the signer name.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws IllegalArgumentException if {@code name} is empty or has
+ * a size bigger than 8, or it contains characters not from the
+ * set "a-zA-Z0-9_-".
+ */
+ public Builder signerName(String name) {
+ if (name.isEmpty() || name.length() > 8) {
+ throw new IllegalArgumentException("Name too long");
+ }
+
+ name = name.toUpperCase(Locale.ENGLISH);
+
+ for (int j = 0; j < name.length(); j++) {
+ char c = name.charAt(j);
+ if (!
+ ((c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ (c == '-') ||
+ (c == '_'))) {
+ throw new IllegalArgumentException(
+ "Invalid characters in name");
+ }
+ }
+ this.signerName = name;
+ return this;
+ }
+
+ /**
+ * Sets en event handler that will be triggered when a {@link JarEntry}
+ * is to be added, signed, or updated during the signing process.
+ *
+ * The handler can be used to display signing progress. The first
+ * argument of the handler can be "adding", "signing", or "updating",
+ * and the second argument is the name of the {@link JarEntry}
+ * being processed.
+ *
+ * @param handler the event handler.
+ * @return the {@code JarSigner.Builder} itself.
+ */
+ public Builder eventHandler(BiConsumer handler) {
+ this.handler = Objects.requireNonNull(handler);
+ return this;
+ }
+
+ /**
+ * Sets an additional implementation-specific property indicated by
+ * the specified key.
+ *
+ * @implNote This implementation supports the following properties:
+ *
+ * - "tsaDigestAlg": algorithm of digest data in the timestamping
+ * request. The default value is the same as the result of
+ * {@link #getDefaultDigestAlgorithm}.
+ *
- "tsaPolicyId": TSAPolicyID for Timestamping Authority.
+ * No default value.
+ *
- "internalsf": "true" if the .SF file is included inside the
+ * signature block, "false" otherwise. Default "false".
+ *
- "sectionsonly": "true" if the .SF file only contains the hash
+ * value for each section of the manifest and not for the whole
+ * manifest, "false" otherwise. Default "false".
+ *
+ * All property names are case-insensitive.
+ *
+ * @param key the name of the property.
+ * @param value the value of the property.
+ * @return the {@code JarSigner.Builder} itself.
+ * @throws UnsupportedOperationException if the key is not supported
+ * by this implementation.
+ * @throws IllegalArgumentException if the value is not accepted as
+ * a legal value for this key.
+ */
+ public Builder setProperty(String key, String value) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(value);
+ switch (key.toLowerCase(Locale.US)) {
+ case "tsadigestalg":
+ try {
+ MessageDigest.getInstance(value);
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new IllegalArgumentException(
+ "Invalid tsadigestalg", nsae);
+ }
+ this.tSADigestAlg = value;
+ break;
+ case "tsapolicyid":
+ this.tSAPolicyID = value;
+ break;
+ case "internalsf":
+ switch (value) {
+ case "true":
+ externalSF = false;
+ break;
+ case "false":
+ externalSF = true;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid internalsf value");
+ }
+ break;
+ case "sectionsonly":
+ switch (value) {
+ case "true":
+ signManifest = false;
+ break;
+ case "false":
+ signManifest = true;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid signManifest value");
+ }
+ break;
+ case "altsignerpath":
+ altSignerPath = value;
+ break;
+ case "altsigner":
+ altSigner = value;
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported key " + key);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the default digest algorithm.
+ *
+ * @implNote This implementation returns "SHA-256". The value may
+ * change in the future.
+ *
+ * @return the default digest algorithm.
+ */
+ public static String getDefaultDigestAlgorithm() {
+ return "SHA-256";
+ }
+
+ /**
+ * Gets the default signature algorithm for a private key.
+ * For example, SHA256withRSA for a 2048-bit RSA key, and
+ * SHA384withECDSA for a 384-bit EC key.
+ *
+ * @implNote This implementation makes use of comparable strengths
+ * as defined in Tables 2 and 3 of NIST SP 800-57 Part 1-Rev.4.
+ * Specifically, if a DSA or RSA key with a key size greater than 7680
+ * bits, or an EC key with a key size greater than or equal to 512 bits,
+ * SHA-512 will be used as the hash function for the signature.
+ * If a DSA or RSA key has a key size greater than 3072 bits, or an
+ * EC key has a key size greater than or equal to 384 bits, SHA-384 will
+ * be used. Otherwise, SHA-256 will be used. The value may
+ * change in the future.
+ *
+ * @param key the private key.
+ * @return the default signature algorithm. Returns null if a default
+ * signature algorithm cannot be found. In this case,
+ * {@link #signatureAlgorithm} must be called to specify a
+ * signature algorithm. Otherwise, the {@link #build} method
+ * will throw an {@link IllegalArgumentException}.
+ */
+ public static String getDefaultSignatureAlgorithm(PrivateKey key) {
+ return AlgorithmId.getDefaultSigAlgForKey(Objects.requireNonNull(key));
+ }
+
+ /**
+ * Builds a {@code JarSigner} object from the parameters set by the
+ * setter methods.
+ *
+ * This method does not modify internal state of this {@code Builder}
+ * object and can be called multiple times to generate multiple
+ * {@code JarSigner} objects. After this method is called, calling
+ * any method on this {@code Builder} will have no effect on
+ * the newly built {@code JarSigner} object.
+ *
+ * @return the {@code JarSigner} object.
+ * @throws IllegalArgumentException if a signature algorithm is not
+ * set and cannot be derived from the private key using the
+ * {@link #getDefaultSignatureAlgorithm} method.
+ */
+ public JarSigner build() {
+ return new JarSigner(this);
+ }
+ }
+
+ private static final String META_INF = "META-INF/";
+
+ // All fields in Builder are duplicated here as final. Those not
+ // provided but has a default value will be filled with default value.
+
+ // Precisely, a final array field can still be modified if only
+ // reference is copied, no clone is done because we are concerned about
+ // casual change instead of malicious attack.
+
+ // Signer materials:
+ private final PrivateKey privateKey;
+ private final X509Certificate[] certChain;
+
+ // JarSigner options:
+ private final String[] digestalg;
+ private final String sigalg;
+ private final Provider digestProvider;
+ private final Provider sigProvider;
+ private final URI tsaUrl;
+ private final String signerName;
+ private final BiConsumer handler;
+
+ // Implementation-specific properties:
+ private final String tSAPolicyID;
+ private final String tSADigestAlg;
+ private final boolean signManifest; // "sign" the whole manifest
+ private final boolean externalSF; // leave the .SF out of the PKCS7 block
+ private final String altSignerPath;
+ private final String altSigner;
+
+ private JarSigner(JarSigner.Builder builder) {
+
+ this.privateKey = builder.privateKey;
+ this.certChain = builder.certChain;
+ if (builder.digestalg != null) {
+ // No need to clone because builder only accepts one alg now
+ this.digestalg = builder.digestalg;
+ } else {
+ this.digestalg = new String[] {
+ Builder.getDefaultDigestAlgorithm() };
+ }
+ this.digestProvider = builder.digestProvider;
+ if (builder.sigalg != null) {
+ this.sigalg = builder.sigalg;
+ } else {
+ this.sigalg = JarSigner.Builder
+ .getDefaultSignatureAlgorithm(privateKey);
+ if (this.sigalg == null) {
+ throw new IllegalArgumentException(
+ "No signature alg for " + privateKey.getAlgorithm());
+ }
+ }
+ this.sigProvider = builder.sigProvider;
+ this.tsaUrl = builder.tsaUrl;
+
+ if (builder.signerName == null) {
+ this.signerName = "SIGNER";
+ } else {
+ this.signerName = builder.signerName;
+ }
+ this.handler = builder.handler;
+
+ if (builder.tSADigestAlg != null) {
+ this.tSADigestAlg = builder.tSADigestAlg;
+ } else {
+ this.tSADigestAlg = Builder.getDefaultDigestAlgorithm();
+ }
+ this.tSAPolicyID = builder.tSAPolicyID;
+ this.signManifest = builder.signManifest;
+ this.externalSF = builder.externalSF;
+ this.altSigner = builder.altSigner;
+ this.altSignerPath = builder.altSignerPath;
+ }
+
+ /**
+ * Signs a file into an {@link OutputStream}. This method will not close
+ * {@code file} or {@code os}.
+ *
+ * @param file the file to sign.
+ * @param os the output stream.
+ * @throws JarSignerException if the signing fails.
+ */
+ public void sign(ZipFile file, OutputStream os) {
+ try {
+ sign0(Objects.requireNonNull(file),
+ Objects.requireNonNull(os));
+ } catch (SocketTimeoutException | CertificateException e) {
+ // CertificateException is thrown when the received cert from TSA
+ // has no id-kp-timeStamping in its Extended Key Usages extension.
+ throw new JarSignerException("Error applying timestamp", e);
+ } catch (IOException ioe) {
+ throw new JarSignerException("I/O error", ioe);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new JarSignerException("Error in signer materials", e);
+ } catch (SignatureException se) {
+ throw new JarSignerException("Error creating signature", se);
+ }
+ }
+
+ /**
+ * Returns the digest algorithm for this {@code JarSigner}.
+ *
+ * The return value is never null.
+ *
+ * @return the digest algorithm.
+ */
+ public String getDigestAlgorithm() {
+ return digestalg[0];
+ }
+
+ /**
+ * Returns the signature algorithm for this {@code JarSigner}.
+ *
+ * The return value is never null.
+ *
+ * @return the signature algorithm.
+ */
+ public String getSignatureAlgorithm() {
+ return sigalg;
+ }
+
+ /**
+ * Returns the URI of the Time Stamping Authority (TSA).
+ *
+ * @return the URI of the TSA.
+ */
+ public URI getTsa() {
+ return tsaUrl;
+ }
+
+ /**
+ * Returns the signer name of this {@code JarSigner}.
+ *
+ * The return value is never null.
+ *
+ * @return the signer name.
+ */
+ public String getSignerName() {
+ return signerName;
+ }
+
+ /**
+ * Returns the value of an additional implementation-specific property
+ * indicated by the specified key. If a property is not set but has a
+ * default value, the default value will be returned.
+ *
+ * @implNote See {@link JarSigner.Builder#setProperty} for a list of
+ * properties this implementation supports. All property names are
+ * case-insensitive.
+ *
+ * @param key the name of the property.
+ * @return the value for the property.
+ * @throws UnsupportedOperationException if the key is not supported
+ * by this implementation.
+ */
+ public String getProperty(String key) {
+ Objects.requireNonNull(key);
+ switch (key.toLowerCase(Locale.US)) {
+ case "tsadigestalg":
+ return tSADigestAlg;
+ case "tsapolicyid":
+ return tSAPolicyID;
+ case "internalsf":
+ return Boolean.toString(!externalSF);
+ case "sectionsonly":
+ return Boolean.toString(!signManifest);
+ case "altsignerpath":
+ return altSignerPath;
+ case "altsigner":
+ return altSigner;
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported key " + key);
+ }
+ }
+
+ private void sign0(ZipFile zipFile, OutputStream os)
+ throws IOException, CertificateException, NoSuchAlgorithmException,
+ SignatureException, InvalidKeyException {
+ MessageDigest[] digests;
+ try {
+ digests = new MessageDigest[digestalg.length];
+ for (int i = 0; i < digestalg.length; i++) {
+ if (digestProvider == null) {
+ digests[i] = MessageDigest.getInstance(digestalg[i]);
+ } else {
+ digests[i] = MessageDigest.getInstance(
+ digestalg[i], digestProvider);
+ }
+ }
+ } catch (NoSuchAlgorithmException asae) {
+ // Should not happen. User provided alg were checked, and default
+ // alg should always be available.
+ throw new AssertionError(asae);
+ }
+
+ PrintStream ps = new PrintStream(os);
+ ZipOutputStream zos = new ZipOutputStream(ps);
+
+ Manifest manifest = new Manifest();
+ Map mfEntries = manifest.getEntries();
+
+ // The Attributes of manifest before updating
+ Attributes oldAttr = null;
+
+ boolean mfModified = false;
+ boolean mfCreated = false;
+ byte[] mfRawBytes = null;
+
+ // Check if manifest exists
+ ZipEntry mfFile;
+ if ((mfFile = getManifestFile(zipFile)) != null) {
+ // Manifest exists. Read its raw bytes.
+ mfRawBytes = zipFile.getInputStream(mfFile).readAllBytes();
+ manifest.read(new ByteArrayInputStream(mfRawBytes));
+ oldAttr = (Attributes) (manifest.getMainAttributes().clone());
+ } else {
+ // Create new manifest
+ Attributes mattr = manifest.getMainAttributes();
+ mattr.putValue(Attributes.Name.MANIFEST_VERSION.toString(),
+ "1.0");
+ String javaVendor = System.getProperty("java.vendor");
+ String jdkVersion = System.getProperty("java.version");
+ mattr.putValue("Created-By", jdkVersion + " (" + javaVendor
+ + ")");
+ mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
+ mfCreated = true;
+ }
+
+ /*
+ * For each entry in jar
+ * (except for signature-related META-INF entries),
+ * do the following:
+ *
+ * - if entry is not contained in manifest, add it to manifest;
+ * - if entry is contained in manifest, calculate its hash and
+ * compare it with the one in the manifest; if they are
+ * different, replace the hash in the manifest with the newly
+ * generated one. (This may invalidate existing signatures!)
+ */
+ Vector mfFiles = new Vector<>();
+
+ boolean wasSigned = false;
+
+ for (Enumeration extends ZipEntry> enum_ = zipFile.entries();
+ enum_.hasMoreElements(); ) {
+ ZipEntry ze = enum_.nextElement();
+
+ if (ze.getName().startsWith(META_INF)) {
+ // Store META-INF files in vector, so they can be written
+ // out first
+ mfFiles.addElement(ze);
+
+ if (SignatureFileVerifier.isBlockOrSF(
+ ze.getName().toUpperCase(Locale.ENGLISH))) {
+ wasSigned = true;
+ }
+
+ if (SignatureFileVerifier.isSigningRelated(ze.getName())) {
+ // ignore signature-related and manifest files
+ continue;
+ }
+ }
+
+ if (manifest.getAttributes(ze.getName()) != null) {
+ // jar entry is contained in manifest, check and
+ // possibly update its digest attributes
+ if (updateDigests(ze, zipFile, digests,
+ manifest)) {
+ mfModified = true;
+ }
+ } else if (!ze.isDirectory()) {
+ // Add entry to manifest
+ Attributes attrs = getDigestAttributes(ze, zipFile, digests);
+ mfEntries.put(ze.getName(), attrs);
+ mfModified = true;
+ }
+ }
+
+ // Recalculate the manifest raw bytes if necessary
+ if (mfModified) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ manifest.write(baos);
+ if (wasSigned) {
+ byte[] newBytes = baos.toByteArray();
+ if (mfRawBytes != null
+ && oldAttr.equals(manifest.getMainAttributes())) {
+
+ /*
+ * Note:
+ *
+ * The Attributes object is based on HashMap and can handle
+ * continuation columns. Therefore, even if the contents are
+ * not changed (in a Map view), the bytes that it write()
+ * may be different from the original bytes that it read()
+ * from. Since the signature on the main attributes is based
+ * on raw bytes, we must retain the exact bytes.
+ */
+
+ int newPos = findHeaderEnd(newBytes);
+ int oldPos = findHeaderEnd(mfRawBytes);
+
+ if (newPos == oldPos) {
+ System.arraycopy(mfRawBytes, 0, newBytes, 0, oldPos);
+ } else {
+ // cat oldHead newTail > newBytes
+ byte[] lastBytes = new byte[oldPos +
+ newBytes.length - newPos];
+ System.arraycopy(mfRawBytes, 0, lastBytes, 0, oldPos);
+ System.arraycopy(newBytes, newPos, lastBytes, oldPos,
+ newBytes.length - newPos);
+ newBytes = lastBytes;
+ }
+ }
+ mfRawBytes = newBytes;
+ } else {
+ mfRawBytes = baos.toByteArray();
+ }
+ }
+
+ // Write out the manifest
+ if (mfModified) {
+ // manifest file has new length
+ mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
+ }
+ if (handler != null) {
+ if (mfCreated) {
+ handler.accept("adding", mfFile.getName());
+ } else if (mfModified) {
+ handler.accept("updating", mfFile.getName());
+ }
+ }
+
+ zos.putNextEntry(mfFile);
+ zos.write(mfRawBytes);
+
+ // Calculate SignatureFile (".SF") and SignatureBlockFile
+ ManifestDigester manDig = new ManifestDigester(mfRawBytes);
+ SignatureFile sf = new SignatureFile(digests, manifest, manDig,
+ signerName, signManifest);
+
+ byte[] block;
+
+ Signature signer;
+ if (sigProvider == null ) {
+ signer = Signature.getInstance(sigalg);
+ } else {
+ signer = Signature.getInstance(sigalg, sigProvider);
+ }
+ signer.initSign(privateKey);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ sf.write(baos);
+
+ byte[] content = baos.toByteArray();
+
+ signer.update(content);
+ byte[] signature = signer.sign();
+
+ @SuppressWarnings("deprecation")
+ ContentSigner signingMechanism = null;
+ if (altSigner != null) {
+ signingMechanism = loadSigningMechanism(altSigner,
+ altSignerPath);
+ }
+
+ @SuppressWarnings("deprecation")
+ ContentSignerParameters params =
+ new JarSignerParameters(null, tsaUrl, tSAPolicyID,
+ tSADigestAlg, signature,
+ signer.getAlgorithm(), certChain, content, zipFile);
+ block = sf.generateBlock(params, externalSF, signingMechanism);
+
+ String sfFilename = sf.getMetaName();
+ String bkFilename = sf.getBlockName(privateKey);
+
+ ZipEntry sfFile = new ZipEntry(sfFilename);
+ ZipEntry bkFile = new ZipEntry(bkFilename);
+
+ long time = System.currentTimeMillis();
+ sfFile.setTime(time);
+ bkFile.setTime(time);
+
+ // signature file
+ zos.putNextEntry(sfFile);
+ sf.write(zos);
+
+ if (handler != null) {
+ if (zipFile.getEntry(sfFilename) != null) {
+ handler.accept("updating", sfFilename);
+ } else {
+ handler.accept("adding", sfFilename);
+ }
+ }
+
+ // signature block file
+ zos.putNextEntry(bkFile);
+ zos.write(block);
+
+ if (handler != null) {
+ if (zipFile.getEntry(bkFilename) != null) {
+ handler.accept("updating", bkFilename);
+ } else {
+ handler.accept("adding", bkFilename);
+ }
+ }
+
+ // Write out all other META-INF files that we stored in the
+ // vector
+ for (int i = 0; i < mfFiles.size(); i++) {
+ ZipEntry ze = mfFiles.elementAt(i);
+ if (!ze.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)
+ && !ze.getName().equalsIgnoreCase(sfFilename)
+ && !ze.getName().equalsIgnoreCase(bkFilename)) {
+ if (handler != null) {
+ if (manifest.getAttributes(ze.getName()) != null) {
+ handler.accept("signing", ze.getName());
+ } else if (!ze.isDirectory()) {
+ handler.accept("adding", ze.getName());
+ }
+ }
+ writeEntry(zipFile, zos, ze);
+ }
+ }
+
+ // Write out all other files
+ for (Enumeration extends ZipEntry> enum_ = zipFile.entries();
+ enum_.hasMoreElements(); ) {
+ ZipEntry ze = enum_.nextElement();
+
+ if (!ze.getName().startsWith(META_INF)) {
+ if (handler != null) {
+ if (manifest.getAttributes(ze.getName()) != null) {
+ handler.accept("signing", ze.getName());
+ } else {
+ handler.accept("adding", ze.getName());
+ }
+ }
+ writeEntry(zipFile, zos, ze);
+ }
+ }
+ zipFile.close();
+ zos.close();
+ }
+
+ private void writeEntry(ZipFile zf, ZipOutputStream os, ZipEntry ze)
+ throws IOException {
+ ZipEntry ze2 = new ZipEntry(ze.getName());
+ ze2.setMethod(ze.getMethod());
+ ze2.setTime(ze.getTime());
+ ze2.setComment(ze.getComment());
+ ze2.setExtra(ze.getExtra());
+ if (ze.getMethod() == ZipEntry.STORED) {
+ ze2.setSize(ze.getSize());
+ ze2.setCrc(ze.getCrc());
+ }
+ os.putNextEntry(ze2);
+ writeBytes(zf, ze, os);
+ }
+
+ private void writeBytes
+ (ZipFile zf, ZipEntry ze, ZipOutputStream os) throws IOException {
+ try (InputStream is = zf.getInputStream(ze)) {
+ is.transferTo(os);
+ }
+ }
+
+ private boolean updateDigests(ZipEntry ze, ZipFile zf,
+ MessageDigest[] digests,
+ Manifest mf) throws IOException {
+ boolean update = false;
+
+ Attributes attrs = mf.getAttributes(ze.getName());
+ String[] base64Digests = getDigests(ze, zf, digests);
+
+ for (int i = 0; i < digests.length; i++) {
+ // The entry name to be written into attrs
+ String name = null;
+ try {
+ // Find if the digest already exists. An algorithm could have
+ // different names. For example, last time it was SHA, and this
+ // time it's SHA-1.
+ AlgorithmId aid = AlgorithmId.get(digests[i].getAlgorithm());
+ for (Object key : attrs.keySet()) {
+ if (key instanceof Attributes.Name) {
+ String n = key.toString();
+ if (n.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST")) {
+ String tmp = n.substring(0, n.length() - 7);
+ if (AlgorithmId.get(tmp).equals(aid)) {
+ name = n;
+ break;
+ }
+ }
+ }
+ }
+ } catch (NoSuchAlgorithmException nsae) {
+ // Ignored. Writing new digest entry.
+ }
+
+ if (name == null) {
+ name = digests[i].getAlgorithm() + "-Digest";
+ attrs.putValue(name, base64Digests[i]);
+ update = true;
+ } else {
+ // compare digests, and replace the one in the manifest
+ // if they are different
+ String mfDigest = attrs.getValue(name);
+ if (!mfDigest.equalsIgnoreCase(base64Digests[i])) {
+ attrs.putValue(name, base64Digests[i]);
+ update = true;
+ }
+ }
+ }
+ return update;
+ }
+
+ private Attributes getDigestAttributes(
+ ZipEntry ze, ZipFile zf, MessageDigest[] digests)
+ throws IOException {
+
+ String[] base64Digests = getDigests(ze, zf, digests);
+ Attributes attrs = new Attributes();
+
+ for (int i = 0; i < digests.length; i++) {
+ attrs.putValue(digests[i].getAlgorithm() + "-Digest",
+ base64Digests[i]);
+ }
+ return attrs;
+ }
+
+ /*
+ * Returns manifest entry from given jar file, or null if given jar file
+ * does not have a manifest entry.
+ */
+ private ZipEntry getManifestFile(ZipFile zf) {
+ ZipEntry ze = zf.getEntry(JarFile.MANIFEST_NAME);
+ if (ze == null) {
+ // Check all entries for matching name
+ Enumeration extends ZipEntry> enum_ = zf.entries();
+ while (enum_.hasMoreElements() && ze == null) {
+ ze = enum_.nextElement();
+ if (!JarFile.MANIFEST_NAME.equalsIgnoreCase
+ (ze.getName())) {
+ ze = null;
+ }
+ }
+ }
+ return ze;
+ }
+
+ private String[] getDigests(
+ ZipEntry ze, ZipFile zf, MessageDigest[] digests)
+ throws IOException {
+
+ int n, i;
+ try (InputStream is = zf.getInputStream(ze)) {
+ long left = ze.getSize();
+ byte[] buffer = new byte[8192];
+ while ((left > 0)
+ && (n = is.read(buffer, 0, buffer.length)) != -1) {
+ for (i = 0; i < digests.length; i++) {
+ digests[i].update(buffer, 0, n);
+ }
+ left -= n;
+ }
+ }
+
+ // complete the digests
+ String[] base64Digests = new String[digests.length];
+ for (i = 0; i < digests.length; i++) {
+ base64Digests[i] = Base64.getEncoder()
+ .encodeToString(digests[i].digest());
+ }
+ return base64Digests;
+ }
+
+ @SuppressWarnings("fallthrough")
+ private int findHeaderEnd(byte[] bs) {
+ // Initial state true to deal with empty header
+ boolean newline = true; // just met a newline
+ int len = bs.length;
+ for (int i = 0; i < len; i++) {
+ switch (bs[i]) {
+ case '\r':
+ if (i < len - 1 && bs[i + 1] == '\n') i++;
+ // fallthrough
+ case '\n':
+ if (newline) return i + 1; //+1 to get length
+ newline = true;
+ break;
+ default:
+ newline = false;
+ }
+ }
+ // If header end is not found, it means the MANIFEST.MF has only
+ // the main attributes section and it does not end with 2 newlines.
+ // Returns the whole length so that it can be completely replaced.
+ return len;
+ }
+
+ /*
+ * Try to load the specified signing mechanism.
+ * The URL class loader is used.
+ */
+ @SuppressWarnings("deprecation")
+ private ContentSigner loadSigningMechanism(String signerClassName,
+ String signerClassPath) {
+
+ // construct class loader
+ String cpString; // make sure env.class.path defaults to dot
+
+ // do prepends to get correct ordering
+ cpString = PathList.appendPath(
+ System.getProperty("env.class.path"), null);
+ cpString = PathList.appendPath(
+ System.getProperty("java.class.path"), cpString);
+ cpString = PathList.appendPath(signerClassPath, cpString);
+ URL[] urls = PathList.pathToURLs(cpString);
+ ClassLoader appClassLoader = new URLClassLoader(urls);
+
+ try {
+ // attempt to find signer
+ Class> signerClass = appClassLoader.loadClass(signerClassName);
+ Object signer = signerClass.newInstance();
+ return (ContentSigner) signer;
+ } catch (ClassNotFoundException|InstantiationException|
+ IllegalAccessException|ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Invalid altSigner or altSignerPath", e);
+ }
+ }
+
+ static class SignatureFile {
+
+ /**
+ * SignatureFile
+ */
+ Manifest sf;
+
+ /**
+ * .SF base name
+ */
+ String baseName;
+
+ public SignatureFile(MessageDigest digests[],
+ Manifest mf,
+ ManifestDigester md,
+ String baseName,
+ boolean signManifest) {
+
+ this.baseName = baseName;
+
+ String version = System.getProperty("java.version");
+ String javaVendor = System.getProperty("java.vendor");
+
+ sf = new Manifest();
+ Attributes mattr = sf.getMainAttributes();
+
+ mattr.putValue(Attributes.Name.SIGNATURE_VERSION.toString(), "1.0");
+ mattr.putValue("Created-By", version + " (" + javaVendor + ")");
+
+ if (signManifest) {
+ for (MessageDigest digest: digests) {
+ mattr.putValue(digest.getAlgorithm() + "-Digest-Manifest",
+ Base64.getEncoder().encodeToString(
+ md.manifestDigest(digest)));
+ }
+ }
+
+ // create digest of the manifest main attributes
+ ManifestDigester.Entry mde =
+ md.get(ManifestDigester.MF_MAIN_ATTRS, false);
+ if (mde != null) {
+ for (MessageDigest digest: digests) {
+ mattr.putValue(digest.getAlgorithm() +
+ "-Digest-" + ManifestDigester.MF_MAIN_ATTRS,
+ Base64.getEncoder().encodeToString(
+ mde.digest(digest)));
+ }
+ } else {
+ throw new IllegalStateException
+ ("ManifestDigester failed to create " +
+ "Manifest-Main-Attribute entry");
+ }
+
+ // go through the manifest entries and create the digests
+ Map entries = sf.getEntries();
+ for (String name: mf.getEntries().keySet()) {
+ mde = md.get(name, false);
+ if (mde != null) {
+ Attributes attr = new Attributes();
+ for (MessageDigest digest: digests) {
+ attr.putValue(digest.getAlgorithm() + "-Digest",
+ Base64.getEncoder().encodeToString(
+ mde.digest(digest)));
+ }
+ entries.put(name, attr);
+ }
+ }
+ }
+
+ // Write .SF file
+ public void write(OutputStream out) throws IOException {
+ sf.write(out);
+ }
+
+ // get .SF file name
+ public String getMetaName() {
+ return "META-INF/" + baseName + ".SF";
+ }
+
+ // get .DSA (or .DSA, .EC) file name
+ public String getBlockName(PrivateKey privateKey) {
+ String keyAlgorithm = privateKey.getAlgorithm();
+ return "META-INF/" + baseName + "." + keyAlgorithm;
+ }
+
+ // Generates the PKCS#7 content of block file
+ @SuppressWarnings("deprecation")
+ public byte[] generateBlock(ContentSignerParameters params,
+ boolean externalSF,
+ ContentSigner signingMechanism)
+ throws NoSuchAlgorithmException,
+ IOException, CertificateException {
+
+ if (signingMechanism == null) {
+ signingMechanism = new TimestampedSigner();
+ }
+ return signingMechanism.generateSignedData(
+ params,
+ externalSF,
+ params.getTimestampingAuthority() != null
+ || params.getTimestampingAuthorityCertificate() != null);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ class JarSignerParameters implements ContentSignerParameters {
+
+ private String[] args;
+ private URI tsa;
+ private byte[] signature;
+ private String signatureAlgorithm;
+ private X509Certificate[] signerCertificateChain;
+ private byte[] content;
+ private ZipFile source;
+ private String tSAPolicyID;
+ private String tSADigestAlg;
+
+ JarSignerParameters(String[] args, URI tsa,
+ String tSAPolicyID, String tSADigestAlg,
+ byte[] signature, String signatureAlgorithm,
+ X509Certificate[] signerCertificateChain,
+ byte[] content, ZipFile source) {
+
+ Objects.requireNonNull(signature);
+ Objects.requireNonNull(signatureAlgorithm);
+ Objects.requireNonNull(signerCertificateChain);
+
+ this.args = args;
+ this.tsa = tsa;
+ this.tSAPolicyID = tSAPolicyID;
+ this.tSADigestAlg = tSADigestAlg;
+ this.signature = signature;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signerCertificateChain = signerCertificateChain;
+ this.content = content;
+ this.source = source;
+ }
+
+ public String[] getCommandLine() {
+ return args;
+ }
+
+ public URI getTimestampingAuthority() {
+ return tsa;
+ }
+
+ public X509Certificate getTimestampingAuthorityCertificate() {
+ // We don't use this param. Always provide tsaURI.
+ return null;
+ }
+
+ public String getTSAPolicyID() {
+ return tSAPolicyID;
+ }
+
+ public String getTSADigestAlg() {
+ return tSADigestAlg;
+ }
+
+ public byte[] getSignature() {
+ return signature;
+ }
+
+ public String getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public X509Certificate[] getSignerCertificateChain() {
+ return signerCertificateChain;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+
+ public ZipFile getSource() {
+ return source;
+ }
+ }
+}