/* * Copyright (c) 2015, 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.3. * 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; } } }