--- a/jdk/src/share/classes/sun/security/tools/JarSigner.java Thu Mar 26 17:39:42 2009 -0700
+++ b/jdk/src/share/classes/sun/security/tools/JarSigner.java Fri Mar 27 11:05:45 2009 +0800
@@ -1,5 +1,5 @@
/*
- * Copyright 1997-2007 Sun Microsystems, Inc. All Rights Reserved.
+ * Copyright 1997-2009 Sun Microsystems, Inc. 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
@@ -32,28 +32,44 @@
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.net.SocketTimeoutException;
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.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
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;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.util.Map.Entry;
import sun.security.x509.*;
import sun.security.util.*;
import sun.misc.BASE64Encoder;
+
/**
* <p>The jarsigner utility.
*
+ * The exit codes for the main method are:
+ *
+ * 0: success
+ * 1: any error that the jar cannot be signed or verified, including:
+ * keystore loading error
+ * TSP communciation error
+ * jarsigner command line error...
+ * otherwise: error codes from -strict
+ *
* @author Roland Schemers
* @author Jan Luehe
*/
@@ -84,8 +100,6 @@
// Attention:
// This is the entry that get launched by the security tool jarsigner.
- // It's marked as exported private per AppServer Team's request.
- // See http://ccc.sfbay/6428446
public static void main(String args[]) throws Exception {
JarSigner js = new JarSigner();
js.run(args);
@@ -93,31 +107,32 @@
static final String VERSION = "1.0";
- static final int IN_KEYSTORE = 0x01;
+ static final int IN_KEYSTORE = 0x01; // signer is in keystore
static final int IN_SCOPE = 0x02;
-
- // signer's certificate chain (when composing)
- X509Certificate[] certChain;
+ static final int NOT_ALIAS = 0x04; // alias list is NOT empty and
+ // signer is not in alias list
+ static final int SIGNED_BY_ALIAS = 0x08; // signer is in alias list
- /*
- * private key
- */
- PrivateKey privateKey;
- KeyStore store;
+ X509Certificate[] certChain; // signer's cert chain (when composing)
+ PrivateKey privateKey; // private key
+ KeyStore store; // the keystore specified by -keystore
+ // or the default keystore, never null
IdentityScope scope;
String keystore; // key store file
boolean nullStream = false; // null keystore input stream (NONE)
boolean token = false; // token-based keystore
- String jarfile; // jar file to sign
+ String jarfile; // jar file to sign or verify
String alias; // alias to sign jar with
+ List<String> ckaliases = new ArrayList<String>(); // aliases in -verify
char[] storepass; // keystore password
boolean protectedPath; // protected authentication path
String storetype; // keystore type
String providerName; // provider name
Vector<String> providers = null; // list of providers
- HashMap<String,String> providerArgs = new HashMap<String, String>(); // arguments for provider constructors
+ // arguments for provider constructors
+ HashMap<String,String> providerArgs = new HashMap<String, String>();
char[] keypass; // private key password
String sigfile; // name of .SF file
String sigalg; // name of signature algorithm
@@ -125,12 +140,14 @@
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
boolean verify = false; // verify the jar
- boolean verbose = false; // verbose output when signing/verifying
+ String verbose = null; // verbose output when signing/verifying
boolean showcerts = false; // show certs when verifying
boolean debug = false; // debug
boolean signManifest = true; // "sign" the whole manifest
boolean externalSF = true; // leave the .SF out of the PKCS7 block
+ boolean strict = false; // treat warnings as error
// read zip entry raw bytes
private ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
@@ -139,14 +156,22 @@
private String altSignerClass = null;
private String altSignerClasspath = null;
private ZipFile zipFile = null;
+
private boolean hasExpiredCert = false;
private boolean hasExpiringCert = false;
private boolean notYetValidCert = false;
-
+ private boolean chainNotValidated = false;
+ private boolean notSignedByAlias = false;
+ private boolean aliasNotInStore = false;
+ private boolean hasUnsignedEntry = false;
private boolean badKeyUsage = false;
private boolean badExtendedKeyUsage = false;
private boolean badNetscapeCertType = false;
+ CertificateFactory certificateFactory;
+ CertPathValidator validator;
+ PKIXParameters pkixParameters;
+
public void run(String args[]) {
try {
parseArgs(args);
@@ -184,14 +209,6 @@
}
}
- hasExpiredCert = false;
- hasExpiringCert = false;
- notYetValidCert = false;
-
- badKeyUsage = false;
- badExtendedKeyUsage = false;
- badNetscapeCertType = false;
-
if (verify) {
try {
loadKeyStore(keystore, false);
@@ -238,6 +255,29 @@
storepass = null;
}
}
+
+ if (strict) {
+ int exitCode = 0;
+ if (hasExpiringCert) {
+ exitCode |= 2;
+ }
+ if (chainNotValidated) {
+ // hasExpiredCert and notYetValidCert included in this case
+ exitCode |= 4;
+ }
+ if (badKeyUsage || badExtendedKeyUsage || badNetscapeCertType) {
+ exitCode |= 8;
+ }
+ if (hasUnsignedEntry) {
+ exitCode |= 16;
+ }
+ if (notSignedByAlias || aliasNotInStore) {
+ exitCode |= 32;
+ }
+ if (exitCode != 0) {
+ System.exit(exitCode);
+ }
+ }
}
/*
@@ -247,25 +287,26 @@
/* parse flags */
int n = 0;
- for (n=0; (n < args.length) && args[n].startsWith("-"); n++) {
+ if (args.length == 0) fullusage();
+ for (n=0; n < args.length; n++) {
String flags = args[n];
if (collator.compare(flags, "-keystore") == 0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
keystore = args[n];
} else if (collator.compare(flags, "-storepass") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
storepass = args[n].toCharArray();
} else if (collator.compare(flags, "-storetype") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
storetype = args[n];
} else if (collator.compare(flags, "-providerName") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
providerName = args[n];
} else if ((collator.compare(flags, "-provider") == 0) ||
(collator.compare(flags, "-providerClass") == 0)) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
if (providers == null) {
providers = new Vector<String>(3);
}
@@ -274,35 +315,38 @@
if (args.length > (n+1)) {
flags = args[n+1];
if (collator.compare(flags, "-providerArg") == 0) {
- if (args.length == (n+2)) usage();
+ if (args.length == (n+2)) usageNoArg();
providerArgs.put(args[n], args[n+2]);
n += 2;
}
}
} else if (collator.compare(flags, "-protected") ==0) {
protectedPath = true;
+ } else if (collator.compare(flags, "-certchain") ==0) {
+ if (++n == args.length) usageNoArg();
+ altCertChain = args[n];
} else if (collator.compare(flags, "-debug") ==0) {
debug = true;
} else if (collator.compare(flags, "-keypass") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
keypass = args[n].toCharArray();
} else if (collator.compare(flags, "-sigfile") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
sigfile = args[n];
} else if (collator.compare(flags, "-signedjar") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
signedjar = args[n];
} else if (collator.compare(flags, "-tsa") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
tsaUrl = args[n];
} else if (collator.compare(flags, "-tsacert") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
tsaAlias = args[n];
} else if (collator.compare(flags, "-altsigner") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
altSignerClass = args[n];
} else if (collator.compare(flags, "-altsignerpath") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
altSignerClasspath = args[n];
} else if (collator.compare(flags, "-sectionsonly") ==0) {
signManifest = false;
@@ -311,30 +355,56 @@
} else if (collator.compare(flags, "-verify") ==0) {
verify = true;
} else if (collator.compare(flags, "-verbose") ==0) {
- verbose = true;
+ verbose = "all";
+ } else if (collator.compare(flags, "-verbose:all") ==0) {
+ verbose = "all";
+ } else if (collator.compare(flags, "-verbose:summary") ==0) {
+ verbose = "summary";
+ } else if (collator.compare(flags, "-verbose:grouped") ==0) {
+ verbose = "grouped";
} else if (collator.compare(flags, "-sigalg") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
sigalg = args[n];
} else if (collator.compare(flags, "-digestalg") ==0) {
- if (++n == args.length) usage();
+ if (++n == args.length) usageNoArg();
digestalg = args[n];
} else if (collator.compare(flags, "-certs") ==0) {
showcerts = true;
+ } else if (collator.compare(flags, "-strict") ==0) {
+ strict = true;
} else if (collator.compare(flags, "-h") == 0 ||
collator.compare(flags, "-help") == 0) {
- usage();
+ fullusage();
} else {
- System.err.println(rb.getString("Illegal option: ") + flags);
- usage();
+ if (!flags.startsWith("-")) {
+ if (jarfile == null) {
+ jarfile = flags;
+ } else {
+ alias = flags;
+ ckaliases.add(alias);
+ }
+ } else {
+ System.err.println(
+ rb.getString("Illegal option: ") + flags);
+ usage();
+ }
}
}
- if (n == args.length) usage();
- jarfile = args[n++];
+ // -certs must always be specified with -verbose
+ if (verbose == null) showcerts = false;
- if (!verify) {
- if (n == args.length) usage();
- alias = args[n++];
+ if (jarfile == null) {
+ System.err.println(rb.getString("Please specify jarfile name"));
+ usage();
+ }
+ if (!verify && alias == null) {
+ System.err.println(rb.getString("Please specify alias name"));
+ usage();
+ }
+ if (!verify && ckaliases.size() > 1) {
+ System.err.println(rb.getString("Only one alias can be specified"));
+ usage();
}
if (storetype == null) {
@@ -357,7 +427,6 @@
if (token && !nullStream) {
System.err.println(MessageFormat.format(rb.getString
("-keystore must be NONE if -storetype is {0}"), storetype));
- System.err.println();
usage();
}
@@ -365,7 +434,6 @@
System.err.println(MessageFormat.format(rb.getString
("-keypass can not be specified " +
"if -storetype is {0}"), storetype));
- System.err.println();
usage();
}
@@ -374,7 +442,6 @@
System.err.println(rb.getString
("If -protected is specified, " +
"then -storepass and -keypass must not be specified"));
- System.err.println();
usage();
}
}
@@ -383,17 +450,27 @@
System.err.println(rb.getString
("If keystore is not password protected, " +
"then -storepass and -keypass must not be specified"));
- System.err.println();
usage();
}
}
}
+ void usageNoArg() {
+ System.out.println(rb.getString("Option lacks argument"));
+ usage();
+ }
+
void usage() {
+ System.out.println();
+ System.out.println(rb.getString("Please type jarsigner -help for usage"));
+ System.exit(1);
+ }
+
+ void fullusage() {
System.out.println(rb.getString
("Usage: jarsigner [options] jar-file alias"));
System.out.println(rb.getString
- (" jarsigner -verify [options] jar-file"));
+ (" jarsigner -verify [options] jar-file [alias...]"));
System.out.println();
System.out.println(rb.getString
("[-keystore <url>] keystore location"));
@@ -408,6 +485,9 @@
("[-keypass <password>] password for private key (if different)"));
System.out.println();
System.out.println(rb.getString
+ ("[-certchain <file>] name of alternative certchain file"));
+ System.out.println();
+ System.out.println(rb.getString
("[-sigfile <file>] name of .SF/.DSA file"));
System.out.println();
System.out.println(rb.getString
@@ -423,7 +503,9 @@
("[-verify] verify a signed JAR file"));
System.out.println();
System.out.println(rb.getString
- ("[-verbose] verbose output when signing/verifying"));
+ ("[-verbose[:suboptions]] verbose output when signing/verifying."));
+ System.out.println(rb.getString
+ (" suboptions can be all, grouped or summary"));
System.out.println();
System.out.println(rb.getString
("[-certs] display certificates when verbose and verifying"));
@@ -457,15 +539,17 @@
System.out.println(rb.getString
(" [-providerArg <arg>]] ... master class file and constructor argument"));
System.out.println();
+ System.out.println(rb.getString
+ ("[-strict] treat warnings as errors"));
+ System.out.println();
- System.exit(1);
+ System.exit(0);
}
void verifyJar(String jarName)
throws Exception
{
- boolean anySigned = false;
- boolean hasUnsignedEntry = false;
+ boolean anySigned = false; // if there exists entry inside jar signed
JarFile jf = null;
try {
@@ -494,11 +578,18 @@
Manifest man = jf.getManifest();
+ // The map to record display info, only used when -verbose provided
+ // key: signer info string
+ // value: the list of files with common key
+ Map<String,List<String>> output =
+ new LinkedHashMap<String,List<String>>();
+
if (man != null) {
- if (verbose) System.out.println();
+ if (verbose != null) System.out.println();
Enumeration<JarEntry> e = entriesVec.elements();
long now = System.currentTimeMillis();
+ String tab = rb.getString(" ");
while (e.hasMoreElements()) {
JarEntry je = e.nextElement();
@@ -509,77 +600,118 @@
hasUnsignedEntry |= !je.isDirectory() && !isSigned
&& !signatureRelated(name);
- if (verbose) {
- int inStoreOrScope = inKeyStore(signers);
- boolean inStore = (inStoreOrScope & IN_KEYSTORE) != 0;
- boolean inScope = (inStoreOrScope & IN_SCOPE) != 0;
+ int inStoreOrScope = inKeyStore(signers);
+
+ boolean inStore = (inStoreOrScope & IN_KEYSTORE) != 0;
+ boolean inScope = (inStoreOrScope & IN_SCOPE) != 0;
+
+ notSignedByAlias |= (inStoreOrScope & NOT_ALIAS) != 0;
+ aliasNotInStore |= isSigned && (!inStore && !inScope);
+
+ // Only used when -verbose provided
+ StringBuffer sb = null;
+ if (verbose != null) {
+ sb = new StringBuffer();
boolean inManifest =
((man.getAttributes(name) != null) ||
(man.getAttributes("./"+name) != null) ||
(man.getAttributes("/"+name) != null));
- System.out.print(
+ sb.append(
(isSigned ? rb.getString("s") : rb.getString(" ")) +
(inManifest ? rb.getString("m") : rb.getString(" ")) +
(inStore ? rb.getString("k") : rb.getString(" ")) +
(inScope ? rb.getString("i") : rb.getString(" ")) +
- rb.getString(" "));
- StringBuffer sb = new StringBuffer();
- String s = Long.toString(je.getSize());
- for (int i = 6 - s.length(); i > 0; --i) {
- sb.append(' ');
- }
- sb.append(s).append(' ').
- append(new Date(je.getTime()).toString());
- sb.append(' ').append(je.getName());
- System.out.println(sb.toString());
+ ((inStoreOrScope & NOT_ALIAS) != 0 ?"X":" ") +
+ rb.getString(" "));
+ sb.append("|");
+ }
- if (signers != null && showcerts) {
- String tab = rb.getString(" ");
- for (int i = 0; i < signers.length; i++) {
- System.out.println();
- List<? extends Certificate> certs =
- signers[i].getSignerCertPath()
- .getCertificates();
- // display the signature timestamp, if present
- Timestamp timestamp = signers[i].getTimestamp();
- if (timestamp != null) {
- System.out.println(
- printTimestamp(tab, timestamp));
- }
- // display the certificate(s)
- for (Certificate c : certs) {
- System.out.println(
- printCert(tab, c, true, now));
- }
+ // When -certs provided, display info has extra empty
+ // lines at the beginning and end.
+ if (isSigned) {
+ if (showcerts) sb.append('\n');
+ for (CodeSigner signer: signers) {
+ // signerInfo() must be called even if -verbose
+ // not provided. The method updates various
+ // warning flags.
+ String si = signerInfo(signer, tab, now);
+ if (showcerts) {
+ sb.append(si);
+ sb.append('\n');
}
- System.out.println();
}
-
- }
- if (isSigned) {
- for (int i = 0; i < signers.length; i++) {
- Certificate cert =
- signers[i].getSignerCertPath()
- .getCertificates().get(0);
- if (cert instanceof X509Certificate) {
- checkCertUsage((X509Certificate)cert, null);
- if (!showcerts) {
- long notAfter = ((X509Certificate)cert)
- .getNotAfter().getTime();
-
- if (notAfter < now) {
- hasExpiredCert = true;
- } else if (notAfter < now + SIX_MONTHS) {
- hasExpiringCert = true;
- }
- }
- }
+ } else if (showcerts && !verbose.equals("all")) {
+ // Print no info for unsigned entries when -verbose:all,
+ // to be consistent with old behavior.
+ if (signatureRelated(name)) {
+ sb.append("\n" + tab + rb.getString(
+ "(Signature related entries)") + "\n\n");
+ } else {
+ sb.append("\n" + tab + rb.getString(
+ "(Unsigned entries)") + "\n\n");
}
}
+ if (verbose != null) {
+ String label = sb.toString();
+ if (signatureRelated(name)) {
+ // Entries inside META-INF and other unsigned
+ // entries are grouped separately.
+ label = "-" + label.substring(1);
+ }
+
+ // The label finally contains 2 parts separated by '|':
+ // The legend displayed before the entry names, and
+ // the cert info (if -certs specfied).
+
+ if (!output.containsKey(label)) {
+ output.put(label, new ArrayList<String>());
+ }
+
+ StringBuffer fb = new StringBuffer();
+ String s = Long.toString(je.getSize());
+ for (int i = 6 - s.length(); i > 0; --i) {
+ fb.append(' ');
+ }
+ fb.append(s).append(' ').
+ append(new Date(je.getTime()).toString());
+ fb.append(' ').append(name);
+
+ output.get(label).add(fb.toString());
+ }
}
}
- if (verbose) {
+ if (verbose != null) {
+ for (Entry<String,List<String>> s: output.entrySet()) {
+ List<String> files = s.getValue();
+ String key = s.getKey();
+ if (key.charAt(0) == '-') { // the signature-related group
+ key = ' ' + key.substring(1);
+ }
+ int pipe = key.indexOf('|');
+ if (verbose.equals("all")) {
+ for (String f: files) {
+ System.out.println(key.substring(0, pipe) + f);
+ System.out.printf(key.substring(pipe+1));
+ }
+ } else {
+ if (verbose.equals("grouped")) {
+ for (String f: files) {
+ System.out.println(key.substring(0, pipe) + f);
+ }
+ } else if (verbose.equals("summary")) {
+ System.out.print(key.substring(0, pipe));
+ if (files.size() > 1) {
+ System.out.println(files.get(0) + " " +
+ String.format(rb.getString(
+ "(and %d more)"), files.size()-1));
+ } else {
+ System.out.println(files.get(0));
+ }
+ }
+ System.out.printf(key.substring(pipe+1));
+ }
+ }
System.out.println();
System.out.println(rb.getString(
" s = signature was verified "));
@@ -589,9 +721,12 @@
" k = at least one certificate was found in keystore"));
System.out.println(rb.getString(
" i = at least one certificate was found in identity scope"));
+ if (ckaliases.size() > 0) {
+ System.out.println((
+ " X = not signed by specified alias(es)"));
+ }
System.out.println();
}
-
if (man == null)
System.out.println(rb.getString("no manifest."));
@@ -602,7 +737,8 @@
System.out.println(rb.getString("jar verified."));
if (hasUnsignedEntry || hasExpiredCert || hasExpiringCert ||
badKeyUsage || badExtendedKeyUsage || badNetscapeCertType ||
- notYetValidCert) {
+ notYetValidCert || chainNotValidated ||
+ aliasNotInStore || notSignedByAlias) {
System.out.println();
System.out.println(rb.getString("Warning: "));
@@ -638,14 +774,27 @@
"This jar contains entries whose signer certificate is not yet valid. "));
}
- if (! (verbose && showcerts)) {
+ if (chainNotValidated) {
+ System.out.println(
+ rb.getString("This jar contains entries whose certificate chain is not validated."));
+ }
+
+ if (notSignedByAlias) {
+ System.out.println(
+ rb.getString("This jar contains signed entries which is not signed by the specified alias(es)."));
+ }
+
+ if (aliasNotInStore) {
+ System.out.println(rb.getString("This jar contains signed entries that's not signed by alias in this keystore."));
+ }
+ if (! (verbose != null && showcerts)) {
System.out.println();
System.out.println(rb.getString(
"Re-run with the -verbose and -certs options for more details."));
}
}
}
- System.exit(0);
+ return;
} catch (Exception e) {
System.out.println(rb.getString("jarsigner: ") + e);
if (debug) {
@@ -660,15 +809,6 @@
System.exit(1);
}
- /*
- * Display some details about a certificate:
- *
- * <cert-type> [", " <subject-DN>] [" (" <keystore-entry-alias> ")"]
- */
- String printCert(Certificate c) {
- return printCert("", c, false, 0);
- }
-
private static MessageFormat validityTimeForm = null;
private static MessageFormat notYetTimeForm = null;
private static MessageFormat expiredTimeForm = null;
@@ -679,6 +819,8 @@
*
* [<tab>] <cert-type> [", " <subject-DN>] [" (" <keystore-entry-alias> ")"]
* [<validity-period> | <expiry-warning>]
+ *
+ * Note: no newline character at the end
*/
String printCert(String tab, Certificate c, boolean checkValidityPeriod,
long now) {
@@ -788,54 +930,75 @@
.append(signTimeForm.format(source)).append("]").toString();
}
+ private Map<CodeSigner,Integer> cacheForInKS =
+ new IdentityHashMap<CodeSigner,Integer>();
+
+ private int inKeyStoreForOneSigner(CodeSigner signer) {
+ if (cacheForInKS.containsKey(signer)) {
+ return cacheForInKS.get(signer);
+ }
+
+ boolean found = false;
+ int result = 0;
+ List<? extends Certificate> certs = signer.getSignerCertPath().getCertificates();
+ for (Certificate c : certs) {
+ String alias = storeHash.get(c);
+ if (alias != null) {
+ if (alias.startsWith("(")) {
+ result |= IN_KEYSTORE;
+ } else if (alias.startsWith("[")) {
+ result |= IN_SCOPE;
+ }
+ if (ckaliases.contains(alias.substring(1, alias.length() - 1))) {
+ result |= SIGNED_BY_ALIAS;
+ }
+ } else {
+ if (store != null) {
+ try {
+ alias = store.getCertificateAlias(c);
+ } catch (KeyStoreException kse) {
+ // never happens, because keystore has been loaded
+ }
+ if (alias != null) {
+ storeHash.put(c, "(" + alias + ")");
+ found = true;
+ result |= IN_KEYSTORE;
+ }
+ }
+ if (!found && (scope != null)) {
+ Identity id = scope.getIdentity(c.getPublicKey());
+ if (id != null) {
+ result |= IN_SCOPE;
+ storeHash.put(c, "[" + id.getName() + "]");
+ }
+ }
+ if (ckaliases.contains(alias)) {
+ result |= SIGNED_BY_ALIAS;
+ }
+ }
+ }
+ cacheForInKS.put(signer, result);
+ return result;
+ }
+
Hashtable<Certificate, String> storeHash =
new Hashtable<Certificate, String>();
int inKeyStore(CodeSigner[] signers) {
- int result = 0;
if (signers == null)
return 0;
- boolean found = false;
-
- for (int i = 0; i < signers.length; i++) {
- found = false;
- List<? extends Certificate> certs =
- signers[i].getSignerCertPath().getCertificates();
-
- for (Certificate c : certs) {
- String alias = storeHash.get(c);
+ int output = 0;
- if (alias != null) {
- if (alias.startsWith("("))
- result |= IN_KEYSTORE;
- else if (alias.startsWith("["))
- result |= IN_SCOPE;
- } else {
- if (store != null) {
- try {
- alias = store.getCertificateAlias(c);
- } catch (KeyStoreException kse) {
- // never happens, because keystore has been loaded
- }
- if (alias != null) {
- storeHash.put(c, "("+alias+")");
- found = true;
- result |= IN_KEYSTORE;
- }
- }
- if (!found && (scope != null)) {
- Identity id = scope.getIdentity(c.getPublicKey());
- if (id != null) {
- result |= IN_SCOPE;
- storeHash.put(c, "["+id.getName()+"]");
- }
- }
- }
- }
+ for (CodeSigner signer: signers) {
+ int result = inKeyStoreForOneSigner(signer);
+ output |= result;
}
- return result;
+ if (ckaliases.size() > 0 && (output & SIGNED_BY_ALIAS) == 0) {
+ output |= NOT_ALIAS;
+ }
+ return output;
}
void signJar(String jarName, String alias, String[] args)
@@ -1025,7 +1188,7 @@
// manifest file has new length
mfFile = new ZipEntry(JarFile.MANIFEST_NAME);
}
- if (verbose) {
+ if (verbose != null) {
if (mfCreated) {
System.out.println(rb.getString(" adding: ") +
mfFile.getName());
@@ -1076,7 +1239,7 @@
// signature file
zos.putNextEntry(sfFile);
sf.write(zos);
- if (verbose) {
+ if (verbose != null) {
if (zipFile.getEntry(sfFilename) != null) {
System.out.println(rb.getString(" updating: ") +
sfFilename);
@@ -1086,7 +1249,7 @@
}
}
- if (verbose) {
+ if (verbose != null) {
if (tsaUrl != null || tsaCert != null) {
System.out.println(
rb.getString("requesting a signature timestamp"));
@@ -1101,8 +1264,8 @@
System.out.println(rb.getString("TSA location: ") +
certUrl);
}
- System.out.println(
- rb.getString("TSA certificate: ") + printCert(tsaCert));
+ System.out.println(rb.getString("TSA certificate: ") +
+ printCert("", tsaCert, false, 0));
}
if (signingMechanism != null) {
System.out.println(
@@ -1113,7 +1276,7 @@
// signature block file
zos.putNextEntry(bkFile);
block.write(zos);
- if (verbose) {
+ if (verbose != null) {
if (zipFile.getEntry(bkFilename) != null) {
System.out.println(rb.getString(" updating: ") +
bkFilename);
@@ -1140,7 +1303,7 @@
ZipEntry ze = enum_.nextElement();
if (!ze.getName().startsWith(META_INF)) {
- if (verbose) {
+ if (verbose != null) {
if (manifest.getAttributes(ze.getName()) != null)
System.out.println(rb.getString(" signing: ") +
ze.getName());
@@ -1194,7 +1357,8 @@
}
if (hasExpiredCert || hasExpiringCert || notYetValidCert
- || badKeyUsage || badExtendedKeyUsage || badNetscapeCertType) {
+ || badKeyUsage || badExtendedKeyUsage
+ || badNetscapeCertType || chainNotValidated) {
System.out.println();
System.out.println(rb.getString("Warning: "));
@@ -1223,6 +1387,11 @@
System.out.println(
rb.getString("The signer certificate is not yet valid."));
}
+
+ if (chainNotValidated) {
+ System.out.println(
+ rb.getString("The signer's certificate chain is not validated."));
+ }
}
// no IOException thrown in the above try clause, so disable
@@ -1274,6 +1443,40 @@
return false;
}
+ Map<CodeSigner,String> cacheForSignerInfo = new IdentityHashMap<CodeSigner,String>();
+
+ /**
+ * Returns a string of singer info, with a newline at the end
+ */
+ private String signerInfo(CodeSigner signer, String tab, long now) {
+ if (cacheForSignerInfo.containsKey(signer)) {
+ return cacheForSignerInfo.get(signer);
+ }
+ StringBuffer s = new StringBuffer();
+ List<? extends Certificate> certs = signer.getSignerCertPath().getCertificates();
+ // display the signature timestamp, if present
+ Timestamp timestamp = signer.getTimestamp();
+ if (timestamp != null) {
+ s.append(printTimestamp(tab, timestamp));
+ }
+ // display the certificate(s)
+ for (Certificate c : certs) {
+ s.append(printCert(tab, c, true, now));
+ s.append('\n');
+ }
+ try {
+ CertPath cp = certificateFactory.generateCertPath(certs);
+ validator.validate(cp, pkixParameters);
+ } catch (Exception e) {
+ chainNotValidated = true;
+ s.append(tab + rb.getString("[CertPath not validated: ") +
+ e.getLocalizedMessage() + "]\n"); // TODO
+ }
+ String result = s.toString();
+ cacheForSignerInfo.put(signer, result);
+ return result;
+ }
+
private void writeEntry(ZipFile zf, ZipOutputStream os, ZipEntry ze)
throws IOException
{
@@ -1360,6 +1563,48 @@
}
}
}
+ Set<TrustAnchor> tas = new HashSet<TrustAnchor>();
+ try {
+ KeyStore caks = KeyTool.getCacertsKeyStore();
+ if (caks != null) {
+ Enumeration<String> aliases = caks.aliases();
+ while (aliases.hasMoreElements()) {
+ String a = aliases.nextElement();
+ try {
+ tas.add(new TrustAnchor((X509Certificate)caks.getCertificate(a), null));
+ } catch (Exception e2) {
+ // ignore, when a SecretkeyEntry does not include a cert
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Ignore, if cacerts cannot be loaded
+ }
+ if (store != null) {
+ Enumeration<String> aliases = store.aliases();
+ while (aliases.hasMoreElements()) {
+ String a = aliases.nextElement();
+ try {
+ X509Certificate c = (X509Certificate)store.getCertificate(a);
+ // Only add TrustedCertificateEntry and self-signed
+ // PrivateKeyEntry
+ if (store.isCertificateEntry(a) ||
+ c.getSubjectDN().equals(c.getIssuerDN())) {
+ tas.add(new TrustAnchor(c, null));
+ }
+ } catch (Exception e2) {
+ // ignore, when a SecretkeyEntry does not include a cert
+ }
+ }
+ }
+ certificateFactory = CertificateFactory.getInstance("X.509");
+ validator = CertPathValidator.getInstance("PKIX");
+ try {
+ pkixParameters = new PKIXParameters(tas);
+ pkixParameters.setRevocationEnabled(false);
+ } catch (InvalidAlgorithmParameterException ex) {
+ // Only if tas is empty
+ }
} catch (IOException ioe) {
throw new RuntimeException(rb.getString("keystore load: ") +
ioe.getMessage());
@@ -1408,7 +1653,8 @@
void checkCertUsage(X509Certificate userCert, boolean[] bad) {
// Can act as a signer?
- // 1. if KeyUsage, then [0] should be true
+ // 1. if KeyUsage, then [0:digitalSignature] or
+ // [1:nonRepudiation] should be true
// 2. if ExtendedKeyUsage, then should contains ANY or CODE_SIGNING
// 3. if NetscapeCertType, then should contains OBJECT_SIGNING
// 1,2,3 must be true
@@ -1419,10 +1665,10 @@
boolean[] keyUsage = userCert.getKeyUsage();
if (keyUsage != null) {
- if (keyUsage.length < 1 || !keyUsage[0]) {
+ keyUsage = Arrays.copyOf(keyUsage, 9);
+ if (!keyUsage[0] && !keyUsage[1]) {
if (bad != null) {
bad[0] = true;
- } else {
badKeyUsage = true;
}
}
@@ -1435,7 +1681,6 @@
&& !xKeyUsage.contains("1.3.6.1.5.5.7.3.3")) { // codeSigning
if (bad != null) {
bad[1] = true;
- } else {
badExtendedKeyUsage = true;
}
}
@@ -1462,7 +1707,6 @@
if (!val) {
if (bad != null) {
bad[2] = true;
- } else {
badNetscapeCertType = true;
}
}
@@ -1477,19 +1721,36 @@
Key key = null;
try {
-
java.security.cert.Certificate[] cs = null;
-
- try {
- cs = store.getCertificateChain(alias);
- } catch (KeyStoreException kse) {
- // this never happens, because keystore has been loaded
+ if (altCertChain != null) {
+ try {
+ cs = CertificateFactory.getInstance("X.509").
+ generateCertificates(new FileInputStream(altCertChain)).
+ toArray(new Certificate[0]);
+ } catch (CertificateException ex) {
+ error(rb.getString("Cannot restore certchain from file specified"));
+ } catch (FileNotFoundException ex) {
+ error(rb.getString("File specified by -certchain does not exist"));
+ }
+ } else {
+ try {
+ cs = store.getCertificateChain(alias);
+ } catch (KeyStoreException kse) {
+ // this never happens, because keystore has been loaded
+ }
}
- if (cs == null) {
- MessageFormat form = new MessageFormat(rb.getString
- ("Certificate chain not found for: alias. alias must reference a valid KeyStore key entry containing a private key and corresponding public key certificate chain."));
- Object[] source = {alias, alias};
- error(form.format(source));
+ if (cs == null || cs.length == 0) {
+ if (altCertChain != null) {
+ error(rb.getString
+ ("Certificate chain not found in the file specified."));
+ } else {
+ MessageFormat form = new MessageFormat(rb.getString
+ ("Certificate chain not found for: alias. alias must" +
+ " reference a valid KeyStore key entry containing a" +
+ " private key and corresponding public key certificate chain."));
+ Object[] source = {alias, alias};
+ error(form.format(source));
+ }
}
certChain = new X509Certificate[cs.length];
@@ -1501,56 +1762,15 @@
certChain[i] = (X509Certificate)cs[i];
}
- // order the cert chain if necessary (put user cert first,
- // root-cert last in the chain)
- X509Certificate userCert
- = (X509Certificate)store.getCertificate(alias);
-
- // check validity of signer certificate
- try {
- userCert.checkValidity();
-
- if (userCert.getNotAfter().getTime() <
- System.currentTimeMillis() + SIX_MONTHS) {
-
- hasExpiringCert = true;
- }
- } catch (CertificateExpiredException cee) {
- hasExpiredCert = true;
-
- } catch (CertificateNotYetValidException cnyve) {
- notYetValidCert = true;
- }
-
- checkCertUsage(userCert, null);
+ // We don't meant to print anything, the next call
+ // checks validity and keyUsage etc
+ printCert("", certChain[0], true, 0);
- if (!userCert.equals(certChain[0])) {
- // need to order ...
- X509Certificate[] certChainTmp
- = new X509Certificate[certChain.length];
- certChainTmp[0] = userCert;
- Principal issuer = userCert.getIssuerDN();
- for (int i=1; i<certChain.length; i++) {
- int j;
- // look for the cert whose subject corresponds to the
- // given issuer
- for (j=0; j<certChainTmp.length; j++) {
- if (certChainTmp[j] == null)
- continue;
- Principal subject = certChainTmp[j].getSubjectDN();
- if (issuer.equals(subject)) {
- certChain[i] = certChainTmp[j];
- issuer = certChainTmp[j].getIssuerDN();
- certChainTmp[j] = null;
- break;
- }
- }
- if (j == certChainTmp.length) {
- error(rb.getString("incomplete certificate chain"));
- }
-
- }
- certChain = certChainTmp; // ordered
+ try {
+ CertPath cp = certificateFactory.generateCertPath(Arrays.asList(certChain));
+ validator.validate(cp, pkixParameters);
+ } catch (Exception e) {
+ chainNotValidated = true;
}
try {
--- a/jdk/src/share/classes/sun/security/tools/JarSignerResources.java Thu Mar 26 17:39:42 2009 -0700
+++ b/jdk/src/share/classes/sun/security/tools/JarSignerResources.java Fri Mar 27 11:05:45 2009 +0800
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2005 Sun Microsystems, Inc. All Rights Reserved.
+ * Copyright 2000-2009 Sun Microsystems, Inc. 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
@@ -54,8 +54,8 @@
"If keystore is not password protected, then -storepass and -keypass must not be specified"},
{"Usage: jarsigner [options] jar-file alias",
"Usage: jarsigner [options] jar-file alias"},
- {" jarsigner -verify [options] jar-file",
- " jarsigner -verify [options] jar-file"},
+ {" jarsigner -verify [options] jar-file [alias...]",
+ " jarsigner -verify [options] jar-file [alias...]"},
{"[-keystore <url>] keystore location",
"[-keystore <url>] keystore location"},
{"[-storepass <password>] password for keystore integrity",
@@ -64,6 +64,8 @@
"[-storetype <type>] keystore type"},
{"[-keypass <password>] password for private key (if different)",
"[-keypass <password>] password for private key (if different)"},
+ {"[-certchain <file>] name of alternative certchain file",
+ "[-certchain <file>] name of alternative certchain file"},
{"[-sigfile <file>] name of .SF/.DSA file",
"[-sigfile <file>] name of .SF/.DSA file"},
{"[-signedjar <file>] name of signed JAR file",
@@ -74,8 +76,10 @@
"[-sigalg <algorithm>] name of signature algorithm"},
{"[-verify] verify a signed JAR file",
"[-verify] verify a signed JAR file"},
- {"[-verbose] verbose output when signing/verifying",
- "[-verbose] verbose output when signing/verifying"},
+ {"[-verbose[:suboptions]] verbose output when signing/verifying.",
+ "[-verbose[:suboptions]] verbose output when signing/verifying."},
+ {" suboptions can be all, grouped or summary",
+ " suboptions can be all, grouped or summary"},
{"[-certs] display certificates when verbose and verifying",
"[-certs] display certificates when verbose and verifying"},
{"[-tsa <url>] location of the Timestamping Authority",
@@ -98,10 +102,22 @@
"[-providerClass <class> name of cryptographic service provider's"},
{" [-providerArg <arg>]] ... master class file and constructor argument",
" [-providerArg <arg>]] ... master class file and constructor argument"},
+ {"[-strict] treat warnings as errors",
+ "[-strict] treat warnings as errors"},
+ {"Option lacks argument", "Option lacks argument"},
+ {"Please type jarsigner -help for usage", "Please type jarsigner -help for usage"},
+ {"Please specify jarfile name", "Please specify jarfile name"},
+ {"Please specify alias name", "Please specify alias name"},
+ {"Only one alias can be specified", "Only one alias can be specified"},
+ {"This jar contains signed entries which is not signed by the specified alias(es).",
+ "This jar contains signed entries which is not signed by the specified alias(es)."},
+ {"This jar contains signed entries that's not signed by alias in this keystore.",
+ "This jar contains signed entries that's not signed by alias in this keystore."},
{"s", "s"},
{"m", "m"},
{"k", "k"},
{"i", "i"},
+ {"(and %d more)", "(and %d more)"},
{" s = signature was verified ",
" s = signature was verified "},
{" m = entry is listed in manifest",
@@ -110,7 +126,11 @@
" k = at least one certificate was found in keystore"},
{" i = at least one certificate was found in identity scope",
" i = at least one certificate was found in identity scope"},
+ {" X = not signed by specified alias(es)",
+ " X = not signed by specified alias(es)"},
{"no manifest.", "no manifest."},
+ {"(Signature related entries)","(Signature related entries)"},
+ {"(Unsigned entries)", "(Unsigned entries)"},
{"jar is unsigned. (signatures missing or not parsable)",
"jar is unsigned. (signatures missing or not parsable)"},
{"jar verified.", "jar verified."},
@@ -134,6 +154,12 @@
"unable to instantiate keystore class: "},
{"Certificate chain not found for: alias. alias must reference a valid KeyStore key entry containing a private key and corresponding public key certificate chain.",
"Certificate chain not found for: {0}. {1} must reference a valid KeyStore key entry containing a private key and corresponding public key certificate chain."},
+ {"File specified by -certchain does not exist",
+ "File specified by -certchain does not exist"},
+ {"Cannot restore certchain from file specified",
+ "Cannot restore certchain from file specified"},
+ {"Certificate chain not found in the file specified.",
+ "Certificate chain not found in the file specified."},
{"found non-X.509 certificate in signer's chain",
"found non-X.509 certificate in signer's chain"},
{"incomplete certificate chain", "incomplete certificate chain"},
@@ -149,6 +175,7 @@
{"certificate is not valid until",
"certificate is not valid until {0}"},
{"certificate will expire on", "certificate will expire on {0}"},
+ {"[CertPath not validated: ", "[CertPath not validated: "},
{"requesting a signature timestamp",
"requesting a signature timestamp"},
{"TSA location: ", "TSA location: "},
@@ -189,14 +216,18 @@
"The signer certificate's ExtendedKeyUsage extension doesn't allow code signing."},
{"The signer certificate's NetscapeCertType extension doesn't allow code signing.",
"The signer certificate's NetscapeCertType extension doesn't allow code signing."},
- {"This jar contains entries whose signer certificate's KeyUsage extension doesn't allow code signing.",
- "This jar contains entries whose signer certificate's KeyUsage extension doesn't allow code signing."},
- {"This jar contains entries whose signer certificate's ExtendedKeyUsage extension doesn't allow code signing.",
- "This jar contains entries whose signer certificate's ExtendedKeyUsage extension doesn't allow code signing."},
- {"This jar contains entries whose signer certificate's NetscapeCertType extension doesn't allow code signing.",
- "This jar contains entries whose signer certificate's NetscapeCertType extension doesn't allow code signing."},
+ {"This jar contains entries whose signer certificate's KeyUsage extension doesn't allow code signing.",
+ "This jar contains entries whose signer certificate's KeyUsage extension doesn't allow code signing."},
+ {"This jar contains entries whose signer certificate's ExtendedKeyUsage extension doesn't allow code signing.",
+ "This jar contains entries whose signer certificate's ExtendedKeyUsage extension doesn't allow code signing."},
+ {"This jar contains entries whose signer certificate's NetscapeCertType extension doesn't allow code signing.",
+ "This jar contains entries whose signer certificate's NetscapeCertType extension doesn't allow code signing."},
{"[{0} extension does not support code signing]",
"[{0} extension does not support code signing]"},
+ {"The signer's certificate chain is not validated.",
+ "The signer's certificate chain is not validated."},
+ {"This jar contains entries whose certificate chain is not validated.",
+ "This jar contains entries whose certificate chain is not validated."},
};
/**
--- a/jdk/src/share/classes/sun/security/tools/KeyTool.java Thu Mar 26 17:39:42 2009 -0700
+++ b/jdk/src/share/classes/sun/security/tools/KeyTool.java Fri Mar 27 11:05:45 2009 +0800
@@ -3108,7 +3108,7 @@
/**
* Returns the keystore with the configured CA certificates.
*/
- private KeyStore getCacertsKeyStore()
+ public static KeyStore getCacertsKeyStore()
throws Exception
{
String sep = File.separator;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/sun/security/tools/jarsigner/concise_jarsigner.sh Fri Mar 27 11:05:45 2009 +0800
@@ -0,0 +1,200 @@
+#
+# Copyright 2009 Sun Microsystems, Inc. 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+# CA 95054 USA or visit www.sun.com if you need additional information or
+# have any questions.
+#
+
+# @test
+# @bug 6802846
+# @summary jarsigner needs enhanced cert validation(options)
+#
+# @run shell concise_jarsigner.sh
+#
+
+if [ "${TESTJAVA}" = "" ] ; then
+ JAVAC_CMD=`which javac`
+ TESTJAVA=`dirname $JAVAC_CMD`/..
+fi
+
+# set platform-dependent variables
+OS=`uname -s`
+case "$OS" in
+ Windows_* )
+ FS="\\"
+ ;;
+ * )
+ FS="/"
+ ;;
+esac
+
+KT="$TESTJAVA${FS}bin${FS}keytool -storepass changeit -keypass changeit -keystore js.jks"
+JAR=$TESTJAVA${FS}bin${FS}jar
+JARSIGNER=$TESTJAVA${FS}bin${FS}jarsigner
+JAVAC=$TESTJAVA${FS}bin${FS}javac
+
+rm js.jks
+
+echo class A1 {} > A1.java
+echo class A2 {} > A2.java
+echo class A3 {} > A3.java
+echo class A4 {} > A4.java
+echo class A5 {} > A5.java
+echo class A6 {} > A6.java
+
+$JAVAC A1.java A2.java A3.java A4.java A5.java A6.java
+YEAR=`date +%Y`
+
+# ==========================================================
+# First part: output format
+# ==========================================================
+
+$KT -genkeypair -alias a1 -dname CN=a1 -validity 365
+$KT -genkeypair -alias a2 -dname CN=a2 -validity 365
+
+# a.jar includes 8 unsigned, 2 signed by a1 and a2, 2 signed by a3
+$JAR cvf a.jar A1.class A2.class
+$JARSIGNER -keystore js.jks -storepass changeit a.jar a1
+$JAR uvf a.jar A3.class A4.class
+$JARSIGNER -keystore js.jks -storepass changeit a.jar a2
+$JAR uvf a.jar A5.class A6.class
+
+# Verify OK
+$JARSIGNER -verify a.jar
+[ $? = 0 ] || exit $LINENO
+
+# 4(chainNotValidated)+16(hasUnsignedEntry)+32(aliasNotInStore)
+$JARSIGNER -verify a.jar -strict
+[ $? = 52 ] || exit $LINENO
+
+# 16(hasUnsignedEntry)
+$JARSIGNER -verify a.jar -strict -keystore js.jks
+[ $? = 16 ] || exit $LINENO
+
+# 16(hasUnsignedEntry)+32(notSignedByAlias)
+$JARSIGNER -verify a.jar a1 -strict -keystore js.jks
+[ $? = 48 ] || exit $LINENO
+
+# 16(hasUnsignedEntry)
+$JARSIGNER -verify a.jar a1 a2 -strict -keystore js.jks
+[ $? = 16 ] || exit $LINENO
+
+# 12 entries all together
+LINES=`$JARSIGNER -verify a.jar -verbose | grep $YEAR | wc -l`
+[ $LINES = 12 ] || exit $LINENO
+
+# 12 entries all listed
+LINES=`$JARSIGNER -verify a.jar -verbose:grouped | grep $YEAR | wc -l`
+[ $LINES = 12 ] || exit $LINENO
+
+# 3 groups: unrelated, signed, unsigned
+LINES=`$JARSIGNER -verify a.jar -verbose:summary | grep $YEAR | wc -l`
+[ $LINES = 3 ] || exit $LINENO
+
+# 4 groups: unrelated, signed by a1/a2, signed by a2, unsigned
+LINES=`$JARSIGNER -verify a.jar -verbose:summary -certs | grep $YEAR | wc -l`
+[ $LINES = 4 ] || exit $LINENO
+
+# 2*2 for A1/A2, 2 for A3/A4
+LINES=`$JARSIGNER -verify a.jar -verbose -certs | grep "\[certificate" | wc -l`
+[ $LINES = 6 ] || exit $LINENO
+
+# a1,a2 for A1/A2, a2 for A3/A4
+LINES=`$JARSIGNER -verify a.jar -verbose:grouped -certs | grep "\[certificate" | wc -l`
+[ $LINES = 3 ] || exit $LINENO
+
+# a1,a2 for A1/A2, a2 for A3/A4
+LINES=`$JARSIGNER -verify a.jar -verbose:summary -certs | grep "\[certificate" | wc -l`
+[ $LINES = 3 ] || exit $LINENO
+
+# 4 groups
+LINES=`$JARSIGNER -verify a.jar -verbose:summary -certs | grep "more)" | wc -l`
+[ $LINES = 4 ] || exit $LINENO
+
+# ==========================================================
+# Second part: exit code 2, 4, 8
+# 16 and 32 already covered in the first part
+# ==========================================================
+
+$KT -genkeypair -alias expiring -dname CN=expiring -startdate -1m
+$KT -genkeypair -alias expired -dname CN=expired -startdate -10m
+$KT -genkeypair -alias notyetvalid -dname CN=notyetvalid -startdate +1m
+$KT -genkeypair -alias badku -dname CN=badku -ext KU=cRLSign -validity 365
+$KT -genkeypair -alias badeku -dname CN=badeku -ext EKU=sa -validity 365
+$KT -genkeypair -alias goodku -dname CN=goodku -ext KU=dig -validity 365
+$KT -genkeypair -alias goodeku -dname CN=goodeku -ext EKU=codesign -validity 365
+
+# badchain signed by ca, but ca is removed later
+$KT -genkeypair -alias badchain -dname CN=badchain -validity 365
+$KT -genkeypair -alias ca -dname CN=ca -ext bc -validity 365
+$KT -certreq -alias badchain | $KT -gencert -alias ca -validity 365 | \
+ $KT -importcert -alias badchain
+$KT -delete -alias ca
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar expiring
+[ $? = 2 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar expired
+[ $? = 4 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar notyetvalid
+[ $? = 4 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar badku
+[ $? = 8 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar badeku
+[ $? = 8 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar goodku
+[ $? = 0 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar goodeku
+[ $? = 0 ] || exit $LINENO
+
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar badchain
+[ $? = 4 ] || exit $LINENO
+
+$JARSIGNER -verify a.jar
+[ $? = 0 ] || exit $LINENO
+
+# ==========================================================
+# Third part: -certchain test
+# ==========================================================
+
+# altchain signed by ca2, but ca2 is removed later
+$KT -genkeypair -alias altchain -dname CN=altchain -validity 365
+$KT -genkeypair -alias ca2 -dname CN=ca2 -ext bc -validity 365
+$KT -certreq -alias altchain | $KT -gencert -alias ca2 -validity 365 -rfc > certchain
+$KT -exportcert -alias ca2 -rfc >> certchain
+$KT -delete -alias ca2
+
+# Now altchain is still self-signed
+$JARSIGNER -strict -keystore js.jks -storepass changeit a.jar altchain
+[ $? = 0 ] || exit $LINENO
+
+# If -certchain is used, then it's bad
+$JARSIGNER -strict -keystore js.jks -storepass changeit -certchain certchain a.jar altchain
+[ $? = 4 ] || exit $LINENO
+
+$JARSIGNER -verify a.jar
+[ $? = 0 ] || exit $LINENO
+
+echo OK
+exit 0