| /* JarVerifier.java -- The verification handler of the gjarsigner tool |
| Copyright (C) 2006 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.classpath.tools.jarsigner; |
| |
| import gnu.classpath.Configuration; |
| import gnu.java.security.OID; |
| import gnu.java.security.Registry; |
| import gnu.java.security.pkcs.PKCS7SignedData; |
| import gnu.java.security.pkcs.SignerInfo; |
| import gnu.java.security.sig.ISignature; |
| import gnu.java.security.sig.ISignatureCodec; |
| import gnu.java.security.sig.dss.DSSSignature; |
| import gnu.java.security.sig.dss.DSSSignatureX509Codec; |
| import gnu.java.security.sig.rsa.RSAPKCS1V1_5Signature; |
| import gnu.java.security.sig.rsa.RSAPKCS1V1_5SignatureX509Codec; |
| import gnu.java.security.util.Util; |
| import gnu.java.util.jar.JarUtils; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.PublicKey; |
| import java.security.cert.Certificate; |
| import java.security.cert.CRLException; |
| import java.security.cert.CertificateException; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Map.Entry; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipException; |
| |
| /** |
| * The JAR verification handler of the <code>gjarsigner</code> tool. |
| */ |
| public class JarVerifier |
| { |
| private static final Logger log = Logger.getLogger(JarVerifier.class.getName()); |
| /** The owner tool of this handler. */ |
| private Main main; |
| private HashUtils util = new HashUtils(); |
| /** The JAR file to verify. */ |
| private JarFile jarFile; |
| /** Map of jar entry names to their hash. */ |
| private Map entryHashes = new HashMap(); |
| |
| JarVerifier(Main main) |
| { |
| super(); |
| |
| this.main = main; |
| } |
| |
| void start() throws Exception |
| { |
| if (Configuration.DEBUG) |
| log.entering(this.getClass().getName(), "start"); //$NON-NLS-1$ |
| String jarFileName = main.getJarFileName(); |
| jarFile = new JarFile(jarFileName); |
| |
| // 1. find all signature (.SF) files |
| List sfFiles = new ArrayList(); |
| for (Enumeration e = jarFile.entries(); e.hasMoreElements(); ) |
| { |
| JarEntry je = (JarEntry) e.nextElement(); |
| String jeName = je.getName(); |
| if (! (jeName.startsWith(JarUtils.META_INF) |
| && jeName.endsWith(JarUtils.SF_SUFFIX))) |
| continue; |
| |
| // only interested in .SF files in, and not deeper than, META-INF |
| String[] jeNameParts = jeName.split("/"); //$NON-NLS-1$ |
| if (jeNameParts.length != 2) |
| continue; |
| |
| String sfName = jeNameParts[1]; |
| String sigFileName = sfName.substring(0, sfName.length() - 3); |
| sfFiles.add(sigFileName); |
| } |
| |
| // 2. verify each one |
| if (sfFiles.isEmpty()) |
| System.out.println(Messages.getString("JarVerifier.2")); //$NON-NLS-1$ |
| else |
| { |
| int limit = sfFiles.size(); |
| int count = 0; |
| for (Iterator it = sfFiles.iterator(); it.hasNext(); ) |
| { |
| String alias = (String) it.next(); |
| if (verifySF(alias)) |
| if (verifySFEntries(alias)) |
| count++; |
| } |
| |
| if (count == 0) |
| System.out.println(Messages.getString("JarVerifier.3")); //$NON-NLS-1$ |
| else if (count != limit) |
| System.out.println(Messages.getFormattedString("JarVerifier.4", //$NON-NLS-1$ |
| new Integer[] {Integer.valueOf(count), |
| Integer.valueOf(limit)})); |
| else |
| System.out.println(Messages.getFormattedString("JarVerifier.7", //$NON-NLS-1$ |
| Integer.valueOf(limit))); |
| } |
| if (Configuration.DEBUG) |
| log.exiting(this.getClass().getName(), "start"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * @param sigFileName the name of the signature file; i.e. the name to use for |
| * both the .SF and .DSA files. |
| * @return <code>true</code> if the designated file-name (usually a key-store |
| * <i>alias</i> name) has been successfully checked as the signer of the |
| * corresponding <code>.SF</code> file. Returns <code>false</code> otherwise. |
| * @throws IOException |
| * @throws ZipException |
| * @throws CertificateException |
| * @throws CRLException |
| */ |
| private boolean verifySF(String sigFileName) throws CRLException, |
| CertificateException, ZipException, IOException |
| { |
| if (Configuration.DEBUG) |
| { |
| log.entering(this.getClass().getName(), "verifySF"); //$NON-NLS-1$ |
| log.fine("About to verify signature of " + sigFileName + "..."); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| // 1. find the corresponding .DSA file for this .SF file |
| JarEntry dsaEntry = jarFile.getJarEntry(JarUtils.META_INF + sigFileName |
| + JarUtils.DSA_SUFFIX); |
| if (dsaEntry == null) |
| throw new SecurityException(Messages.getFormattedString("JarVerifier.13", //$NON-NLS-1$ |
| sigFileName)); |
| // 2. read the .DSA file contents as a PKCS7 SignedData |
| InputStream in = jarFile.getInputStream(dsaEntry); |
| PKCS7SignedData pkcs7SignedData = new PKCS7SignedData(in); |
| |
| // 4. get the encrypted digest octet string from the first SignerInfo |
| // this octet string is the digital signature of the .SF file contents |
| Set signerInfos = pkcs7SignedData.getSignerInfos(); |
| if (signerInfos == null || signerInfos.isEmpty()) |
| throw new SecurityException(Messages.getString("JarVerifier.14")); //$NON-NLS-1$ |
| |
| SignerInfo signerInfo = (SignerInfo) signerInfos.iterator().next(); |
| byte[] encryptedDigest = signerInfo.getEncryptedDigest(); |
| if (encryptedDigest == null) |
| throw new SecurityException(Messages.getString("JarVerifier.16")); //$NON-NLS-1$ |
| |
| if (Configuration.DEBUG) |
| log.fine("\n" + Util.dumpString(encryptedDigest, "--- signedSFBytes ")); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| // 5. get the signer public key |
| Certificate cert = pkcs7SignedData.getCertificates()[0]; |
| PublicKey verifierKey = cert.getPublicKey(); |
| if (Configuration.DEBUG) |
| log.fine("--- verifier public key = " + verifierKey); //$NON-NLS-1$ |
| |
| // 6. verify the signature file signature |
| OID digestEncryptionAlgorithmOID = signerInfo.getDigestEncryptionAlgorithmId(); |
| ISignature signatureAlgorithm; |
| ISignatureCodec signatureCodec; |
| if (digestEncryptionAlgorithmOID.equals(Main.DSA_SIGNATURE_OID)) |
| { |
| signatureAlgorithm = new DSSSignature(); |
| signatureCodec = new DSSSignatureX509Codec(); |
| } |
| else |
| { |
| signatureAlgorithm = new RSAPKCS1V1_5Signature(Registry.MD5_HASH); |
| signatureCodec = new RSAPKCS1V1_5SignatureX509Codec(); |
| } |
| |
| Map signatureAttributes = new HashMap(); |
| signatureAttributes.put(ISignature.VERIFIER_KEY, verifierKey); |
| signatureAlgorithm.setupVerify(signatureAttributes); |
| |
| Object herSignature = signatureCodec.decodeSignature(encryptedDigest); |
| |
| // 7. verify the signature file contents |
| JarEntry sfEntry = jarFile.getJarEntry(JarUtils.META_INF + sigFileName |
| + JarUtils.SF_SUFFIX); |
| in = jarFile.getInputStream(sfEntry); |
| byte[] buffer = new byte[2048]; |
| int n; |
| while ((n = in.read(buffer)) != -1) |
| if (n > 0) |
| signatureAlgorithm.update(buffer, 0, n); |
| |
| boolean result = signatureAlgorithm.verify(herSignature); |
| if (Configuration.DEBUG) |
| { |
| log.fine("Signature block [" + sigFileName + "] is " //$NON-NLS-1$ //$NON-NLS-2$ |
| + (result ? "" : "NOT ") + "OK"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| log.exiting(this.getClass().getName(), "verifySF", Boolean.valueOf(result)); //$NON-NLS-1$ |
| } |
| return result; |
| } |
| |
| /** |
| * This method is called after at least one signer (usually a key-store |
| * <code>alias</code> name) was found to be trusted; i.e. his/her signature |
| * block in the corresponding <code>.DSA</code> file was successfully |
| * verified using his/her public key. |
| * <p> |
| * This method, uses the contents of the corresponding <code>.SF</code> file |
| * to compute and verify the hashes of the manifest entries in the JAR file. |
| * |
| * @param alias the name of the signature file; i.e. the name to use for both |
| * the .SF and .DSA files. |
| * @return <code>true</code> if all the entries in the corresponding |
| * <code>.SF</code> file have the same hash values as their |
| * alter-ego in the <i>manifest</i> file of the JAR file inquestion. |
| * @throws IOException if an I/O related exception occurs during the process. |
| */ |
| private boolean verifySFEntries(String alias) throws IOException |
| { |
| if (Configuration.DEBUG) |
| log.entering(this.getClass().getName(), "verifySFEntries"); //$NON-NLS-1$ |
| // 1. read the signature file |
| JarEntry jarEntry = jarFile.getJarEntry(JarUtils.META_INF + alias |
| + JarUtils.SF_SUFFIX); |
| InputStream in = jarFile.getInputStream(jarEntry); |
| Attributes attr = new Attributes(); |
| Map entries = new HashMap(); |
| JarUtils.readSFManifest(attr, entries, in); |
| |
| // 2. The .SF file by default includes a header containing a hash of the |
| // entire manifest file. When the header is present, then the verification |
| // can check to see whether or not the hash in the header indeed matches |
| // the hash of the manifest file. |
| boolean result = false; |
| String hash = attr.getValue(Main.DIGEST_MANIFEST_ATTR); |
| if (hash != null) |
| result = verifyManifest(hash); |
| |
| // A verification is still considered successful if none of the files that |
| // were in the JAR file when the signature was generated have been changed |
| // since then, which is the case if the hashes in the non-header sections |
| // of the .SF file equal the hashes of the corresponding sections in the |
| // manifest file. |
| // |
| // 3. Read each file in the JAR file that has an entry in the .SF file. |
| // While reading, compute the file's digest, and then compare the result |
| // with the digest for this file in the manifest section. The digests |
| // should be the same, or verification fails. |
| if (! result) |
| for (Iterator it = entries.keySet().iterator(); it.hasNext();) |
| { |
| Entry me = (Entry) it.next(); |
| String name = (String) me.getKey(); |
| attr = (Attributes) me.getValue(); |
| hash = attr.getValue(Main.DIGEST_ATTR); |
| result = verifySFEntry(name, hash); |
| if (! result) |
| break; |
| } |
| |
| if (Configuration.DEBUG) |
| log.exiting(this.getClass().getName(), "verifySFEntries", //$NON-NLS-1$ |
| Boolean.valueOf(result)); |
| return result; |
| } |
| |
| /** |
| * @param hash Base-64 encoded form of the manifest's digest. |
| * @return <code>true</code> if our computation of the manifest's hash |
| * matches the given value; <code>false</code> otherwise. |
| * @throws IOException if unable to acquire the JAR's manifest entry. |
| */ |
| private boolean verifyManifest(String hash) throws IOException |
| { |
| return verifySFEntry(JarFile.MANIFEST_NAME, hash); |
| } |
| |
| /** |
| * @param name the name of a JAR entry to verify. |
| * @param hash Base-64 encoded form of the designated entry's digest. |
| * @return <code>true</code> if our computation of the JAR entry's hash |
| * matches the given value; <code>false</code> otherwise. |
| * @throws IOException if an exception occurs while returning the entry's |
| * input stream. |
| */ |
| private boolean verifySFEntry(String name, String hash) throws IOException |
| { |
| String expectedValue = getEntryHash(JarFile.MANIFEST_NAME); |
| boolean result = expectedValue.equalsIgnoreCase(hash); |
| if (Configuration.DEBUG) |
| log.fine("Is " + name + " OK? " + result); //$NON-NLS-1$ //$NON-NLS-2$ |
| return result; |
| } |
| |
| private String getEntryHash(String entryName) throws IOException |
| { |
| String result = (String) entryHashes.get(entryName); |
| if (result == null) |
| { |
| JarEntry manifest = jarFile.getJarEntry(entryName); |
| InputStream in = jarFile.getInputStream(manifest); |
| result = util.hashStream(in); |
| entryHashes.put(entryName, result); |
| } |
| |
| return result; |
| } |
| } |