8180289: jarsigner treats timestamped signed jar invalid after the signer cert expires
Reviewed-by: mullan
--- a/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java Fri Oct 27 21:10:56 2017 +0800
+++ b/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java Fri Oct 27 21:11:15 2017 +0800
@@ -724,7 +724,8 @@
if (signers == null) {
signers = new ArrayList<>();
}
- // Append the new code signer
+ // Append the new code signer. If timestamp is invalid, this
+ // jar will be treated as unsigned.
signers.add(new CodeSigner(certChain, info.getTimestamp()));
if (debug != null) {
--- a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java Fri Oct 27 21:10:56 2017 +0800
+++ b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java Fri Oct 27 21:11:15 2017 +0800
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2017, 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
@@ -162,11 +162,20 @@
private boolean noTimestamp = false;
private Date expireDate = new Date(0L); // used in noTimestamp warning
- // Severe warnings
+ // Severe warnings.
+
+ // jarsigner used to check signer cert chain validity and key usages
+ // itself and set various warnings. Later CertPath validation is
+ // added but chainNotValidated is only flagged when no other existing
+ // warnings are set. TSA cert chain check is added separately and
+ // only tsaChainNotValidated is set, i.e. has no affect on hasExpiredCert,
+ // notYetValidCert, or any badXyzUsage.
+
private int weakAlg = 0; // 1. digestalg, 2. sigalg, 4. tsadigestalg
private boolean hasExpiredCert = false;
private boolean notYetValidCert = false;
private boolean chainNotValidated = false;
+ private boolean tsaChainNotValidated = false;
private boolean notSignedByAlias = false;
private boolean aliasNotInStore = false;
private boolean hasUnsignedEntry = false;
@@ -176,6 +185,7 @@
private boolean signerSelfSigned = false;
private Throwable chainNotValidatedReason = null;
+ private Throwable tsaChainNotValidatedReason = null;
private boolean seeWeak = false;
@@ -266,7 +276,8 @@
if (strict) {
int exitCode = 0;
- if (weakAlg != 0 || chainNotValidated || hasExpiredCert || notYetValidCert || signerSelfSigned) {
+ if (weakAlg != 0 || chainNotValidated
+ || hasExpiredCert || notYetValidCert || signerSelfSigned) {
exitCode |= 4;
}
if (badKeyUsage || badExtendedKeyUsage || badNetscapeCertType) {
@@ -278,6 +289,9 @@
if (notSignedByAlias || aliasNotInStore) {
exitCode |= 32;
}
+ if (tsaChainNotValidated) {
+ exitCode |= 64;
+ }
if (exitCode != 0) {
System.exit(exitCode);
}
@@ -864,6 +878,9 @@
signerSelfSigned = false;
}
+ // If there is a time stamp block inside the PKCS7 block file
+ boolean hasTimestampBlock = false;
+
// Even if the verbose option is not specified, all out strings
// must be generated so seeWeak can be updated.
if (!digestMap.isEmpty()
@@ -892,6 +909,7 @@
PublicKey key = signer.getPublicKey();
PKCS7 tsToken = si.getTsToken();
if (tsToken != null) {
+ hasTimestampBlock = true;
SignerInfo tsSi = tsToken.getSignerInfos()[0];
X509Certificate tsSigner = tsSi.getCertificate(tsToken);
byte[] encTsTokenInfo = tsToken.getContentInfo().getData();
@@ -967,7 +985,7 @@
if (badKeyUsage || badExtendedKeyUsage || badNetscapeCertType ||
notYetValidCert || chainNotValidated || hasExpiredCert ||
hasUnsignedEntry || signerSelfSigned || (weakAlg != 0) ||
- aliasNotInStore || notSignedByAlias) {
+ aliasNotInStore || notSignedByAlias || tsaChainNotValidated) {
if (strict) {
System.out.println(rb.getString("jar.verified.with.signer.errors."));
@@ -1019,10 +1037,16 @@
if (chainNotValidated) {
System.out.println(String.format(
- rb.getString("This.jar.contains.entries.whose.certificate.chain.is.not.validated.reason.1"),
+ rb.getString("This.jar.contains.entries.whose.certificate.chain.is.invalid.reason.1"),
chainNotValidatedReason.getLocalizedMessage()));
}
+ if (tsaChainNotValidated) {
+ System.out.println(String.format(
+ rb.getString("This.jar.contains.entries.whose.tsa.certificate.chain.is.invalid.reason.1"),
+ tsaChainNotValidatedReason.getLocalizedMessage()));
+ }
+
if (notSignedByAlias) {
System.out.println(
rb.getString("This.jar.contains.signed.entries.which.is.not.signed.by.the.specified.alias.es."));
@@ -1050,8 +1074,15 @@
"This.jar.contains.entries.whose.signer.certificate.will.expire.within.six.months."));
}
if (noTimestamp) {
- System.out.println(
- String.format(rb.getString("no.timestamp.verifying"), expireDate));
+ if (hasTimestampBlock) {
+ // JarSigner API has not seen the timestamp,
+ // might have ignored it due to weak alg, etc.
+ System.out.println(
+ String.format(rb.getString("bad.timestamp.verifying"), expireDate));
+ } else {
+ System.out.println(
+ String.format(rb.getString("no.timestamp.verifying"), expireDate));
+ }
}
}
if (warningAppeared || errorAppeared) {
@@ -1106,16 +1137,23 @@
private static MessageFormat expiredTimeForm = null;
private static MessageFormat expiringTimeForm = null;
- /*
- * Display some details about a certificate:
+ /**
+ * Returns a string about a certificate:
*
* [<tab>] <cert-type> [", " <subject-DN>] [" (" <keystore-entry-alias> ")"]
* [<validity-period> | <expiry-warning>]
+ * [<key-usage-warning>]
*
- * Note: no newline character at the end
+ * Note: no newline character at the end.
+ *
+ * When isTsCert is true, this method sets global flags like hasExpiredCert,
+ * notYetValidCert, badKeyUsage, badExtendedKeyUsage, badNetscapeCertType.
+ *
+ * @param isTsCert true if c is in the TSA cert chain, false otherwise.
+ * @param checkUsage true to check code signer keyUsage
*/
- String printCert(String tab, Certificate c, boolean checkValidityPeriod,
- Date timestamp, boolean checkUsage) {
+ String printCert(boolean isTsCert, String tab, Certificate c,
+ Date timestamp, boolean checkUsage) throws Exception {
StringBuilder certStr = new StringBuilder();
String space = rb.getString("SPACE");
@@ -1135,7 +1173,7 @@
certStr.append(space).append(alias);
}
- if (checkValidityPeriod && x509Cert != null) {
+ if (x509Cert != null) {
certStr.append("\n").append(tab).append("[");
Date notAfter = x509Cert.getNotAfter();
@@ -1148,7 +1186,7 @@
x509Cert.checkValidity();
// test if cert will expire within six months
if (notAfter.getTime() < System.currentTimeMillis() + SIX_MONTHS) {
- hasExpiringCert = true;
+ if (!isTsCert) hasExpiringCert = true;
if (expiringTimeForm == null) {
expiringTimeForm = new MessageFormat(
rb.getString("certificate.will.expire.on"));
@@ -1169,7 +1207,7 @@
certStr.append(validityTimeForm.format(source));
}
} catch (CertificateExpiredException cee) {
- hasExpiredCert = true;
+ if (!isTsCert) hasExpiredCert = true;
if (expiredTimeForm == null) {
expiredTimeForm = new MessageFormat(
@@ -1179,7 +1217,7 @@
certStr.append(expiredTimeForm.format(source));
} catch (CertificateNotYetValidException cnyve) {
- notYetValidCert = true;
+ if (!isTsCert) notYetValidCert = true;
if (notYetTimeForm == null) {
notYetTimeForm = new MessageFormat(
@@ -1398,7 +1436,7 @@
System.out.println(rb.getString("TSA.location.") + tsaUrl);
} else if (tsaCert != null) {
System.out.println(rb.getString("TSA.certificate.") +
- printCert("", tsaCert, false, null, false));
+ printCert(true, "", tsaCert, null, false));
}
}
builder.tsa(tsaURI);
@@ -1458,6 +1496,30 @@
}
}
+ // The JarSigner API always accepts the timestamp received.
+ // We need to extract the certs from the signed jar to
+ // validate it.
+ if (!noTimestamp) {
+ try (JarFile check = new JarFile(signedJarFile)) {
+ PKCS7 p7 = new PKCS7(check.getInputStream(check.getEntry(
+ "META-INF/" + sigfile + "." + privateKey.getAlgorithm())));
+ SignerInfo si = p7.getSignerInfos()[0];
+ PKCS7 tsToken = si.getTsToken();
+ SignerInfo tsSi = tsToken.getSignerInfos()[0];
+ try {
+ validateCertChain(Validator.VAR_TSA_SERVER,
+ tsSi.getCertificateChain(tsToken), null);
+ } catch (Exception e) {
+ tsaChainNotValidated = true;
+ tsaChainNotValidatedReason = e;
+ }
+ } catch (Exception e) {
+ if (debug) {
+ e.printStackTrace();
+ }
+ }
+ }
+
// no IOException thrown in the follow try clause, so disable
// the try clause.
// try {
@@ -1487,8 +1549,10 @@
}
boolean warningAppeared = false;
- if (weakAlg != 0 || badKeyUsage || badExtendedKeyUsage || badNetscapeCertType ||
- notYetValidCert || chainNotValidated || hasExpiredCert || signerSelfSigned) {
+ if (weakAlg != 0 || badKeyUsage || badExtendedKeyUsage
+ || badNetscapeCertType || notYetValidCert
+ || chainNotValidated || tsaChainNotValidated
+ || hasExpiredCert || signerSelfSigned) {
if (strict) {
System.out.println(rb.getString("jar.signed.with.signer.errors."));
System.out.println();
@@ -1525,10 +1589,16 @@
if (chainNotValidated) {
System.out.println(String.format(
- rb.getString("The.signer.s.certificate.chain.is.not.validated.reason.1"),
+ rb.getString("The.signer.s.certificate.chain.is.invalid.reason.1"),
chainNotValidatedReason.getLocalizedMessage()));
}
+ if (tsaChainNotValidated) {
+ System.out.println(String.format(
+ rb.getString("The.tsa.certificate.chain.is.invalid.reason.1"),
+ tsaChainNotValidatedReason.getLocalizedMessage()));
+ }
+
if (signerSelfSigned) {
System.out.println(
rb.getString("The.signer.s.certificate.is.self.signed."));
@@ -1600,7 +1670,7 @@
/**
* Returns a string of singer info, with a newline at the end
*/
- private String signerInfo(CodeSigner signer, String tab) {
+ private String signerInfo(CodeSigner signer, String tab) throws Exception {
if (cacheForSignerInfo.containsKey(signer)) {
return cacheForSignerInfo.get(signer);
}
@@ -1620,18 +1690,35 @@
// display the certificate(sb). The first one is end-entity cert and
// its KeyUsage should be checked.
boolean first = true;
+ sb.append(tab).append(rb.getString("...Signer")).append('\n');
for (Certificate c : certs) {
- sb.append(printCert(tab, c, true, timestamp, first));
+ sb.append(printCert(false, tab, c, timestamp, first));
sb.append('\n');
first = false;
}
try {
- validateCertChain(certs);
+ validateCertChain(Validator.VAR_CODE_SIGNING, certs, ts);
} catch (Exception e) {
chainNotValidated = true;
chainNotValidatedReason = e;
- sb.append(tab).append(rb.getString(".CertPath.not.validated."))
- .append(e.getLocalizedMessage()).append("]\n"); // TODO
+ sb.append(tab).append(rb.getString(".Invalid.certificate.chain."))
+ .append(e.getLocalizedMessage()).append("]\n");
+ }
+ if (ts != null) {
+ sb.append(tab).append(rb.getString("...TSA")).append('\n');
+ for (Certificate c : ts.getSignerCertPath().getCertificates()) {
+ sb.append(printCert(true, tab, c, timestamp, false));
+ sb.append('\n');
+ }
+ try {
+ validateCertChain(Validator.VAR_TSA_SERVER,
+ ts.getSignerCertPath().getCertificates(), null);
+ } catch (Exception e) {
+ tsaChainNotValidated = true;
+ tsaChainNotValidatedReason = e;
+ sb.append(tab).append(rb.getString(".Invalid.TSA.certificate.chain."))
+ .append(e.getLocalizedMessage()).append("]\n");
+ }
}
if (certs.size() == 1
&& KeyStoreUtil.isSelfSigned((X509Certificate)certs.get(0))) {
@@ -1841,7 +1928,7 @@
}
}
- void getAliasInfo(String alias) {
+ void getAliasInfo(String alias) throws Exception {
Key key = null;
@@ -1887,10 +1974,11 @@
// We don't meant to print anything, the next call
// checks validity and keyUsage etc
- printCert("", certChain[0], true, null, true);
+ printCert(false, "", certChain[0], null, true);
try {
- validateCertChain(Arrays.asList(certChain));
+ validateCertChain(Validator.VAR_CODE_SIGNING,
+ Arrays.asList(certChain), null);
} catch (Exception e) {
chainNotValidated = true;
chainNotValidatedReason = e;
@@ -1949,17 +2037,31 @@
System.exit(1);
}
- void validateCertChain(List<? extends Certificate> certs) throws Exception {
+ /**
+ * Validates a cert chain.
+ *
+ * @param parameter this might be a timestamp
+ */
+ void validateCertChain(String variant, List<? extends Certificate> certs,
+ Object parameter)
+ throws Exception {
try {
Validator.getInstance(Validator.TYPE_PKIX,
- Validator.VAR_CODE_SIGNING,
+ variant,
pkixParameters)
- .validate(certs.toArray(new X509Certificate[certs.size()]));
+ .validate(certs.toArray(new X509Certificate[certs.size()]),
+ null, parameter);
} catch (Exception e) {
if (debug) {
e.printStackTrace();
}
- if (e instanceof ValidatorException) {
+
+ // Exception might be dismissed if another warning flag
+ // is already set by printCert. This is only done for
+ // code signing certs.
+
+ if (variant.equals(Validator.VAR_CODE_SIGNING) &&
+ e instanceof ValidatorException) {
// Throw cause if it's CertPathValidatorException,
if (e.getCause() != null &&
e.getCause() instanceof CertPathValidatorException) {
--- a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java Fri Oct 27 21:10:56 2017 +0800
+++ b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java Fri Oct 27 21:11:15 2017 +0800
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2017, 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
@@ -207,7 +207,8 @@
{"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: "},
+ {".Invalid.certificate.chain.", "[Invalid certificate chain: "},
+ {".Invalid.TSA.certificate.chain.", "[Invalid TSA certificate chain: "},
{"requesting.a.signature.timestamp",
"requesting a signature timestamp"},
{"TSA.location.", "TSA location: "},
@@ -224,6 +225,8 @@
{"entry.was.signed.on", "entry was signed on {0}"},
{"Warning.", "Warning: "},
{"Error.", "Error: "},
+ {"...Signer", ">>> Signer"},
+ {"...TSA", ">>> TSA"},
{"This.jar.contains.unsigned.entries.which.have.not.been.integrity.checked.",
"This jar contains unsigned entries which have not been integrity-checked. "},
{"This.jar.contains.entries.whose.signer.certificate.has.expired.",
@@ -258,20 +261,26 @@
"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.reason.1",
- "The signer's certificate chain is not validated. Reason: %s"},
+ {"The.signer.s.certificate.chain.is.invalid.reason.1",
+ "The signer's certificate chain is invalid. Reason: %s"},
+ {"The.tsa.certificate.chain.is.invalid.reason.1",
+ "The TSA certificate chain is invalid. Reason: %s"},
{"The.signer.s.certificate.is.self.signed.",
"The signer's certificate is self-signed."},
{"The.1.algorithm.specified.for.the.2.option.is.considered.a.security.risk.",
"The %1$s algorithm specified for the %2$s option is considered a security risk."},
{"The.1.signing.key.has.a.keysize.of.2.which.is.considered.a.security.risk.",
"The %s signing key has a keysize of %d which is considered a security risk."},
- {"This.jar.contains.entries.whose.certificate.chain.is.not.validated.reason.1",
- "This jar contains entries whose certificate chain is not validated. Reason: %s"},
+ {"This.jar.contains.entries.whose.certificate.chain.is.invalid.reason.1",
+ "This jar contains entries whose certificate chain is invalid. Reason: %s"},
+ {"This.jar.contains.entries.whose.tsa.certificate.chain.is.invalid.reason.1",
+ "This jar contains entries whose TSA certificate chain is invalid. Reason: %s"},
{"no.timestamp.signing",
"No -tsa or -tsacert is provided and this jar is not timestamped. Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date (%1$tY-%1$tm-%1$td)."},
{"no.timestamp.verifying",
"This jar contains signatures that do not include a timestamp. Without a timestamp, users may not be able to validate this jar after any of the signer certificates expire (as early as %1$tY-%1$tm-%1$td)."},
+ {"bad.timestamp.verifying",
+ "This jar contains signatures that include an invalid timestamp. Without a valid timestamp, users may not be able to validate this jar after any of the signer certificates expire (as early as %1$tY-%1$tm-%1$td).\nRerun jarsigner with -J-Djava.security.debug=jar for more information."},
{"Unknown.password.type.", "Unknown password type: "},
{"Cannot.find.environment.variable.",
"Cannot find environment variable: "},
--- a/test/jdk/sun/security/tools/jarsigner/TimestampCheck.java Fri Oct 27 21:10:56 2017 +0800
+++ b/test/jdk/sun/security/tools/jarsigner/TimestampCheck.java Fri Oct 27 21:11:15 2017 +0800
@@ -22,6 +22,8 @@
*/
import com.sun.net.httpserver.*;
+
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@@ -35,16 +37,17 @@
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.List;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import jdk.test.lib.SecurityTools;
-import jdk.testlibrary.*;
+import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.util.JarUtils;
import sun.security.pkcs.ContentInfo;
import sun.security.pkcs.PKCS7;
@@ -59,7 +62,7 @@
/*
* @test
- * @bug 6543842 6543440 6939248 8009636 8024302 8163304 8169911
+ * @bug 6543842 6543440 6939248 8009636 8024302 8163304 8169911 8180289
* @summary checking response of timestamp
* @modules java.base/sun.security.pkcs
* java.base/sun.security.timestamp
@@ -123,12 +126,12 @@
byte[] sign(byte[] input, String path) throws Exception {
DerValue value = new DerValue(input);
- System.err.println("\nIncoming Request\n===================");
- System.err.println("Version: " + value.data.getInteger());
+ System.out.println("\nIncoming Request\n===================");
+ System.out.println("Version: " + value.data.getInteger());
DerValue messageImprint = value.data.getDerValue();
AlgorithmId aid = AlgorithmId.parse(
messageImprint.data.getDerValue());
- System.err.println("AlgorithmId: " + aid);
+ System.out.println("AlgorithmId: " + aid);
ObjectIdentifier policyId = new ObjectIdentifier(defaultPolicyId);
BigInteger nonce = null;
@@ -136,23 +139,22 @@
DerValue v = value.data.getDerValue();
if (v.tag == DerValue.tag_Integer) {
nonce = v.getBigInteger();
- System.err.println("nonce: " + nonce);
+ System.out.println("nonce: " + nonce);
} else if (v.tag == DerValue.tag_Boolean) {
- System.err.println("certReq: " + v.getBoolean());
+ System.out.println("certReq: " + v.getBoolean());
} else if (v.tag == DerValue.tag_ObjectId) {
policyId = v.getOID();
- System.err.println("PolicyID: " + policyId);
+ System.out.println("PolicyID: " + policyId);
}
}
- System.err.println("\nResponse\n===================");
+ System.out.println("\nResponse\n===================");
KeyStore ks = KeyStore.getInstance(
new File(keystore), "changeit".toCharArray());
- String alias = "ts";
- if (path.startsWith("bad") || path.equals("weak")) {
- alias = "ts" + path;
- }
+ // If path starts with "ts", use the TSA it points to.
+ // Otherwise, always use "ts".
+ String alias = path.startsWith("ts") ? path : "ts";
if (path.equals("diffpolicy")) {
policyId = new ObjectIdentifier(defaultPolicyId);
@@ -199,8 +201,11 @@
tst.putInteger(1);
- Calendar cal = Calendar.getInstance();
- tst.putGeneralizedTime(cal.getTime());
+ Instant instant = Instant.now();
+ if (path.equals("tsold")) {
+ instant = instant.minus(20, ChronoUnit.DAYS);
+ }
+ tst.putGeneralizedTime(Date.from(instant));
if (path.equals("diffnonce")) {
tst.putInteger(1234);
@@ -227,10 +232,10 @@
"1.2.840.113549.1.9.16.1.4"),
new DerValue(tstInfo2.toByteArray()));
- System.err.println("Signing...");
- System.err.println(new X500Name(signer
+ System.out.println("Signing...");
+ System.out.println(new X500Name(signer
.getIssuerX500Principal().getName()));
- System.err.println(signer.getSerialNumber());
+ System.out.println(signer.getSerialNumber());
SignerInfo signerInfo = new SignerInfo(
new X500Name(signer.getIssuerX500Principal().getName()),
@@ -303,23 +308,51 @@
prepare();
- try (Handler tsa = Handler.init(0, "tsks");) {
+ try (Handler tsa = Handler.init(0, "ks");) {
tsa.start();
int port = tsa.getPort();
host = "http://localhost:" + port + "/";
if (args.length == 0) { // Run this test
- sign("none")
+
+ sign("normal")
+ .shouldNotContain("Warning")
+ .shouldHaveExitValue(0);
+
+ verify("normal.jar")
+ .shouldNotContain("Warning")
+ .shouldHaveExitValue(0);
+
+ // Simulate signing at a previous date:
+ // 1. tsold will create a timestamp of 20 days ago.
+ // 2. oldsigner expired 10 days ago.
+ // jarsigner will show a warning at signing.
+ signVerbose("tsold", "unsigned.jar", "tsold.jar", "oldsigner")
+ .shouldHaveExitValue(4);
+
+ // It verifies perfectly.
+ verify("tsold.jar", "-verbose", "-certs")
+ .shouldNotContain("Warning")
+ .shouldHaveExitValue(0);
+
+ signVerbose(null, "unsigned.jar", "none.jar", "signer")
.shouldContain("is not timestamped")
.shouldHaveExitValue(0);
- sign("badku")
- .shouldHaveExitValue(0);
+ signVerbose(null, "unsigned.jar", "badku.jar", "badku")
+ .shouldHaveExitValue(8);
checkBadKU("badku.jar");
- sign("normal")
- .shouldNotContain("is not timestamped")
- .shouldHaveExitValue(0);
+ // 8180289: unvalidated TSA cert chain
+ sign("tsnoca")
+ .shouldContain("TSA certificate chain is invalid")
+ .shouldHaveExitValue(64);
+
+ verify("tsnoca.jar", "-verbose", "-certs")
+ .shouldHaveExitValue(64)
+ .shouldContain("jar verified")
+ .shouldContain("Invalid TSA certificate chain")
+ .shouldContain("TSA certificate chain is invalid");
sign("nononce")
.shouldHaveExitValue(1);
@@ -331,11 +364,11 @@
.shouldHaveExitValue(1);
sign("fullchain")
.shouldHaveExitValue(0); // Success, 6543440 solved.
- sign("bad1")
+ sign("tsbad1")
.shouldHaveExitValue(1);
- sign("bad2")
+ sign("tsbad2")
.shouldHaveExitValue(1);
- sign("bad3")
+ sign("tsbad3")
.shouldHaveExitValue(1);
sign("nocert")
.shouldHaveExitValue(1);
@@ -347,116 +380,173 @@
sign("diffpolicy", "-tsapolicyid", "1.2.3")
.shouldHaveExitValue(1);
- sign("tsaalg", "-tsadigestalg", "SHA")
+ sign("sha1alg", "-tsadigestalg", "SHA")
.shouldHaveExitValue(0);
- checkTimestamp("tsaalg.jar", defaultPolicyId, "SHA-1");
+ checkTimestamp("sha1alg.jar", defaultPolicyId, "SHA-1");
- sign("weak", "-digestalg", "MD5",
+ sign("tsweak", "-digestalg", "MD5",
"-sigalg", "MD5withRSA", "-tsadigestalg", "MD5")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(68)
.shouldMatch("MD5.*-digestalg.*risk")
.shouldMatch("MD5.*-tsadigestalg.*risk")
.shouldMatch("MD5withRSA.*-sigalg.*risk");
- checkWeak("weak.jar");
+ checkWeak("tsweak.jar");
+
+ signVerbose("tsweak", "unsigned.jar", "tsweak2.jar", "signer")
+ .shouldHaveExitValue(64)
+ .shouldContain("TSA certificate chain is invalid");
- signWithAliasAndTsa("halfWeak", "old.jar", "old", "-digestalg", "MD5")
- .shouldHaveExitValue(0);
+ // Weak timestamp is an error and jar treated unsigned
+ verify("tsweak2.jar", "-verbose")
+ .shouldHaveExitValue(16)
+ .shouldContain("treated as unsigned")
+ .shouldMatch("Timestamp.*512.*weak");
+
+ signVerbose("normal", "unsigned.jar", "halfWeak.jar", "signer",
+ "-digestalg", "MD5")
+ .shouldHaveExitValue(4);
checkHalfWeak("halfWeak.jar");
// sign with DSA key
- signWithAliasAndTsa("sign1", "old.jar", "dsakey")
+ signVerbose("normal", "unsigned.jar", "sign1.jar", "dsakey")
.shouldHaveExitValue(0);
// sign with RSAkeysize < 1024
- signWithAliasAndTsa("sign2", "sign1.jar", "weakkeysize")
- .shouldHaveExitValue(0);
+ signVerbose("normal", "sign1.jar", "sign2.jar", "weakkeysize")
+ .shouldHaveExitValue(4);
checkMultiple("sign2.jar");
// When .SF or .RSA is missing or invalid
checkMissingOrInvalidFiles("normal.jar");
+
+ if (Files.exists(Paths.get("ts2.cert"))) {
+ checkInvalidTsaCertKeyUsage();
+ }
} else { // Run as a standalone server
- System.err.println("Press Enter to quit server");
+ System.out.println("Press Enter to quit server");
System.in.read();
}
}
}
+ private static void checkInvalidTsaCertKeyUsage() throws Exception {
+
+ // Hack: Rewrite the TSA cert inside normal.jar into ts2.jar.
+
+ // Both the cert and the serial number must be rewritten.
+ byte[] tsCert = Files.readAllBytes(Paths.get("ts.cert"));
+ byte[] ts2Cert = Files.readAllBytes(Paths.get("ts2.cert"));
+ byte[] tsSerial = getCert(tsCert)
+ .getSerialNumber().toByteArray();
+ byte[] ts2Serial = getCert(ts2Cert)
+ .getSerialNumber().toByteArray();
+
+ byte[] oldBlock;
+ try (JarFile normal = new JarFile("normal.jar")) {
+ oldBlock = normal.getInputStream(
+ normal.getJarEntry("META-INF/SIGNER.RSA")).readAllBytes();
+ }
+
+ JarUtils.updateJar("normal.jar", "ts2.jar",
+ Map.of("META-INF/SIGNER.RSA",
+ updateBytes(updateBytes(oldBlock, tsCert, ts2Cert),
+ tsSerial, ts2Serial)));
+
+ verify("ts2.jar", "-verbose", "-certs")
+ .shouldHaveExitValue(64)
+ .shouldContain("jar verified")
+ .shouldContain("Invalid TSA certificate chain: Extended key usage does not permit use for TSA server");
+ }
+
+ public static X509Certificate getCert(byte[] data)
+ throws CertificateException, IOException {
+ return (X509Certificate)
+ CertificateFactory.getInstance("X.509")
+ .generateCertificate(new ByteArrayInputStream(data));
+ }
+
+ private static byte[] updateBytes(byte[] old, byte[] from, byte[] to) {
+ int pos = 0;
+ while (true) {
+ if (pos + from.length > old.length) {
+ return null;
+ }
+ if (Arrays.equals(Arrays.copyOfRange(old, pos, pos+from.length), from)) {
+ byte[] result = old.clone();
+ System.arraycopy(to, 0, result, pos, from.length);
+ return result;
+ }
+ pos++;
+ }
+ }
+
private static void checkMissingOrInvalidFiles(String s)
throws Throwable {
- JarUtils.updateJar(s, "1.jar", "-", "META-INF/OLD.SF");
+
+ JarUtils.updateJar(s, "1.jar", Map.of("META-INF/SIGNER.SF", Boolean.FALSE));
verify("1.jar", "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
- .shouldContain("Missing signature-related file META-INF/OLD.SF");
- JarUtils.updateJar(s, "2.jar", "-", "META-INF/OLD.RSA");
+ .shouldContain("Missing signature-related file META-INF/SIGNER.SF");
+ JarUtils.updateJar(s, "2.jar", Map.of("META-INF/SIGNER.RSA", Boolean.FALSE));
verify("2.jar", "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
- .shouldContain("Missing block file for signature-related file META-INF/OLD.SF");
- JarUtils.updateJar(s, "3.jar", "META-INF/OLD.SF");
+ .shouldContain("Missing block file for signature-related file META-INF/SIGNER.SF");
+ JarUtils.updateJar(s, "3.jar", Map.of("META-INF/SIGNER.SF", "dummy"));
verify("3.jar", "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
- .shouldContain("Unparsable signature-related file META-INF/OLD.SF");
- JarUtils.updateJar(s, "4.jar", "META-INF/OLD.RSA");
+ .shouldContain("Unparsable signature-related file META-INF/SIGNER.SF");
+ JarUtils.updateJar(s, "4.jar", Map.of("META-INF/SIGNER.RSA", "dummy"));
verify("4.jar", "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
- .shouldContain("Unparsable signature-related file META-INF/OLD.RSA");
+ .shouldContain("Unparsable signature-related file META-INF/SIGNER.RSA");
}
static OutputAnalyzer jarsigner(List<String> extra)
- throws Throwable {
- JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jarsigner")
- .addVMArg("-Duser.language=en")
- .addVMArg("-Duser.country=US")
- .addToolArg("-keystore")
- .addToolArg("tsks")
- .addToolArg("-storepass")
- .addToolArg("changeit");
- for (String s : extra) {
- if (s.startsWith("-J")) {
- launcher.addVMArg(s.substring(2));
- } else {
- launcher.addToolArg(s);
- }
- }
- return ProcessTools.executeCommand(launcher.getCommand());
+ throws Exception {
+ List<String> args = new ArrayList<>(
+ List.of("-keystore", "ks", "-storepass", "changeit"));
+ args.addAll(extra);
+ return SecurityTools.jarsigner(args);
}
static OutputAnalyzer verify(String file, String... extra)
- throws Throwable {
+ throws Exception {
List<String> args = new ArrayList<>();
args.add("-verify");
+ args.add("-strict");
args.add(file);
args.addAll(Arrays.asList(extra));
return jarsigner(args);
}
- static void checkBadKU(String file) throws Throwable {
+ static void checkBadKU(String file) throws Exception {
verify(file)
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
.shouldContain("re-run jarsigner with debug enabled");
verify(file, "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("Signed by")
.shouldContain("treated as unsigned")
.shouldContain("re-run jarsigner with debug enabled");
verify(file, "-J-Djava.security.debug=jar")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("SignatureException: Key usage restricted")
.shouldContain("treated as unsigned")
.shouldContain("re-run jarsigner with debug enabled");
}
- static void checkWeak(String file) throws Throwable {
+ static void checkWeak(String file) throws Exception {
verify(file)
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
.shouldMatch("weak algorithm that is now disabled.")
.shouldMatch("Re-run jarsigner with the -verbose option for more details");
verify(file, "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
.shouldMatch("weak algorithm that is now disabled by")
.shouldMatch("Digest algorithm: .*weak")
@@ -465,14 +555,14 @@
.shouldNotMatch("Timestamp signature algorithm: .*weak.*weak")
.shouldMatch("Timestamp signature algorithm: .*key.*weak");
verify(file, "-J-Djava.security.debug=jar")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldMatch("SignatureException:.*disabled");
// For 8171319: keytool should print out warnings when reading or
// generating cert/cert req using weak algorithms.
// Must call keytool the command, otherwise doPrintCert() might not
// be able to reset "jdk.certpath.disabledAlgorithms".
- String sout = SecurityTools.keytool("-printcert -jarfile weak.jar")
+ String sout = SecurityTools.keytool("-printcert -jarfile " + file)
.stderrShouldContain("The TSA certificate uses a 512-bit RSA key" +
" which is considered a security risk.")
.getStdout();
@@ -481,14 +571,14 @@
}
}
- static void checkHalfWeak(String file) throws Throwable {
+ static void checkHalfWeak(String file) throws Exception {
verify(file)
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
.shouldMatch("weak algorithm that is now disabled.")
.shouldMatch("Re-run jarsigner with the -verbose option for more details");
verify(file, "-verbose")
- .shouldHaveExitValue(0)
+ .shouldHaveExitValue(16)
.shouldContain("treated as unsigned")
.shouldMatch("weak algorithm that is now disabled by")
.shouldMatch("Digest algorithm: .*weak")
@@ -498,7 +588,7 @@
.shouldNotMatch("Timestamp signature algorithm: .*key.*weak");
}
- static void checkMultiple(String file) throws Throwable {
+ static void checkMultiple(String file) throws Exception {
verify(file)
.shouldHaveExitValue(0)
.shouldContain("jar verified");
@@ -515,7 +605,7 @@
static void checkTimestamp(String file, String policyId, String digestAlg)
throws Exception {
try (JarFile jf = new JarFile(file)) {
- JarEntry je = jf.getJarEntry("META-INF/OLD.RSA");
+ JarEntry je = jf.getJarEntry("META-INF/SIGNER.RSA");
try (InputStream is = jf.getInputStream(je)) {
byte[] content = is.readAllBytes();
PKCS7 p7 = new PKCS7(content);
@@ -541,22 +631,32 @@
static int which = 0;
/**
+ * Sign with a TSA path. Always use alias "signer" to sign "unsigned.jar".
+ * The signed jar name is always path.jar.
+ *
* @param extra more args given to jarsigner
*/
static OutputAnalyzer sign(String path, String... extra)
- throws Throwable {
- String alias = path.equals("badku") ? "badku" : "old";
- return signWithAliasAndTsa(path, "old.jar", alias, extra);
+ throws Exception {
+ return signVerbose(
+ path,
+ "unsigned.jar",
+ path + ".jar",
+ "signer",
+ extra);
}
- static OutputAnalyzer signWithAliasAndTsa (String path, String jar,
- String alias, String...extra) throws Throwable {
+ static OutputAnalyzer signVerbose(
+ String path, // TSA URL path
+ String oldJar,
+ String newJar,
+ String alias, // signer
+ String...extra) throws Exception {
which++;
- System.err.println("\n>> Test #" + which + ": " + Arrays.toString(extra));
- List<String> args = List.of("-J-Djava.security.egd=file:/dev/./urandom",
- "-debug", "-signedjar", path + ".jar", jar, alias);
- args = new ArrayList<>(args);
- if (!path.equals("none") && !path.equals("badku")) {
+ System.out.println("\n>> Test #" + which);
+ List<String> args = new ArrayList<>(List.of(
+ "-strict", "-verbose", "-debug", "-signedjar", newJar, oldJar, alias));
+ if (path != null) {
args.add("-tsa");
args.add(host + path);
}
@@ -565,24 +665,50 @@
}
static void prepare() throws Exception {
- JarUtils.createJar("old.jar", "A");
- Files.deleteIfExists(Paths.get("tsks"));
- keytool("-alias ca -genkeypair -ext bc -dname CN=CA");
- keytool("-alias old -genkeypair -dname CN=old");
+ JarUtils.createJar("unsigned.jar", "A");
+ Files.deleteIfExists(Paths.get("ks"));
+ keytool("-alias signer -genkeypair -ext bc -dname CN=signer");
+ keytool("-alias oldsigner -genkeypair -dname CN=oldsigner");
keytool("-alias dsakey -genkeypair -keyalg DSA -dname CN=dsakey");
keytool("-alias weakkeysize -genkeypair -keysize 512 -dname CN=weakkeysize");
keytool("-alias badku -genkeypair -dname CN=badku");
keytool("-alias ts -genkeypair -dname CN=ts");
- keytool("-alias tsweak -genkeypair -keysize 512 -dname CN=tsbad1");
+ keytool("-alias tsold -genkeypair -dname CN=tsold");
+ keytool("-alias tsweak -genkeypair -keysize 512 -dname CN=tsweak");
keytool("-alias tsbad1 -genkeypair -dname CN=tsbad1");
keytool("-alias tsbad2 -genkeypair -dname CN=tsbad2");
keytool("-alias tsbad3 -genkeypair -dname CN=tsbad3");
+ keytool("-alias tsnoca -genkeypair -dname CN=tsnoca");
- gencert("old");
+ // tsnoca's issuer will be removed from keystore later
+ keytool("-alias ca -genkeypair -ext bc -dname CN=CA");
+ gencert("tsnoca", "-ext eku:critical=ts");
+ keytool("-delete -alias ca");
+ keytool("-alias ca -genkeypair -ext bc -dname CN=CA -startdate -40d");
+
+ gencert("signer");
+ gencert("oldsigner", "-startdate -30d -validity 20");
gencert("dsakey");
gencert("weakkeysize");
gencert("badku", "-ext ku:critical=keyAgreement");
gencert("ts", "-ext eku:critical=ts");
+
+
+ // Issue another cert for "ts" with a different EKU.
+ // Length should be the same. Try several times.
+ keytool("-gencert -alias ca -infile ts.req -outfile ts2.cert " +
+ "-ext eku:critical=1.3.6.1.5.5.7.3.9");
+ for (int i = 0; i < 5; i++) {
+ if (Files.size(Paths.get("ts.cert")) != Files.size(Paths.get("ts2.cert"))) {
+ Files.delete(Paths.get("ts2.cert"));
+ System.out.println("Warning: cannot create same length");
+ } else {
+ break;
+ }
+ }
+
+ gencert("tsold", "-ext eku:critical=ts -startdate -40d -validity 45");
+
gencert("tsweak", "-ext eku:critical=ts");
gencert("tsbad1");
gencert("tsbad2", "-ext eku=ts");
@@ -601,7 +727,7 @@
}
static void keytool(String cmd) throws Exception {
- cmd = "-keystore tsks -storepass changeit -keypass changeit " +
+ cmd = "-keystore ks -storepass changeit -keypass changeit " +
"-keyalg rsa -validity 200 " + cmd;
sun.security.tools.keytool.Main.main(cmd.split(" "));
}
--- a/test/jdk/sun/security/tools/jarsigner/Warning.java Fri Oct 27 21:10:56 2017 +0800
+++ b/test/jdk/sun/security/tools/jarsigner/Warning.java Fri Oct 27 21:11:15 2017 +0800
@@ -83,14 +83,14 @@
issueCert("b", "-sigalg MD5withRSA");
run("jarsigner", "a.jar b")
- .shouldMatch("chain is not validated. Reason:.*MD5withRSA");
+ .shouldMatch("chain is invalid. Reason:.*MD5withRSA");
recreateJar();
newCert("c", "-keysize 512");
issueCert("c");
run("jarsigner", "a.jar c")
- .shouldContain("chain is not validated. " +
+ .shouldContain("chain is invalid. " +
"Reason: Algorithm constraints check failed");
recreateJar();
--- a/test/jdk/sun/security/tools/jarsigner/checkusage.sh Fri Oct 27 21:10:56 2017 +0800
+++ b/test/jdk/sun/security/tools/jarsigner/checkusage.sh Fri Oct 27 21:11:15 2017 +0800
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2017, 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
@@ -91,7 +91,7 @@
#[ $RESULT = 0 ] || exit 2
# Test 3: When no keystore is specified, the error is only
-# "chain not validated"
+# "chain invalid"
$JARSIGNER -strict -verify a.jar
RESULT=$?
@@ -99,7 +99,7 @@
#[ $RESULT = 4 ] || exit 3
# Test 4: When unrelated keystore is specified, the error is
-# "chain not validated" and "not alias in keystore"
+# "chain invalid" and "not alias in keystore"
$JARSIGNER -keystore unrelated.jks -strict -verify a.jar
RESULT=$?
--- a/test/jdk/sun/security/tools/jarsigner/warnings/Test.java Fri Oct 27 21:10:56 2017 +0800
+++ b/test/jdk/sun/security/tools/jarsigner/warnings/Test.java Fri Oct 27 21:11:15 2017 +0800
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2017, 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
@@ -63,7 +63,7 @@
static final String CHAIN_NOT_VALIDATED_VERIFYING_WARNING
= "This jar contains entries "
- + "whose certificate chain is not validated.";
+ + "whose certificate chain is invalid.";
static final String ALIAS_NOT_IN_STORE_VERIFYING_WARNING
= "This jar contains signed entries "
@@ -95,7 +95,7 @@
+ "doesn't allow code signing.";
static final String CHAIN_NOT_VALIDATED_SIGNING_WARNING
- = "The signer's certificate chain is not validated.";
+ = "The signer's certificate chain is invalid.";
static final String HAS_EXPIRING_CERT_SIGNING_WARNING
= "The signer certificate will expire within six months.";
--- a/test/jdk/sun/security/tools/jarsigner/weaksize.sh Fri Oct 27 21:10:56 2017 +0800
+++ b/test/jdk/sun/security/tools/jarsigner/weaksize.sh Fri Oct 27 21:11:15 2017 +0800
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2014, 2017, 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
@@ -54,9 +54,9 @@
$JAR cvf a.jar ks
# We always trust a TrustedCertificateEntry
-$JS a.jar ca | grep "chain is not validated" && exit 1
+$JS a.jar ca | grep "chain is invalid" && exit 1
# An end-entity cert must follow algorithm constraints
-$JS a.jar signer | grep "chain is not validated" || exit 2
+$JS a.jar signer | grep "chain is invalid" || exit 2
exit 0
--- a/test/lib/jdk/test/lib/SecurityTools.java Fri Oct 27 21:10:56 2017 +0800
+++ b/test/lib/jdk/test/lib/SecurityTools.java Fri Oct 27 21:11:15 2017 +0800
@@ -52,10 +52,7 @@
launcher.addToolArg(arg);
}
}
- String[] cmds = launcher.getCommand();
- String cmdLine = Arrays.stream(cmds).collect(Collectors.joining(" "));
- System.out.println("Command line: [" + cmdLine + "]");
- return new ProcessBuilder(cmds);
+ return new ProcessBuilder(launcher.getCommand());
}
// keytool
@@ -72,7 +69,7 @@
pb.redirectInput(ProcessBuilder.Redirect.from(new File(RESPONSE_FILE)));
try {
- return ProcessTools.executeProcess(pb);
+ return execute(pb);
} finally {
Files.delete(p);
}
@@ -102,8 +99,21 @@
public static OutputAnalyzer jarsigner(List<String> args)
throws Exception {
- return ProcessTools.executeProcess(
- getProcessBuilder("jarsigner", args));
+ return execute(getProcessBuilder("jarsigner", args));
+ }
+
+ private static OutputAnalyzer execute(ProcessBuilder pb) throws Exception {
+ try {
+ OutputAnalyzer oa = ProcessTools.executeCommand(pb);
+ System.out.println("Exit value: " + oa.getExitValue());
+ return oa;
+ } catch (Throwable t) {
+ if (t instanceof Exception) {
+ throw (Exception) t;
+ } else {
+ throw new Exception(t);
+ }
+ }
}
// Only call this if there is no white space in every argument
--- a/test/lib/jdk/test/lib/util/JarUtils.java Fri Oct 27 21:10:56 2017 +0800
+++ b/test/lib/jdk/test/lib/util/JarUtils.java Fri Oct 27 21:11:15 2017 +0800
@@ -27,9 +27,13 @@
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.ArrayList;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Enumeration;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@@ -79,70 +83,93 @@
*/
public static void updateJar(String src, String dest, String... files)
throws IOException {
+ Map<String,Object> changes = new HashMap<>();
+ boolean update = true;
+ for (String file : files) {
+ if (file.equals("-")) {
+ update = false;
+ } else if (update) {
+ try {
+ Path p = Paths.get(file);
+ if (Files.exists(p)) {
+ changes.put(file, p);
+ } else {
+ changes.put(file, file);
+ }
+ } catch (InvalidPathException e) {
+ // Fallback if file not a valid Path.
+ changes.put(file, file);
+ }
+ } else {
+ changes.put(file, Boolean.FALSE);
+ }
+ }
+ updateJar(src, dest, changes);
+ }
+
+ /**
+ * Update content of a jar file.
+ *
+ * @param src the original jar file name
+ * @param dest the new jar file name
+ * @param changes a map of changes, key is jar entry name, value is content.
+ * Value can be Path, byte[] or String. If key exists in
+ * src but value is Boolean FALSE. The entry is removed.
+ * Existing entries in src not a key is unmodified.
+ * @throws IOException
+ */
+ public static void updateJar(String src, String dest,
+ Map<String,Object> changes)
+ throws IOException {
+
+ // What if input changes is immutable?
+ changes = new HashMap<>(changes);
+
+ System.out.printf("Creating %s from %s...\n", dest, src);
try (JarOutputStream jos = new JarOutputStream(
new FileOutputStream(dest))) {
- // copy each old entry into destination unless the entry name
- // is in the updated list
- List<String> updatedFiles = new ArrayList<>();
try (JarFile srcJarFile = new JarFile(src)) {
Enumeration<JarEntry> entries = srcJarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
- boolean found = false;
- boolean update = true;
- for (String file : files) {
- if (file.equals("-")) {
- update = false;
- } else if (name.equals(file)) {
- updatedFiles.add(file);
- found = true;
- break;
- }
- }
-
- if (found) {
- if (update) {
- System.out.println(String.format("Updating %s with %s",
- dest, name));
- jos.putNextEntry(new JarEntry(name));
- try (FileInputStream fis = new FileInputStream(name)) {
- fis.transferTo(jos);
- } catch (FileNotFoundException e) {
- jos.write(name.getBytes());
- }
- } else {
- System.out.println(String.format("Removing %s from %s",
- name, dest));
- }
+ if (changes.containsKey(name)) {
+ System.out.println(String.format("- Update %s", name));
+ updateEntry(jos, name, changes.get(name));
+ changes.remove(name);
} else {
- System.out.println(String.format("Copying %s to %s",
- name, dest));
+ System.out.println(String.format("- Copy %s", name));
jos.putNextEntry(entry);
srcJarFile.getInputStream(entry).transferTo(jos);
}
}
}
-
- // append new files
- for (String file : files) {
- if (file.equals("-")) {
- break;
- }
- if (!updatedFiles.contains(file)) {
- System.out.println(String.format("Adding %s with %s",
- dest, file));
- jos.putNextEntry(new JarEntry(file));
- try (FileInputStream fis = new FileInputStream(file)) {
- fis.transferTo(jos);
- } catch (FileNotFoundException e) {
- jos.write(file.getBytes());
- }
- }
+ for (Map.Entry<String, Object> e : changes.entrySet()) {
+ System.out.println(String.format("- Add %s", e.getKey()));
+ updateEntry(jos, e.getKey(), e.getValue());
}
}
System.out.println();
}
+ private static void updateEntry(JarOutputStream jos, String name, Object content)
+ throws IOException {
+ if (content instanceof Boolean) {
+ if (((Boolean) content).booleanValue()) {
+ throw new RuntimeException("Boolean value must be FALSE");
+ }
+ } else {
+ jos.putNextEntry(new JarEntry(name));
+ if (content instanceof Path) {
+ Files.newInputStream((Path) content).transferTo(jos);
+ } else if (content instanceof byte[]) {
+ jos.write((byte[]) content);
+ } else if (content instanceof String) {
+ jos.write(((String) content).getBytes());
+ } else {
+ throw new RuntimeException("Unknown type " + content.getClass());
+ }
+ }
+ }
}