From a7d92d59f9f19a2aefdd063c3c4b654ec8e87c61 Mon Sep 17 00:00:00 2001 From: Weijun Wang Date: Fri, 20 Nov 2015 08:34:04 +0800 Subject: [PATCH] 8056174: New APIs for jar signing Reviewed-by: mullan --- .../sun/security/x509/AlgorithmId.java | 65 + .../jdk/security/jarsigner/JarSigner.java | 1286 +++++++++++++++++ .../jarsigner/JarSignerException.java | 56 + .../sun/security/tools/jarsigner/Main.java | 1016 ++----------- jdk/test/jdk/security/jarsigner/Function.java | 182 +++ jdk/test/jdk/security/jarsigner/Spec.java | 251 ++++ .../sun/security/tools/jarsigner/Options.java | 137 ++ 7 files changed, 2065 insertions(+), 928 deletions(-) create mode 100644 jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java create mode 100644 jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSignerException.java create mode 100644 jdk/test/jdk/security/jarsigner/Function.java create mode 100644 jdk/test/jdk/security/jarsigner/Spec.java create mode 100644 jdk/test/sun/security/tools/jarsigner/Options.java diff --git a/jdk/src/java.base/share/classes/sun/security/x509/AlgorithmId.java b/jdk/src/java.base/share/classes/sun/security/x509/AlgorithmId.java index 530dc167c00..f84371da80e 100644 --- a/jdk/src/java.base/share/classes/sun/security/x509/AlgorithmId.java +++ b/jdk/src/java.base/share/classes/sun/security/x509/AlgorithmId.java @@ -977,4 +977,69 @@ public class AlgorithmId implements Serializable, DerEncoder { } return null; } + + /** + * Checks if a signature algorithm matches a key algorithm, i.e. a + * signature can be initialized with a key. + * + * @param kAlg must not be null + * @param sAlg must not be null + * @throws IllegalArgumentException if they do not match + */ + public static void checkKeyAndSigAlgMatch(String kAlg, String sAlg) { + String sAlgUp = sAlg.toUpperCase(Locale.US); + if ((sAlgUp.endsWith("WITHRSA") && !kAlg.equalsIgnoreCase("RSA")) || + (sAlgUp.endsWith("WITHECDSA") && !kAlg.equalsIgnoreCase("EC")) || + (sAlgUp.endsWith("WITHDSA") && !kAlg.equalsIgnoreCase("DSA"))) { + throw new IllegalArgumentException( + "key algorithm not compatible with signature algorithm"); + } + } + + /** + * Returns the default signature algorithm for a private key. The digest + * part might evolve with time. Remember to update the spec of + * {@link jdk.security.jarsigner.JarSigner.Builder#getDefaultSignatureAlgorithm(PrivateKey)} + * if updated. + * + * @param k cannot be null + * @return the default alg, might be null if unsupported + */ + public static String getDefaultSigAlgForKey(PrivateKey k) { + switch (k.getAlgorithm().toUpperCase()) { + case "EC": + return ecStrength(KeyUtil.getKeySize(k)) + + "withECDSA"; + case "DSA": + return ifcFfcStrength(KeyUtil.getKeySize(k)) + + "withDSA"; + case "RSA": + return ifcFfcStrength(KeyUtil.getKeySize(k)) + + "withRSA"; + default: + return null; + } + } + + // Values from SP800-57 part 1 rev 3 tables 2 and three + private static String ecStrength (int bitLength) { + if (bitLength >= 512) { // 256 bits of strength + return "SHA512"; + } else if (bitLength >= 384) { // 192 bits of strength + return "SHA384"; + } else { // 128 bits of strength and less + return "SHA256"; + } + } + + // same values for RSA and DSA + private static String ifcFfcStrength (int bitLength) { + if (bitLength > 7680) { // 256 bits + return "SHA512"; + } else if (bitLength > 3072) { // 192 bits + return "SHA384"; + } else { // 128 bits and less + return "SHA256"; + } + } } diff --git a/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java b/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java new file mode 100644 index 00000000000..b9ce4e30225 --- /dev/null +++ b/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSigner.java @@ -0,0 +1,1286 @@ +/* + * 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 1.9 + */ +@jdk.Exported +public final class JarSigner { + + /** + * A mutable builder class that can create an immutable {@code JarSigner} + * from various signing-related parameters. + * + * @since 1.9 + */ + @jdk.Exported + 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; + } + } +} diff --git a/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSignerException.java b/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSignerException.java new file mode 100644 index 00000000000..a6c73a68af4 --- /dev/null +++ b/jdk/src/jdk.jartool/share/classes/jdk/security/jarsigner/JarSignerException.java @@ -0,0 +1,56 @@ +/* + * 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; + +/** + * This exception is thrown when {@link JarSigner#sign} fails. + * + * @since 1.9 + */ +@jdk.Exported +public class JarSignerException extends RuntimeException { + + private static final long serialVersionUID = -4732217075689309530L; + + /** + * Constructs a new {@code JarSignerException} with the specified detail + * message and cause. + *

+ * Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this {@code JarSignerException}'s detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + */ + public JarSignerException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/jdk/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java b/jdk/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java index 1417800400e..9d2932e8880 100644 --- a/jdk/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java +++ b/jdk/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java @@ -29,22 +29,16 @@ import java.io.*; import java.util.*; import java.util.zip.*; import java.util.jar.*; -import java.math.BigInteger; import java.net.URI; -import java.net.URISyntaxException; import java.text.Collator; import java.text.MessageFormat; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.*; -import java.lang.reflect.Constructor; -import com.sun.jarsigner.ContentSigner; -import com.sun.jarsigner.ContentSignerParameters; import java.net.SocketTimeoutException; import java.net.URL; -import java.net.URLClassLoader; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; import java.security.cert.CertificateExpiredException; @@ -53,11 +47,12 @@ import java.security.cert.CertificateNotYetValidException; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.util.Map.Entry; + +import jdk.security.jarsigner.JarSigner; +import jdk.security.jarsigner.JarSignerException; import sun.security.tools.KeyStoreUtil; -import sun.security.tools.PathList; import sun.security.x509.*; import sun.security.util.*; -import java.util.Base64; /** @@ -88,10 +83,6 @@ public class Main { collator.setStrength(Collator.PRIMARY); } - private static final String META_INF = "META-INF/"; - - private static final Class[] PARAM_STRING = { String.class }; - private static final String NONE = "NONE"; private static final String P11KEYSTORE = "PKCS11"; @@ -133,13 +124,13 @@ public class Main { char[] keypass; // private key password String sigfile; // name of .SF file String sigalg; // name of signature algorithm - String digestalg = "SHA-256"; // name of digest algorithm + String digestalg; // name of digest algorithm String signedjar; // output filename String tsaUrl; // location of the Timestamping Authority String tsaAlias; // alias for the Timestamping Authority's certificate String altCertChain; // file to read alternative cert chain from String tSAPolicyID; - String tSADigestAlg = "SHA-256"; + String tSADigestAlg; boolean verify = false; // verify the jar String verbose = null; // verbose output when signing/verifying boolean showcerts = false; // show certs when verifying @@ -149,9 +140,6 @@ public class Main { boolean strict = false; // treat warnings as error // read zip entry raw bytes - private ByteArrayOutputStream baos = new ByteArrayOutputStream(2048); - private byte[] buffer = new byte[8192]; - private ContentSigner signingMechanism = null; private String altSignerClass = null; private String altSignerClasspath = null; private ZipFile zipFile = null; @@ -216,6 +204,9 @@ public class Main { if ((keystore != null) || (storepass != null)) { System.out.println(rb.getString("jarsigner.error.") + e.getMessage()); + if (debug) { + e.printStackTrace(); + } System.exit(1); } } @@ -229,12 +220,7 @@ public class Main { loadKeyStore(keystore, true); getAliasInfo(alias); - // load the alternative signing mechanism - if (altSignerClass != null) { - signingMechanism = loadSigningMechanism(altSignerClass, - altSignerClasspath); - } - signJar(jarfile, alias, args); + signJar(jarfile, alias); } } catch (Exception e) { System.out.println(rb.getString("jarsigner.error.") + e); @@ -626,8 +612,7 @@ public class Main { InputStream is = null; try { is = jf.getInputStream(je); - int n; - while ((n = is.read(buffer, 0, buffer.length)) != -1) { + while (is.read(buffer, 0, buffer.length) != -1) { // we just read. this will throw a SecurityException // if a signature/digest check fails. } @@ -1035,7 +1020,6 @@ public class Main { return cacheForInKS.get(signer); } - boolean found = false; int result = 0; List certs = signer.getSignerCertPath().getCertificates(); for (Certificate c : certs) { @@ -1058,7 +1042,6 @@ public class Main { } if (alias != null) { storeHash.put(c, "(" + alias + ")"); - found = true; result |= IN_KEYSTORE; } } @@ -1090,7 +1073,7 @@ public class Main { return output; } - void signJar(String jarName, String alias, String[] args) + void signJar(String jarName, String alias) throws Exception { boolean aliasUsed = false; X509Certificate tsaCert = null; @@ -1110,17 +1093,17 @@ public class Main { for (int j = 0; j < sigfile.length(); j++) { char c = sigfile.charAt(j); if (! - ((c>= 'A' && c<= 'Z') || - (c>= '0' && c<= '9') || - (c == '-') || - (c == '_'))) { + ((c>= 'A' && c<= 'Z') || + (c>= '0' && c<= '9') || + (c == '-') || + (c == '_'))) { if (aliasUsed) { // convert illegal characters from the alias to be _'s c = '_'; } else { - throw new - RuntimeException(rb.getString - ("signature.filename.must.consist.of.the.following.characters.A.Z.0.9.or.")); + throw new + RuntimeException(rb.getString + ("signature.filename.must.consist.of.the.following.characters.A.Z.0.9.or.")); } } tmpSigFile.append(c); @@ -1149,275 +1132,88 @@ public class Main { error(rb.getString("unable.to.create.")+tmpJarName, ioe); } - PrintStream ps = new PrintStream(fos); - ZipOutputStream zos = new ZipOutputStream(ps); + CertPath cp = CertificateFactory.getInstance("X.509") + .generateCertPath(Arrays.asList(certChain)); + JarSigner.Builder builder = new JarSigner.Builder(privateKey, cp); - /* First guess at what they might be - we don't xclude RSA ones. */ - String sfFilename = (META_INF + sigfile + ".SF").toUpperCase(Locale.ENGLISH); - String bkFilename = (META_INF + sigfile + ".DSA").toUpperCase(Locale.ENGLISH); + if (verbose != null) { + builder.eventHandler((action, file) -> { + System.out.println(rb.getString("." + action + ".") + file); + }); + } - Manifest manifest = new Manifest(); - Map mfEntries = manifest.getEntries(); + if (digestalg != null) { + builder.digestAlgorithm(digestalg); + } + if (sigalg != null) { + builder.signatureAlgorithm(sigalg); + } - // The Attributes of manifest before updating - Attributes oldAttr = null; + URI tsaURI = null; - boolean mfModified = false; - boolean mfCreated = false; - byte[] mfRawBytes = null; + if (tsaUrl != null) { + tsaURI = new URI(tsaUrl); + } else if (tsaAlias != null) { + tsaCert = getTsaCert(tsaAlias); + tsaURI = TimestampedSigner.getTimestampingURI(tsaCert); + } - try { - MessageDigest digests[] = { MessageDigest.getInstance(digestalg) }; - - // Check if manifest exists - ZipEntry mfFile; - if ((mfFile = getManifestFile(zipFile)) != null) { - // Manifest exists. Read its raw bytes. - mfRawBytes = getBytes(zipFile, mfFile); - 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 (signatureRelated(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) == true) { - 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 (tsaURI != null) { if (verbose != null) { - if (mfCreated) { - System.out.println(rb.getString(".adding.") + - mfFile.getName()); - } else if (mfModified) { - System.out.println(rb.getString(".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, - sigfile, signManifest); - - if (tsaAlias != null) { - tsaCert = getTsaCert(tsaAlias); - } - - if (tsaUrl == null && tsaCert == null) { - noTimestamp = true; - } - - SignatureFile.Block block = null; - - try { - block = - sf.generateBlock(privateKey, sigalg, certChain, - externalSF, tsaUrl, tsaCert, tSAPolicyID, tSADigestAlg, - signingMechanism, args, zipFile); - } catch (SocketTimeoutException e) { - // Provide a helpful message when TSA is beyond a firewall - error(rb.getString("unable.to.sign.jar.") + - rb.getString("no.response.from.the.Timestamping.Authority.") + - "\n -J-Dhttp.proxyHost=" + - "\n -J-Dhttp.proxyPort=\n" + - rb.getString("or") + - "\n -J-Dhttps.proxyHost= " + - "\n -J-Dhttps.proxyPort= ", e); - } - - sfFilename = sf.getMetaName(); - bkFilename = block.getMetaName(); - - 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 (verbose != null) { - if (zipFile.getEntry(sfFilename) != null) { - System.out.println(rb.getString(".updating.") + - sfFilename); - } else { - System.out.println(rb.getString(".adding.") + - sfFilename); - } - } - - if (verbose != null) { - if (tsaUrl != null || tsaCert != null) { - System.out.println( + System.out.println( rb.getString("requesting.a.signature.timestamp")); - } if (tsaUrl != null) { System.out.println(rb.getString("TSA.location.") + tsaUrl); - } - if (tsaCert != null) { - URI tsaURI = TimestampedSigner.getTimestampingURI(tsaCert); - if (tsaURI != null) { - System.out.println(rb.getString("TSA.location.") + - tsaURI); - } + } else if (tsaCert != null) { System.out.println(rb.getString("TSA.certificate.") + - printCert("", tsaCert, false, null, false)); - } - if (signingMechanism != null) { - System.out.println( - rb.getString("using.an.alternative.signing.mechanism")); + printCert("", tsaCert, false, null, false)); } } + builder.tsa(tsaURI); + if (tSADigestAlg != null) { + builder.setProperty("tsaDigestAlg", tSADigestAlg); + } - // signature block file - zos.putNextEntry(bkFile); - block.write(zos); + if (tSAPolicyID != null) { + builder.setProperty("tsaPolicyId", tSAPolicyID); + } + } else { + noTimestamp = true; + } + + if (altSignerClass != null) { + builder.setProperty("altSigner", altSignerClass); if (verbose != null) { - if (zipFile.getEntry(bkFilename) != null) { - System.out.println(rb.getString(".updating.") + - bkFilename); - } else { - System.out.println(rb.getString(".adding.") + - bkFilename); - } + System.out.println( + rb.getString("using.an.alternative.signing.mechanism")); } + } - // Write out all other META-INF files that we stored in the - // vector - for (int i=0; i" + + "\n -J-Dhttp.proxyPort=\n" + + rb.getString("or") + + "\n -J-Dhttps.proxyHost= " + + "\n -J-Dhttps.proxyPort= ", e); + } else { + error(rb.getString("unable.to.sign.jar.")+e.getCause(), e.getCause()); } - - // Write out all other files - for (Enumeration enum_=zipFile.entries(); - enum_.hasMoreElements();) { - ZipEntry ze = enum_.nextElement(); - - if (!ze.getName().startsWith(META_INF)) { - if (verbose != null) { - if (manifest.getAttributes(ze.getName()) != null) - System.out.println(rb.getString(".signing.") + - ze.getName()); - else - System.out.println(rb.getString(".adding.") + - ze.getName()); - } - writeEntry(zipFile, zos, ze); - } - } - } catch(IOException ioe) { - error(rb.getString("unable.to.sign.jar.")+ioe, ioe); } finally { // close the resouces if (zipFile != null) { @@ -1425,8 +1221,8 @@ public class Main { zipFile = null; } - if (zos != null) { - zos.close(); + if (fos != null) { + fos.close(); } } @@ -1526,35 +1322,6 @@ public class Main { // } } - /** - * Find the length of header inside bs. The header is a multiple (>=0) - * lines of attributes plus an empty line. The empty line is included - * in the header. - */ - @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 0) && (n = is.read(buffer, 0, buffer.length)) != -1) { - os.write(buffer, 0, n); - left -= n; - } - } finally { - if (is != null) { - is.close(); - } - } - } - void loadKeyStore(String keyStoreName, boolean prompt) { if (!nullStream && keyStoreName == null) { @@ -1958,15 +1686,13 @@ public class Main { } } - void error(String message) - { + void error(String message) { System.out.println(rb.getString("jarsigner.")+message); System.exit(1); } - void error(String message, Exception e) - { + void error(String message, Throwable e) { System.out.println(rb.getString("jarsigner.")+message); if (debug) { e.printStackTrace(); @@ -1990,8 +1716,7 @@ public class Main { } } - char[] getPass(String prompt) - { + char[] getPass(String prompt) { System.err.print(prompt); System.err.flush(); try { @@ -2008,569 +1733,4 @@ public class Main { // this shouldn't happen return null; } - - /* - * Reads all the bytes for a given zip entry. - */ - private synchronized byte[] getBytes(ZipFile zf, - ZipEntry ze) throws IOException { - int n; - - InputStream is = null; - try { - is = zf.getInputStream(ze); - baos.reset(); - long left = ze.getSize(); - - while((left > 0) && (n = is.read(buffer, 0, buffer.length)) != -1) { - baos.write(buffer, 0, n); - left -= n; - } - } finally { - if (is != null) { - is.close(); - } - } - - return baos.toByteArray(); - } - - /* - * 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; - } - - /* - * Computes the digests of a zip entry, and returns them as an array - * of base64-encoded strings. - */ - private synchronized String[] getDigests(ZipEntry ze, ZipFile zf, - MessageDigest[] digests) - throws IOException { - - int n, i; - InputStream is = null; - try { - is = zf.getInputStream(ze); - long left = ze.getSize(); - while((left > 0) - && (n = is.read(buffer, 0, buffer.length)) != -1) { - for (i=0; i signerClass = appClassLoader.loadClass(signerClassName); - - // Check that it implements ContentSigner - Object signer = signerClass.newInstance(); - if (!(signer instanceof ContentSigner)) { - MessageFormat form = new MessageFormat( - rb.getString("signerClass.is.not.a.signing.mechanism")); - Object[] source = {signerClass.getName()}; - throw new IllegalArgumentException(form.format(source)); - } - return (ContentSigner)signer; - } -} - -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) { - // sign the whole manifest - for (int i=0; i < digests.length; i++) { - mattr.putValue(digests[i].getAlgorithm()+"-Digest-Manifest", - Base64.getEncoder().encodeToString(md.manifestDigest(digests[i]))); - } - } - - // create digest of the manifest main attributes - ManifestDigester.Entry mde = - md.get(ManifestDigester.MF_MAIN_ATTRS, false); - if (mde != null) { - for (int i=0; i < digests.length; i++) { - mattr.putValue(digests[i].getAlgorithm() + - "-Digest-" + ManifestDigester.MF_MAIN_ATTRS, - Base64.getEncoder().encodeToString(mde.digest(digests[i]))); - } - } 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(); - Iterator> mit = - mf.getEntries().entrySet().iterator(); - while(mit.hasNext()) { - Map.Entry e = mit.next(); - String name = e.getKey(); - mde = md.get(name, false); - if (mde != null) { - Attributes attr = new Attributes(); - for (int i=0; i < digests.length; i++) { - attr.putValue(digests[i].getAlgorithm()+"-Digest", - Base64.getEncoder().encodeToString(mde.digest(digests[i]))); - } - entries.put(name, attr); - } - } - } - - /** - * Writes the SignatureFile to the specified OutputStream. - * - * @param out the output stream - * @exception IOException if an I/O error has occurred - */ - - public void write(OutputStream out) throws IOException - { - sf.write(out); - } - - /** - * get .SF file name - */ - public String getMetaName() - { - return "META-INF/"+ baseName + ".SF"; - } - - /** - * get base file name - */ - public String getBaseName() - { - return baseName; - } - - /* - * Generate a signed data block. - * If a URL or a certificate (containing a URL) for a Timestamping - * Authority is supplied then a signature timestamp is generated and - * inserted into the signed data block. - * - * @param sigalg signature algorithm to use, or null to use default - * @param tsaUrl The location of the Timestamping Authority. If null - * then no timestamp is requested. - * @param tsaCert The certificate for the Timestamping Authority. If null - * then no timestamp is requested. - * @param signingMechanism The signing mechanism to use. - * @param args The command-line arguments to jarsigner. - * @param zipFile The original source Zip file. - */ - @SuppressWarnings("deprecation") - public Block generateBlock(PrivateKey privateKey, - String sigalg, - X509Certificate[] certChain, - boolean externalSF, String tsaUrl, - X509Certificate tsaCert, - String tSAPolicyID, - String tSADigestAlg, - ContentSigner signingMechanism, - String[] args, ZipFile zipFile) - throws NoSuchAlgorithmException, InvalidKeyException, IOException, - SignatureException, CertificateException - { - return new Block(this, privateKey, sigalg, certChain, externalSF, - tsaUrl, tsaCert, tSAPolicyID, tSADigestAlg, signingMechanism, args, zipFile); - } - - - public static class Block { - - private byte[] block; - private String blockFileName; - - /* - * Construct a new signature block. - */ - @SuppressWarnings("deprecation") - Block(SignatureFile sfg, PrivateKey privateKey, String sigalg, - X509Certificate[] certChain, boolean externalSF, String tsaUrl, - X509Certificate tsaCert, String tSAPolicyID, String tSADigestAlg, - ContentSigner signingMechanism, String[] args, ZipFile zipFile) - throws NoSuchAlgorithmException, InvalidKeyException, IOException, - SignatureException, CertificateException { - - Principal issuerName = certChain[0].getIssuerDN(); - if (!(issuerName instanceof X500Name)) { - // must extract the original encoded form of DN for subsequent - // name comparison checks (converting to a String and back to - // an encoded DN could cause the types of String attribute - // values to be changed) - X509CertInfo tbsCert = new - X509CertInfo(certChain[0].getTBSCertificate()); - issuerName = (Principal) - tbsCert.get(X509CertInfo.ISSUER + "." + - X509CertInfo.DN_NAME); - } - BigInteger serial = certChain[0].getSerialNumber(); - - String signatureAlgorithm; - String keyAlgorithm = privateKey.getAlgorithm(); - /* - * If no signature algorithm was specified, we choose a - * default that is compatible with the private key algorithm. - */ - if (sigalg == null) { - - if (keyAlgorithm.equalsIgnoreCase("DSA")) - signatureAlgorithm = "SHA256withDSA"; - else if (keyAlgorithm.equalsIgnoreCase("RSA")) - signatureAlgorithm = "SHA256withRSA"; - else if (keyAlgorithm.equalsIgnoreCase("EC")) - signatureAlgorithm = "SHA256withECDSA"; - else - throw new RuntimeException("private key is not a DSA or " - + "RSA key"); - } else { - signatureAlgorithm = sigalg; - } - - // check common invalid key/signature algorithm combinations - String sigAlgUpperCase = signatureAlgorithm.toUpperCase(Locale.ENGLISH); - if ((sigAlgUpperCase.endsWith("WITHRSA") && - !keyAlgorithm.equalsIgnoreCase("RSA")) || - (sigAlgUpperCase.endsWith("WITHECDSA") && - !keyAlgorithm.equalsIgnoreCase("EC")) || - (sigAlgUpperCase.endsWith("WITHDSA") && - !keyAlgorithm.equalsIgnoreCase("DSA"))) { - throw new SignatureException - ("private key algorithm is not compatible with signature algorithm"); - } - - blockFileName = "META-INF/"+sfg.getBaseName()+"."+keyAlgorithm; - - AlgorithmId sigAlg = AlgorithmId.get(signatureAlgorithm); - AlgorithmId digEncrAlg = AlgorithmId.get(keyAlgorithm); - - Signature sig = Signature.getInstance(signatureAlgorithm); - sig.initSign(privateKey); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - sfg.write(baos); - - byte[] content = baos.toByteArray(); - - sig.update(content); - byte[] signature = sig.sign(); - - // Timestamp the signature and generate the signature block file - if (signingMechanism == null) { - signingMechanism = new TimestampedSigner(); - } - URI tsaUri = null; - try { - if (tsaUrl != null) { - tsaUri = new URI(tsaUrl); - } - } catch (URISyntaxException e) { - throw new IOException(e); - } - - // Assemble parameters for the signing mechanism - ContentSignerParameters params = - new JarSignerParameters(args, tsaUri, tsaCert, tSAPolicyID, - tSADigestAlg, signature, - signatureAlgorithm, certChain, content, zipFile); - - // Generate the signature block - block = signingMechanism.generateSignedData( - params, externalSF, (tsaUrl != null || tsaCert != null)); - } - - /* - * get block file name. - */ - public String getMetaName() - { - return blockFileName; - } - - /** - * Writes the block file to the specified OutputStream. - * - * @param out the output stream - * @exception IOException if an I/O error has occurred - */ - - public void write(OutputStream out) throws IOException - { - out.write(block); - } - } -} - - -/* - * This object encapsulates the parameters used to perform content signing. - */ -@SuppressWarnings("deprecation") -class JarSignerParameters implements ContentSignerParameters { - - private String[] args; - private URI tsa; - private X509Certificate tsaCertificate; - private byte[] signature; - private String signatureAlgorithm; - private X509Certificate[] signerCertificateChain; - private byte[] content; - private ZipFile source; - private String tSAPolicyID; - private String tSADigestAlg; - - /** - * Create a new object. - */ - JarSignerParameters(String[] args, URI tsa, X509Certificate tsaCertificate, - String tSAPolicyID, String tSADigestAlg, - byte[] signature, String signatureAlgorithm, - X509Certificate[] signerCertificateChain, byte[] content, - ZipFile source) { - - if (signature == null || signatureAlgorithm == null || - signerCertificateChain == null || tSADigestAlg == null) { - throw new NullPointerException(); - } - this.args = args; - this.tsa = tsa; - this.tsaCertificate = tsaCertificate; - this.tSAPolicyID = tSAPolicyID; - this.tSADigestAlg = tSADigestAlg; - this.signature = signature; - this.signatureAlgorithm = signatureAlgorithm; - this.signerCertificateChain = signerCertificateChain; - this.content = content; - this.source = source; - } - - /** - * Retrieves the command-line arguments. - * - * @return The command-line arguments. May be null. - */ - public String[] getCommandLine() { - return args; - } - - /** - * Retrieves the identifier for a Timestamping Authority (TSA). - * - * @return The TSA identifier. May be null. - */ - public URI getTimestampingAuthority() { - return tsa; - } - - /** - * Retrieves the certificate for a Timestamping Authority (TSA). - * - * @return The TSA certificate. May be null. - */ - public X509Certificate getTimestampingAuthorityCertificate() { - return tsaCertificate; - } - - public String getTSAPolicyID() { - return tSAPolicyID; - } - - public String getTSADigestAlg() { - return tSADigestAlg; - } - - /** - * Retrieves the signature. - * - * @return The non-null signature bytes. - */ - public byte[] getSignature() { - return signature; - } - - /** - * Retrieves the name of the signature algorithm. - * - * @return The non-null string name of the signature algorithm. - */ - public String getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Retrieves the signer's X.509 certificate chain. - * - * @return The non-null array of X.509 public-key certificates. - */ - public X509Certificate[] getSignerCertificateChain() { - return signerCertificateChain; - } - - /** - * Retrieves the content that was signed. - * - * @return The content bytes. May be null. - */ - public byte[] getContent() { - return content; - } - - /** - * Retrieves the original source ZIP file before it was signed. - * - * @return The original ZIP file. May be null. - */ - public ZipFile getSource() { - return source; - } } diff --git a/jdk/test/jdk/security/jarsigner/Function.java b/jdk/test/jdk/security/jarsigner/Function.java new file mode 100644 index 00000000000..eead632be87 --- /dev/null +++ b/jdk/test/jdk/security/jarsigner/Function.java @@ -0,0 +1,182 @@ +/* + * 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. + * + * 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. + */ + +/* + * @test + * @bug 8056174 + * @summary test the functions of JarSigner API + * @modules java.base/sun.security.tools.keytool + * jdk.jartool + */ + +import jdk.security.jarsigner.JarSigner; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +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; + +public class Function { + public static void main(String[] args) throws Exception { + + try (FileOutputStream fout =new FileOutputStream("src.zip"); + ZipOutputStream zout = new ZipOutputStream(fout)) { + zout.putNextEntry(new ZipEntry("x")); + zout.write(new byte[10]); + zout.closeEntry(); + } + + sun.security.tools.keytool.Main.main( + ("-storetype jks -keystore ks -storepass changeit" + + " -keypass changeit -dname" + + " CN=RSA -alias r -genkeypair -keyalg rsa").split(" ")); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream("ks"), "changeit".toCharArray()); + PrivateKey key = (PrivateKey)ks.getKey("r", "changeit".toCharArray()); + Certificate cert = ks.getCertificate("r"); + JarSigner.Builder jsb = new JarSigner.Builder(key, + CertificateFactory.getInstance("X.509").generateCertPath( + Collections.singletonList(cert))); + + jsb.digestAlgorithm("SHA1"); + jsb.signatureAlgorithm("SHA1withRSA"); + + AtomicInteger counter = new AtomicInteger(0); + StringBuilder sb = new StringBuilder(); + jsb.eventHandler( + (a, f)->{ + counter.incrementAndGet(); + sb.append(a).append(' ').append(f).append('\n'); + }); + + OutputStream blackHole = new OutputStream() { + @Override + public void write(int b) throws IOException { } + }; + + try (ZipFile src = new ZipFile("src.zip")) { + jsb.build().sign(src, blackHole); + } + + if (counter.get() != 4) { + throw new Exception("Event number is " + counter.get() + + ":\n" + sb.toString()); + } + + // Provider test. + Provider p = new MyProvider(); + jsb.digestAlgorithm("Five", p); + jsb.signatureAlgorithm("SHA1WithRSA", p); + try (ZipFile src = new ZipFile("src.zip"); + FileOutputStream out = new FileOutputStream("out.jar")) { + jsb.build().sign(src, out); + } + + try (JarFile signed = new JarFile("out.jar")) { + Manifest man = signed.getManifest(); + assertTrue(man.getAttributes("x").getValue("Five-Digest").equals("FAKE")); + + Manifest sf = new Manifest(signed.getInputStream( + signed.getJarEntry("META-INF/SIGNER.SF"))); + assertTrue(sf.getMainAttributes().getValue("Five-Digest-Manifest") + .equals("FAKE")); + assertTrue(sf.getAttributes("x").getValue("Five-Digest").equals("FAKE")); + + try (InputStream sig = signed.getInputStream( + signed.getJarEntry("META-INF/SIGNER.RSA"))) { + byte[] data = sig.readAllBytes(); + assertTrue(Arrays.equals( + Arrays.copyOfRange(data, data.length-8, data.length), + "FAKEFAKE".getBytes())); + } + } + } + + private static void assertTrue(boolean v) { + if (!v) { + throw new AssertionError(); + } + } + + public static class MyProvider extends Provider { + MyProvider() { + super("MY", 1.0d, null); + put("MessageDigest.Five", Five.class.getName()); + put("Signature.SHA1WithRSA", SHA1WithRSA.class.getName()); + } + } + + // "Five" is a MessageDigest always returns the same value + public static class Five extends MessageDigest { + static final byte[] dig = {0x14, 0x02, (byte)0x84}; //base64 -> FAKE + public Five() { super("Five"); } + protected void engineUpdate(byte input) { } + protected void engineUpdate(byte[] input, int offset, int len) { } + protected byte[] engineDigest() { return dig; } + protected void engineReset() { } + } + + // This fake "SHA1withRSA" is a Signature always returns the same value. + // An existing name must be used otherwise PKCS7 does not which OID to use. + public static class SHA1WithRSA extends Signature { + static final byte[] sig = "FAKEFAKE".getBytes(); + public SHA1WithRSA() { super("SHA1WithRSA"); } + protected void engineInitVerify(PublicKey publicKey) + throws InvalidKeyException { } + protected void engineInitSign(PrivateKey privateKey) + throws InvalidKeyException { } + protected void engineUpdate(byte b) throws SignatureException { } + protected void engineUpdate(byte[] b, int off, int len) + throws SignatureException { } + protected byte[] engineSign() throws SignatureException { return sig; } + protected boolean engineVerify(byte[] sigBytes) + throws SignatureException { + return Arrays.equals(sigBytes, sig); + } + protected void engineSetParameter(String param, Object value) + throws InvalidParameterException { } + protected Object engineGetParameter(String param) + throws InvalidParameterException { return null; } + } +} diff --git a/jdk/test/jdk/security/jarsigner/Spec.java b/jdk/test/jdk/security/jarsigner/Spec.java new file mode 100644 index 00000000000..a4853bf08c2 --- /dev/null +++ b/jdk/test/jdk/security/jarsigner/Spec.java @@ -0,0 +1,251 @@ +/* + * 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. + * + * 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. + */ + +/** + * @test + * @bug 8056174 + * @summary Make sure JarSigner impl conforms to spec + * @library /lib/testlibrary + * @modules java.base/sun.security.tools.keytool + * java.base/sun.security.provider.certpath + * jdk.jartool + */ + +import com.sun.jarsigner.ContentSigner; +import com.sun.jarsigner.ContentSignerParameters; +import jdk.security.jarsigner.JarSigner; +import jdk.testlibrary.JarUtils; +import sun.security.provider.certpath.X509CertPath; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.CertPath; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Collections; +import java.util.function.BiConsumer; + +public class Spec { + + public static void main(String[] args) throws Exception { + + // Prepares raw file + Files.write(Paths.get("a"), "a".getBytes()); + + // Pack + JarUtils.createJar("a.jar", "a"); + + // Prepare a keystore + sun.security.tools.keytool.Main.main( + ("-keystore ks -storepass changeit -keypass changeit -dname" + + " CN=RSA -alias r -genkeypair -keyalg rsa").split(" ")); + sun.security.tools.keytool.Main.main( + ("-keystore ks -storepass changeit -keypass changeit -dname" + + " CN=DSA -alias d -genkeypair -keyalg dsa").split(" ")); + + char[] pass = "changeit".toCharArray(); + + KeyStore ks = KeyStore.getInstance( + new File("ks"), pass); + PrivateKey pkr = (PrivateKey)ks.getKey("r", pass); + PrivateKey pkd = (PrivateKey)ks.getKey("d", pass); + CertPath cp = CertificateFactory.getInstance("X.509") + .generateCertPath(Arrays.asList(ks.getCertificateChain("r"))); + + Provider sun = Security.getProvider("SUN"); + + // throws + npe(()->new JarSigner.Builder(null)); + npe(()->new JarSigner.Builder(null, cp)); + iae(()->new JarSigner.Builder( + pkr, new X509CertPath(Collections.emptyList()))); + iae(()->new JarSigner.Builder(pkd, cp)); // unmatched certs alg + + JarSigner.Builder b1 = new JarSigner.Builder(pkr, cp); + + npe(()->b1.digestAlgorithm(null)); + nsae(()->b1.digestAlgorithm("HAHA")); + b1.digestAlgorithm("SHA-256"); + + npe(()->b1.digestAlgorithm("SHA-256", null)); + npe(()->b1.digestAlgorithm(null, sun)); + nsae(()->b1.digestAlgorithm("HAHA", sun)); + b1.digestAlgorithm("SHA-256", sun); + + npe(()->b1.signatureAlgorithm(null)); + nsae(()->b1.signatureAlgorithm("HAHAwithHEHE")); + iae(()->b1.signatureAlgorithm("SHA256withECDSA")); + + npe(()->b1.signatureAlgorithm(null, sun)); + npe(()->b1.signatureAlgorithm("SHA256withRSA", null)); + nsae(()->b1.signatureAlgorithm("HAHAwithHEHE", sun)); + iae(()->b1.signatureAlgorithm("SHA256withDSA", sun)); + + npe(()->b1.tsa(null)); + + npe(()->b1.signerName(null)); + iae(()->b1.signerName("")); + iae(()->b1.signerName("123456789")); + iae(()->b1.signerName("a+b")); + + npe(()->b1.setProperty(null, "")); + uoe(()->b1.setProperty("what", "")); + npe(()->b1.setProperty("tsadigestalg", null)); + iae(()->b1.setProperty("tsadigestalg", "HAHA")); + npe(()->b1.setProperty("tsapolicyid", null)); + npe(()->b1.setProperty("internalsf", null)); + iae(()->b1.setProperty("internalsf", "Hello")); + npe(()->b1.setProperty("sectionsonly", null)); + iae(()->b1.setProperty("sectionsonly", "OK")); + npe(()->b1.setProperty("altsigner", null)); + npe(()->b1.eventHandler(null)); + + // default values + JarSigner.Builder b2 = new JarSigner.Builder(pkr, cp); + JarSigner js2 = b2.build(); + + assertTrue(js2.getDigestAlgorithm().equals( + JarSigner.Builder.getDefaultDigestAlgorithm())); + assertTrue(js2.getSignatureAlgorithm().equals( + JarSigner.Builder.getDefaultSignatureAlgorithm(pkr))); + assertTrue(js2.getTsa() == null); + assertTrue(js2.getSignerName().equals("SIGNER")); + assertTrue(js2.getProperty("tsadigestalg").equals( + JarSigner.Builder.getDefaultDigestAlgorithm())); + assertTrue(js2.getProperty("tsapolicyid") == null); + assertTrue(js2.getProperty("internalsf").equals("false")); + assertTrue(js2.getProperty("sectionsonly").equals("false")); + assertTrue(js2.getProperty("altsigner") == null); + uoe(()->js2.getProperty("invalid")); + + // default values + BiConsumer myeh = (a,s)->{}; + URI tsa = new URI("https://tsa.com"); + + JarSigner.Builder b3 = new JarSigner.Builder(pkr, cp) + .digestAlgorithm("SHA-1") + .signatureAlgorithm("SHA1withRSA") + .signerName("Duke") + .tsa(tsa) + .setProperty("tsadigestalg", "SHA-512") + .setProperty("tsapolicyid", "1.2.3.4") + .setProperty("internalsf", "true") + .setProperty("sectionsonly", "true") + .setProperty("altsigner", "MyContentSigner") + .eventHandler(myeh); + JarSigner js3 = b3.build(); + + assertTrue(js3.getDigestAlgorithm().equals("SHA-1")); + assertTrue(js3.getSignatureAlgorithm().equals("SHA1withRSA")); + assertTrue(js3.getTsa().equals(tsa)); + assertTrue(js3.getSignerName().equals("DUKE")); + assertTrue(js3.getProperty("tsadigestalg").equals("SHA-512")); + assertTrue(js3.getProperty("tsapolicyid").equals("1.2.3.4")); + assertTrue(js3.getProperty("internalsf").equals("true")); + assertTrue(js3.getProperty("sectionsonly").equals("true")); + assertTrue(js3.getProperty("altsigner").equals("MyContentSigner")); + assertTrue(js3.getProperty("altsignerpath") == null); + + assertTrue(JarSigner.Builder.getDefaultDigestAlgorithm().equals("SHA-256")); + + // Calculating large DSA and RSA keys are too slow. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(1024); + assertTrue(JarSigner.Builder + .getDefaultSignatureAlgorithm(kpg.generateKeyPair().getPrivate()) + .equals("SHA256withRSA")); + + kpg = KeyPairGenerator.getInstance("DSA"); + kpg.initialize(1024); + assertTrue(JarSigner.Builder + .getDefaultSignatureAlgorithm(kpg.generateKeyPair().getPrivate()) + .equals("SHA256withDSA")); + + kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(192); + assertTrue(JarSigner.Builder + .getDefaultSignatureAlgorithm(kpg.generateKeyPair().getPrivate()) + .equals("SHA256withECDSA")); + kpg.initialize(384); + assertTrue(JarSigner.Builder + .getDefaultSignatureAlgorithm(kpg.generateKeyPair().getPrivate()) + .equals("SHA384withECDSA")); + kpg.initialize(571); + assertTrue(JarSigner.Builder + .getDefaultSignatureAlgorithm(kpg.generateKeyPair().getPrivate()) + .equals("SHA512withECDSA")); + } + + interface RunnableWithException { + void run() throws Exception; + } + + static void uoe(RunnableWithException r) throws Exception { + checkException(r, UnsupportedOperationException.class); + } + + static void nsae(RunnableWithException r) throws Exception { + checkException(r, NoSuchAlgorithmException.class); + } + + static void npe(RunnableWithException r) throws Exception { + checkException(r, NullPointerException.class); + } + + static void iae(RunnableWithException r) throws Exception { + checkException(r, IllegalArgumentException.class); + } + + static void checkException(RunnableWithException r, Class ex) + throws Exception { + try { + r.run(); + } catch (Exception e) { + if (ex.isAssignableFrom(e.getClass())) { + return; + } + throw e; + } + throw new Exception("No exception thrown"); + } + + static void assertTrue(boolean x) throws Exception { + if (!x) throw new Exception("Not true"); + } + + static class MyContentSigner extends ContentSigner { + @Override + public byte[] generateSignedData( + ContentSignerParameters parameters, + boolean omitContent, + boolean applyTimestamp) throws NoSuchAlgorithmException, + CertificateException, IOException { + return new byte[0]; + } + } +} diff --git a/jdk/test/sun/security/tools/jarsigner/Options.java b/jdk/test/sun/security/tools/jarsigner/Options.java new file mode 100644 index 00000000000..e70903d06e3 --- /dev/null +++ b/jdk/test/sun/security/tools/jarsigner/Options.java @@ -0,0 +1,137 @@ +/* + * 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. + * + * 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. + */ + +/** + * @test + * @bug 8056174 + * @summary Make sure the jarsigner tool still works after it's modified to + * be based on JarSigner API + * @library /lib/testlibrary + * @modules java.base/sun.security.tools.keytool + * jdk.jartool/sun.security.tools.jarsigner + * java.base/sun.security.pkcs + * java.base/sun.security.x509 + */ + +import com.sun.jarsigner.ContentSigner; +import com.sun.jarsigner.ContentSignerParameters; +import jdk.testlibrary.JarUtils; +import sun.security.pkcs.PKCS7; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public class Options { + + public static void main(String[] args) throws Exception { + + // Prepares raw file + Files.write(Paths.get("a"), "a".getBytes()); + + // Pack + JarUtils.createJar("a.jar", "a"); + + // Prepare a keystore + sun.security.tools.keytool.Main.main( + ("-keystore jks -storepass changeit -keypass changeit -dname" + + " CN=A -alias a -genkeypair -keyalg rsa").split(" ")); + + // -altsign + sun.security.tools.jarsigner.Main.main( + ("-debug -signedjar altsign.jar -keystore jks -storepass changeit" + + " -altsigner Options$X a.jar a").split(" ")); + + try (JarFile jf = new JarFile("altsign.jar")) { + JarEntry je = jf.getJarEntry("META-INF/A.RSA"); + try (InputStream is = jf.getInputStream(je)) { + if (!Arrays.equals(is.readAllBytes(), "1234".getBytes())) { + throw new Exception("altsign go wrong"); + } + } + } + + // -sigfile, -digestalg, -sigalg, -internalsf, -sectionsonly + sun.security.tools.jarsigner.Main.main( + ("-debug -signedjar new.jar -keystore jks -storepass changeit" + + " -sigfile olala -digestalg SHA1 -sigalg SHA224withRSA" + + " -internalsf -sectionsonly a.jar a").split(" ")); + + try (JarFile jf = new JarFile("new.jar")) { + JarEntry je = jf.getJarEntry("META-INF/OLALA.SF"); + Objects.requireNonNull(je); // check -sigfile + byte[] sf = null; // content of .SF + try (InputStream is = jf.getInputStream(je)) { + sf = is.readAllBytes(); // save for later comparison + Attributes attrs = new Manifest(new ByteArrayInputStream(sf)) + .getMainAttributes(); + // check -digestalg + if (!attrs.containsKey(new Attributes.Name( + "SHA1-Digest-Manifest-Main-Attributes"))) { + throw new Exception("digestalg incorrect"); + } + // check -sectionsonly + if (attrs.containsKey(new Attributes.Name( + "SHA1-Digest-Manifest"))) { + throw new Exception("SF should not have file digest"); + } + } + + je = jf.getJarEntry("META-INF/OLALA.RSA"); + try (InputStream is = jf.getInputStream(je)) { + PKCS7 p7 = new PKCS7(is.readAllBytes()); + String alg = p7.getSignerInfos()[0] + .getDigestAlgorithmId().getName(); + if (!alg.equals("SHA-224")) { // check -sigalg + throw new Exception("PKCS7 signing is using " + alg); + } + // check -internalsf + if (!Arrays.equals(sf, p7.getContentInfo().getData())) { + throw new Exception("SF not in RSA"); + } + } + + } + + // TSA-related ones are checked in ts.sh + } + + public static class X extends ContentSigner { + @Override + public byte[] generateSignedData(ContentSignerParameters parameters, + boolean omitContent, boolean applyTimestamp) + throws NoSuchAlgorithmException, CertificateException, + IOException { + return "1234".getBytes(); + } + } +}