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}. + *

+ * Example: + *

+ * JarSigner signer = new JarSigner.Builder(key, certPath)
+ *         .digestAlgorithm("SHA-1")
+ *         .signatureAlgorithm("SHA1withDSA")
+ *         .build();
+ * try (ZipFile in = new ZipFile(inputFile);
+ *         FileOutputStream out = new FileOutputStream(outputFile)) {
+ *     signer.sign(in, out);
+ * }
+ * 
+ * + * @since 9 + */ +public final class JarSigner { + + /** + * A mutable builder class that can create an immutable {@code JarSigner} + * from various signing-related parameters. + * + * @since 9 + */ + public static class Builder { + + // Signer materials: + final PrivateKey privateKey; + final X509Certificate[] certChain; + + // JarSigner options: + // Support multiple digestalg internally. Can be null, but not empty + String[] digestalg; + String sigalg; + // Precisely should be one provider for each digestalg, maybe later + Provider digestProvider; + Provider sigProvider; + URI tsaUrl; + String signerName; + BiConsumer 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 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: + *

+ * 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 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 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 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; + } + } +}