1 /* Main.java -- JAR signing and verification tool not unlike jarsigner
2 Copyright (C) 2006 Free Software Foundation, Inc.
4 This file is part of GNU Classpath.
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library. Thus, the terms and
23 conditions of the GNU General Public License cover the whole
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module. An independent module is a module which is not derived from
33 or based on this library. If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so. If you do not wish to do so, delete this
36 exception statement from your version. */
39 package gnu.classpath.tools.jarsigner;
41 import gnu.classpath.Configuration;
42 import gnu.classpath.SystemProperties;
43 import gnu.classpath.tools.common.CallbackUtil;
44 import gnu.classpath.tools.common.ClasspathToolParser;
45 import gnu.classpath.tools.common.ProviderUtil;
46 import gnu.classpath.tools.getopt.FileArgumentCallback;
47 import gnu.classpath.tools.getopt.Option;
48 import gnu.classpath.tools.getopt.OptionException;
49 import gnu.classpath.tools.getopt.OptionGroup;
50 import gnu.java.security.OID;
51 import gnu.java.security.Registry;
52 import gnu.javax.security.auth.callback.ConsoleCallbackHandler;
55 import java.io.FileNotFoundException;
56 import java.io.IOException;
57 import java.io.InputStream;
59 import java.security.Key;
60 import java.security.KeyStore;
61 import java.security.KeyStoreException;
62 import java.security.NoSuchAlgorithmException;
63 import java.security.PrivateKey;
64 import java.security.Provider;
65 import java.security.Security;
66 import java.security.UnrecoverableKeyException;
67 import java.security.cert.Certificate;
68 import java.security.cert.CertificateException;
69 import java.util.ArrayList;
70 import java.util.Locale;
71 import java.util.jar.Attributes.Name;
72 import java.util.logging.Logger;
74 import javax.security.auth.callback.Callback;
75 import javax.security.auth.callback.CallbackHandler;
76 import javax.security.auth.callback.PasswordCallback;
77 import javax.security.auth.callback.UnsupportedCallbackException;
80 * The GNU Classpath implementation of the <i>jarsigner</i> tool.
82 * The <i>jarsigner</i> tool is used to sign and verify JAR (Java ARchive)
85 * This implementation is intended to be compatible with the behaviour
86 * described in the public documentation of the same tool included in JDK 1.4.
90 protected static final Logger log = Logger.getLogger(Main.class.getName());
91 static final String KEYTOOL_TOOL = "jarsigner"; //$NON-NLS-1$
92 private static final Locale EN_US_LOCALE = new Locale("en", "US"); //$NON-NLS-1$ //$NON-NLS-2$
93 static final String DIGEST = "SHA1-Digest"; //$NON-NLS-1$
94 static final String DIGEST_MANIFEST = "SHA1-Digest-Manifest"; //$NON-NLS-1$
95 static final Name DIGEST_ATTR = new Name(DIGEST);
96 static final Name DIGEST_MANIFEST_ATTR = new Name(DIGEST_MANIFEST);
97 static final OID DSA_SIGNATURE_OID = new OID(Registry.DSA_OID_STRING);
98 static final OID RSA_SIGNATURE_OID = new OID(Registry.RSA_OID_STRING);
100 protected boolean verify;
101 protected String ksURL;
102 protected String ksType;
103 protected String password;
104 protected String ksPassword;
105 protected String sigFileName;
106 protected String signedJarFileName;
107 protected boolean verbose;
108 protected boolean certs;
109 protected boolean internalSF;
110 protected boolean sectionsOnly;
111 protected String providerClassName;
112 protected String jarFileName;
113 protected String alias;
115 protected Provider provider;
116 private boolean providerInstalled;
117 private char[] ksPasswordChars;
118 private KeyStore store;
119 private char[] passwordChars;
120 private PrivateKey signerPrivateKey;
121 private Certificate[] signerCertificateChain;
122 /** The callback handler to use when needing to interact with user. */
123 private CallbackHandler handler;
124 /** The command line parser. */
125 private ToolParser cmdLineParser;
126 protected ArrayList fileAndAlias = new ArrayList();;
133 public static final void main(String[] args)
135 if (Configuration.DEBUG)
136 log.entering(Main.class.getName(), "main", args); //$NON-NLS-1$
137 Main tool = new Main();
141 tool.processArgs(args);
145 catch (SecurityException x)
147 if (Configuration.DEBUG)
148 log.throwing(Main.class.getName(), "main", x); //$NON-NLS-1$
149 System.err.println(Messages.getString("Main.7") + x.getMessage()); //$NON-NLS-1$
153 if (Configuration.DEBUG)
154 log.throwing(Main.class.getName(), "main", x); //$NON-NLS-1$
155 System.err.println(Messages.getString("Main.9") + x); //$NON-NLS-1$
161 if (Configuration.DEBUG)
162 log.exiting(Main.class.getName(), "main", Integer.valueOf(result)); //$NON-NLS-1$
166 // helper methods -----------------------------------------------------------
169 * Read the command line arguments setting the tool's parameters in
170 * preparation for the user desired action.
172 * @param args an array of options (strings).
173 * @throws Exception if an exception occurs during the process.
175 private void processArgs(String[] args) throws Exception
177 if (Configuration.DEBUG)
178 log.entering(this.getClass().getName(), "processArgs", args); //$NON-NLS-1$
179 cmdLineParser = new ToolParser();
180 cmdLineParser.initializeParser();
181 cmdLineParser.parse(args, new ToolParserCallback());
186 if (Configuration.DEBUG)
188 log.fine("Will verify with the following parameters:"); //$NON-NLS-1$
189 log.fine(" jar-file = '" + jarFileName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
190 log.fine("Options:"); //$NON-NLS-1$
191 log.fine(" provider = '" + providerClassName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
192 log.fine(" verbose ? " + verbose); //$NON-NLS-1$
193 log.fine(" certs ? " + certs); //$NON-NLS-1$
194 log.fine(" internalsf ? " + internalSF); //$NON-NLS-1$
195 log.fine(" sectionsonly ? " + sectionsOnly); //$NON-NLS-1$
200 setupSigningParams();
201 if (Configuration.DEBUG)
203 log.fine("Will sign with the following parameters:"); //$NON-NLS-1$
204 log.fine(" jar-file = '" + jarFileName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
205 log.fine(" alias = '" + alias + "'"); //$NON-NLS-1$ //$NON-NLS-2$
206 log.fine("Options:"); //$NON-NLS-1$
207 log.fine(" keystore = '" + ksURL + "'"); //$NON-NLS-1$ //$NON-NLS-2$
208 log.fine(" storetype = '" + ksType + "'"); //$NON-NLS-1$ //$NON-NLS-2$
209 log.fine(" storepass = '" + ksPassword + "'"); //$NON-NLS-1$ //$NON-NLS-2$
210 log.fine(" keypass = '" + password + "'"); //$NON-NLS-1$ //$NON-NLS-2$
211 log.fine(" sigfile = '" + sigFileName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
212 log.fine(" signedjar = '" + signedJarFileName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
213 log.fine(" provider = '" + providerClassName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
214 log.fine(" verbose ? " + verbose); //$NON-NLS-1$
215 log.fine(" internalsf ? " + internalSF); //$NON-NLS-1$
216 log.fine(" sectionsonly ? " + sectionsOnly); //$NON-NLS-1$
219 if (Configuration.DEBUG)
220 log.exiting(this.getClass().getName(), "processArgs"); //$NON-NLS-1$
224 * Invokes the <code>start()</code> method of the concrete handler.
226 * Depending on the result of processing the command line arguments, this
227 * handler may be one for signing the jar, or verifying it.
229 * @throws Exception if an exception occurs during the process.
231 private void start() throws Exception
233 if (Configuration.DEBUG)
234 log.entering(this.getClass().getName(), "start"); //$NON-NLS-1$
237 JarVerifier jv = new JarVerifier(this);
242 JarSigner js = new JarSigner(this);
245 if (Configuration.DEBUG)
246 log.exiting(this.getClass().getName(), "start"); //$NON-NLS-1$
250 * Ensures that the underlying JVM is left in the same state as we found it
251 * when we first launched the tool. Specifically, if we have installed a new
252 * security provider then now is the time to remove it.
254 * Note (rsn): this may not be necessary if we terminate the JVM; i.e. call
255 * {@link System#exit(int)} at the end of the tool's invocation. Nevertheless
256 * it's good practive to return the JVM to its initial state.
258 private void teardown()
260 if (Configuration.DEBUG)
261 log.entering(this.getClass().getName(), "teardown"); //$NON-NLS-1$
262 if (providerInstalled)
263 ProviderUtil.removeProvider(provider.getName());
265 if (Configuration.DEBUG)
266 log.exiting(this.getClass().getName(), "teardown"); //$NON-NLS-1$
270 * After processing the command line arguments, this method is invoked to
271 * process the common parameters which may have been encountered among the
274 * Common parameters are those which are allowed in both signing and
275 * verification modes.
277 * @throws InstantiationException if a security provider class name is
278 * specified but that class name is that of either an interface or
280 * @throws IllegalAccessException if a security provider class name is
281 * specified but no 0-arguments constructor is defined for that
283 * @throws ClassNotFoundException if a security provider class name is
284 * specified but no such class was found in the classpath.
285 * @throws IOException if the JAR file name for signing, or verifying, does
286 * not exist, exists but denotes a directory, or is not readable.
288 private void setupCommonParams() throws InstantiationException,
289 IllegalAccessException, ClassNotFoundException, IOException
291 if (Configuration.DEBUG)
292 log.entering(this.getClass().getName(), "setupCommonParams"); //$NON-NLS-1$
293 File jar = new File(jarFileName);
295 throw new FileNotFoundException(jarFileName);
297 if (jar.isDirectory())
298 throw new IOException(Messages.getFormattedString("Main.70", jarFileName)); //$NON-NLS-1$
301 throw new IOException(Messages.getFormattedString("Main.72", jarFileName)); //$NON-NLS-1$ //$NON-NLS-2$
303 if (providerClassName != null && providerClassName.length() > 0)
305 provider = (Provider) Class.forName(providerClassName).newInstance();
306 // is it already installed?
307 String providerName = provider.getName();
308 Provider installedProvider = Security.getProvider(providerName);
309 if (installedProvider != null)
311 if (Configuration.DEBUG)
312 log.finer("Provider " + providerName + " is already installed"); //$NON-NLS-1$ //$NON-NLS-2$
315 installNewProvider();
318 if (! verbose && certs)
320 if (Configuration.DEBUG)
321 log.fine("Option <certs> is set but <verbose> is not. Ignored"); //$NON-NLS-1$
325 if (Configuration.DEBUG)
326 log.exiting(this.getClass().getName(), "setupCommonParams"); //$NON-NLS-1$
330 * Install the user defined security provider in the underlying JVM.
332 * Also record this fact so we can remove it when we exit the tool.
334 private void installNewProvider()
336 if (Configuration.DEBUG)
337 log.entering(this.getClass().getName(), "installNewProvider"); //$NON-NLS-1$
338 providerInstalled = ProviderUtil.addProvider(provider) != -1;
339 if (Configuration.DEBUG)
340 log.exiting(this.getClass().getName(), "installNewProvider"); //$NON-NLS-1$
344 * After processing the command line arguments, this method is invoked to
345 * process the parameters which may have been encountered among the actual
346 * arguments, and which are specific to the signing action of the tool.
348 * @throws KeyStoreException if no implementation of the designated (or
349 * default type) of a key store is availabe.
350 * @throws IOException if an I/O related exception occurs during the process.
351 * @throws NoSuchAlgorithmException if an implementation of an algorithm used
352 * by the key store is not available.
353 * @throws CertificateException if an exception occurs while reading a
354 * certificate from the key store.
355 * @throws UnsupportedCallbackException if no implementation of a password
356 * callback is available.
357 * @throws UnrecoverableKeyException if the wrong password was used to unlock
359 * @throws SecurityException if the designated alias is not known to the key
360 * store or is not an Alias of a Key Entry.
362 private void setupSigningParams() throws KeyStoreException, IOException,
363 NoSuchAlgorithmException, CertificateException,
364 UnsupportedCallbackException, UnrecoverableKeyException
366 if (Configuration.DEBUG)
367 log.entering(this.getClass().getName(), "setupSigningParams"); //$NON-NLS-1$
368 if (ksURL == null || ksURL.trim().length() == 0)
370 String userHome = SystemProperties.getProperty("user.home"); //$NON-NLS-1$
371 if (userHome == null || userHome.trim().length() == 0)
372 throw new SecurityException(Messages.getString("Main.85")); //$NON-NLS-1$
374 ksURL = "file:" + userHome.trim() + "/.keystore"; //$NON-NLS-1$ //$NON-NLS-2$
378 ksURL = ksURL.trim();
379 if (ksURL.indexOf(":") == -1) //$NON-NLS-1$
380 ksURL = "file:" + ksURL; //$NON-NLS-1$
383 if (ksType == null || ksType.trim().length() == 0)
384 ksType = KeyStore.getDefaultType();
386 ksType = ksType.trim();
388 store = KeyStore.getInstance(ksType);
390 if (ksPassword == null)
392 // ask the user to provide one
393 PasswordCallback pcb = new PasswordCallback(Messages.getString("Main.92"), //$NON-NLS-1$
395 getCallbackHandler().handle(new Callback[] { pcb });
396 ksPasswordChars = pcb.getPassword();
399 ksPasswordChars = ksPassword.toCharArray();
401 URL url = new URL(ksURL);
402 InputStream stream = url.openStream();
403 store.load(stream, ksPasswordChars);
405 if (! store.containsAlias(alias))
406 throw new SecurityException(Messages.getFormattedString("Main.6", alias)); //$NON-NLS-1$
408 if (! store.isKeyEntry(alias))
409 throw new SecurityException(Messages.getFormattedString("Main.95", alias)); //$NON-NLS-1$
412 if (password == null)
414 passwordChars = ksPasswordChars;
417 key = store.getKey(alias, passwordChars);
419 catch (UnrecoverableKeyException x)
421 // ask the user to provide one
422 String prompt = Messages.getFormattedString("Main.97", alias); //$NON-NLS-1$
423 PasswordCallback pcb = new PasswordCallback(prompt, false);
424 getCallbackHandler().handle(new Callback[] { pcb });
425 passwordChars = pcb.getPassword();
427 key = store.getKey(alias, passwordChars);
432 passwordChars = password.toCharArray();
433 key = store.getKey(alias, passwordChars);
436 if (! (key instanceof PrivateKey))
437 throw new SecurityException(Messages.getFormattedString("Main.99", alias)); //$NON-NLS-1$
439 signerPrivateKey = (PrivateKey) key;
440 signerCertificateChain = store.getCertificateChain(alias);
441 if (Configuration.DEBUG)
442 log.fine(String.valueOf(signerCertificateChain));
444 if (sigFileName == null)
447 sigFileName = sigFileName.toUpperCase(EN_US_LOCALE);
448 if (sigFileName.length() > 8)
449 sigFileName = sigFileName.substring(0, 8);
451 char[] chars = sigFileName.toCharArray();
452 for (int i = 0; i < chars.length; i++)
455 if (! (Character.isLetter(c)
456 || Character.isDigit(c)
462 sigFileName = new String(chars);
464 if (signedJarFileName == null)
465 signedJarFileName = jarFileName;
467 if (Configuration.DEBUG)
468 log.exiting(this.getClass().getName(), "setupSigningParams"); //$NON-NLS-1$
481 String getSigFileName()
483 return this.sigFileName;
486 String getJarFileName()
488 return this.jarFileName;
491 boolean isSectionsOnly()
493 return this.sectionsOnly;
496 boolean isInternalSF()
498 return this.internalSF;
501 PrivateKey getSignerPrivateKey()
503 return this.signerPrivateKey;
506 Certificate[] getSignerCertificateChain()
508 return signerCertificateChain;
511 String getSignedJarFileName()
513 return this.signedJarFileName;
517 * Return a CallbackHandler which uses the Console (System.in and System.out)
518 * for interacting with the user.
520 * This method first finds all currently installed security providers capable
521 * of providing such service and then in turn attempts to instantiate the
522 * handler from those providers. As soon as one provider returns a non-null
523 * instance of the callback handler, the search stops and that instance is
524 * set to be used from now on.
526 * If no installed providers were found, this method falls back on the GNU
527 * provider, by-passing the Security search mechanism. The default console
528 * callback handler implementation is {@link ConsoleCallbackHandler}.
530 * @return a console-based {@link CallbackHandler}.
532 protected CallbackHandler getCallbackHandler()
535 handler = CallbackUtil.getConsoleHandler();
540 private class ToolParserCallback
541 extends FileArgumentCallback
543 public void notifyFile(String fileArgument)
545 fileAndAlias.add(fileArgument);
549 private class ToolParser
550 extends ClasspathToolParser
554 super(KEYTOOL_TOOL, true);
557 protected void validate() throws OptionException
559 if (fileAndAlias.size() < 1)
560 throw new OptionException(Messages.getString("Main.133")); //$NON-NLS-1$
562 jarFileName = (String) fileAndAlias.get(0);
563 if (! verify) // must have an ALIAS. use "mykey" if undefined
564 if (fileAndAlias.size() < 2)
566 if (Configuration.DEBUG)
567 log.fine("Missing ALIAS argument. Will use [mykey] instead"); //$NON-NLS-1$
568 alias = "mykey"; //$NON-NLS-1$
571 alias = (String) fileAndAlias.get(1);
574 public void initializeParser()
576 setHeader(Messages.getString("Main.2")); //$NON-NLS-1$
577 setFooter(Messages.getString("Main.1")); //$NON-NLS-1$
578 OptionGroup signGroup = new OptionGroup(Messages.getString("Main.0")); //$NON-NLS-1$
579 signGroup.add(new Option("keystore", //$NON-NLS-1$
580 Messages.getString("Main.101"), //$NON-NLS-1$
581 Messages.getString("Main.102")) //$NON-NLS-1$
583 public void parsed(String argument) throws OptionException
588 signGroup.add(new Option("storetype", //$NON-NLS-1$
589 Messages.getString("Main.104"), //$NON-NLS-1$
590 Messages.getString("Main.105")) //$NON-NLS-1$
592 public void parsed(String argument) throws OptionException
597 signGroup.add(new Option("storepass", //$NON-NLS-1$
598 Messages.getString("Main.107"), //$NON-NLS-1$
599 Messages.getString("Main.108")) //$NON-NLS-1$
601 public void parsed(String argument) throws OptionException
603 ksPassword = argument;
606 signGroup.add(new Option("keypass", //$NON-NLS-1$
607 Messages.getString("Main.110"), //$NON-NLS-1$
608 Messages.getString("Main.111")) //$NON-NLS-1$
610 public void parsed(String argument) throws OptionException
615 signGroup.add(new Option("sigfile", //$NON-NLS-1$
616 Messages.getString("Main.113"), //$NON-NLS-1$
617 Messages.getString("Main.114")) //$NON-NLS-1$
619 public void parsed(String argument) throws OptionException
621 sigFileName = argument;
624 signGroup.add(new Option("signedjar", //$NON-NLS-1$
625 Messages.getString("Main.116"), //$NON-NLS-1$
626 Messages.getString("Main.117")) //$NON-NLS-1$
628 public void parsed(String argument) throws OptionException
630 signedJarFileName = argument;
635 OptionGroup verifyGroup = new OptionGroup(Messages.getString("Main.118")); //$NON-NLS-1$
636 verifyGroup.add(new Option("verify", //$NON-NLS-1$
637 Messages.getString("Main.120")) //$NON-NLS-1$
639 public void parsed(String argument) throws OptionException
644 verifyGroup.add(new Option("certs", //$NON-NLS-1$
645 Messages.getString("Main.122")) //$NON-NLS-1$
647 public void parsed(String argument) throws OptionException
654 OptionGroup commonGroup = new OptionGroup(Messages.getString("Main.123")); //$NON-NLS-1$
655 commonGroup.add(new Option("verbose", //$NON-NLS-1$
656 Messages.getString("Main.125")) //$NON-NLS-1$
658 public void parsed(String argument) throws OptionException
663 commonGroup.add(new Option("internalsf", //$NON-NLS-1$
664 Messages.getString("Main.127")) //$NON-NLS-1$
666 public void parsed(String argument) throws OptionException
671 commonGroup.add(new Option("sectionsonly", //$NON-NLS-1$
672 Messages.getString("Main.129")) //$NON-NLS-1$
674 public void parsed(String argument) throws OptionException
679 commonGroup.add(new Option("provider", //$NON-NLS-1$
680 Messages.getString("Main.131"), //$NON-NLS-1$
681 Messages.getString("Main.132")) //$NON-NLS-1$
683 public void parsed(String argument) throws OptionException
685 providerClassName = argument;