提交 6dc32977 编写于 作者: D Daniel Beck

Add support for SHA-256/512 in update site metadata

上级 5be4ddec
......@@ -23,6 +23,7 @@
*/
package hudson.model;
import com.google.common.annotations.VisibleForTesting;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionPoint;
......@@ -37,6 +38,7 @@ import jenkins.util.SystemProperties;
import hudson.Util;
import hudson.XmlFile;
import static hudson.init.InitMilestone.PLUGINS_STARTED;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import hudson.init.Initializer;
......@@ -51,7 +53,6 @@ import hudson.util.DaemonThreadFactory;
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.util.NamingThreadFactory;
import hudson.util.IOException2;
import hudson.util.PersistedList;
import hudson.util.XStream2;
import jenkins.MissingDependencyException;
......@@ -64,6 +65,7 @@ import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.jenkinsci.Symbol;
......@@ -1107,12 +1109,15 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
public File download(DownloadJob job, URL src) throws IOException {
MessageDigest sha1 = null;
MessageDigest sha256 = null;
MessageDigest sha512 = null;
try {
// Java spec says SHA-1 and SHA-256 exist, and SHA-512 might not, so one try/catch block should be fine
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException ignored) {
// Irrelevant as the Java spec says SHA-1 must exist. Still, if this fails
// the DownloadJob will just have computedSha1 = null and that is expected
// to be handled by caller
sha256 = MessageDigest.getInstance("SHA-256");
sha512 = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException nsa) {
LOGGER.log(Level.WARNING, "Failed to instantiate message digest algorithm, may only have weak or no verification of downloaded file", nsa);
}
URLConnection con = null;
......@@ -1135,7 +1140,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
String oldName = t.getName();
t.setName(oldName + ": " + src);
try (OutputStream _out = Files.newOutputStream(tmp.toPath());
OutputStream out = sha1 != null ? new DigestOutputStream(_out, sha1) : _out;
OutputStream out =
sha1 != null ? new DigestOutputStream(
sha256 != null ? new DigestOutputStream(
sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out;
InputStream in = con.getInputStream();
CountingInputStream cin = new CountingInputStream(in)) {
while ((len = cin.read(buf)) >= 0) {
......@@ -1159,6 +1167,14 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
byte[] digest = sha1.digest();
job.computedSHA1 = Base64.encodeBase64String(digest);
}
if (sha256 != null) {
byte[] digest = sha256.digest();
job.computedSHA256 = Hex.encodeHexString(digest);
}
if (sha512 != null) {
byte[] digest = sha512.digest();
job.computedSHA512 = Hex.encodeHexString(digest);
}
return tmp;
} catch (IOException e) {
// assist troubleshooting in case of e.g. "too many redirects" by printing actual URL
......@@ -1169,7 +1185,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
// Also, since it involved name resolution, it'd be an expensive operation.
extraMessage = " (redirected to: " + con.getURL() + ")";
}
throw new IOException2("Failed to download from "+src+extraMessage,e);
throw new IOException("Failed to download from "+src+extraMessage,e);
}
}
......@@ -1589,11 +1605,18 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
status = new Success();
}
}
@Restricted(NoExternalUse.class)
/*package*/ interface WithComputedChecksums {
String getComputedSHA1();
String getComputedSHA256();
String getComputedSHA512();
}
/**
* Base class for a job that downloads a file from the Jenkins project.
*/
public abstract class DownloadJob extends UpdateCenterJob {
public abstract class DownloadJob extends UpdateCenterJob implements WithComputedChecksums {
/**
* Immutable object representing the current state of this job.
*/
......@@ -1620,16 +1643,41 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
/**
* During download, an attempt is made to compute the SHA-1 checksum of the file.
* This is the base64 encoded SHA-1 checksum.
*
* @since 1.641
*/
@CheckForNull
protected String getComputedSHA1() {
public String getComputedSHA1() {
return computedSHA1;
}
private String computedSHA1;
/**
* Hex encoded SHA-256 checksum of the downloaded file, if it could be computed.
*
* @since TODO
*/
@CheckForNull
public String getComputedSHA256() {
return computedSHA256;
}
private String computedSHA256;
/**
*Hex encoded SHA-512 checksum of the downloaded file, if it could be computed.
*
* @since TODO
*/
@CheckForNull
public String getComputedSHA512() {
return computedSHA512;
}
private String computedSHA512;
private Authentication authentication;
/**
......@@ -1794,22 +1842,94 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
}
/**
* If expectedSHA1 is non-null, ensure that actualSha1 is the same value, otherwise throw.
*
* Utility method for InstallationJob and HudsonUpgradeJob.
* Compare the provided values and return the appropriate {@link VerificationResult}.
*
* @throws IOException when checksums don't match, or actual checksum was null.
*/
private void verifyChecksums(String expectedSHA1, String actualSha1, File downloadedFile) throws IOException {
if (expectedSHA1 != null) {
if (actualSha1 == null) {
// refuse to install if SHA-1 could not be computed
private static VerificationResult verifyChecksums(String expectedDigest, String actualDigest, boolean caseSensitive) {
if (expectedDigest == null) {
return VerificationResult.NOT_PROVIDED;
}
if (actualDigest == null) {
return VerificationResult.NOT_COMPUTED;
}
if (caseSensitive ? expectedDigest.equals(actualDigest) : expectedDigest.equalsIgnoreCase(actualDigest)) {
return VerificationResult.PASS;
}
return VerificationResult.FAIL;
}
private static enum VerificationResult {
PASS,
NOT_PROVIDED,
NOT_COMPUTED,
FAIL
}
/**
* Throws an {@code IOException} with a message about {@code actual} not matching {@code expected} for {@code file} when using {@code algorithm}.
*/
private static void throwVerificationFailure(String expected, String actual, File file, String algorithm) throws IOException {
throw new IOException("Downloaded file " + file.getAbsolutePath() + " does not match expected " + algorithm + ", expected '" + expected + "', actual '" + actual + "'");
}
/**
* Implements the checksum verification logic with fallback to weaker algorithm for {@link DownloadJob}.
* @param job The job downloading the file to check
* @param entry The metadata entry for the file to check
* @param file The downloaded file
* @throws IOException thrown when one of the checks failed, or no checksum could be computed.
*/
@VisibleForTesting
@Restricted(NoExternalUse.class)
/* package */ static void verifyChecksums(WithComputedChecksums job, UpdateSite.Entry entry, File file) throws IOException {
VerificationResult result512 = verifyChecksums(entry.getSha512(), job.getComputedSHA512(), false);
switch (result512) {
case PASS:
// this has passed but no real reason not to check the others
break;
case FAIL:
throwVerificationFailure(entry.getSha512(), job.getComputedSHA512(), file, "SHA-512");
case NOT_COMPUTED:
LOGGER.log(WARNING, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 failed since it could not be computed. Falling back to weaker algorithms. Update your JRE.");
break;
case NOT_PROVIDED:
LOGGER.log(INFO, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 failed since your configured update site does not provide this checksum. Falling back to weaker algorithms.");
break;
}
VerificationResult result256 = verifyChecksums(entry.getSha256(), job.getComputedSHA256(), false);
switch (result256) {
case PASS:
// this has passed but no real reason not to check the others
break;
case FAIL:
throwVerificationFailure(entry.getSha256(), job.getComputedSHA256(), file, "SHA-256");
case NOT_COMPUTED:
case NOT_PROVIDED:
// we've probably already complained once, and if not, we've passed SHA-512.
break;
}
VerificationResult result1 = verifyChecksums(entry.getSha1(), job.getComputedSHA1(), true);
switch (result1) {
case PASS:
return;
case FAIL:
throwVerificationFailure(entry.getSha1(), job.getComputedSHA1(), file, "SHA-1");
case NOT_COMPUTED:
// we need to make sure that at least one of the checks has passed, otherwise this is a problem
if (result256 == VerificationResult.PASS || result512 == VerificationResult.PASS) {
return;
}
throw new IOException("Failed to compute SHA-1 of downloaded file, refusing installation");
}
if (!expectedSHA1.equals(actualSha1)) {
throw new IOException("Downloaded file " + downloadedFile.getAbsolutePath() + " does not match expected SHA-1, expected '" + expectedSHA1 + "', actual '" + actualSha1 + "'");
// keep 'downloadedFile' around for investigating what's going on
}
case NOT_PROVIDED:
if (result256 == VerificationResult.PASS || result512 == VerificationResult.PASS) {
return;
}
throw new IOException("Unable to confirm integrity of downloaded file, refusing installation");
}
}
......@@ -1959,8 +2079,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
@Override
protected void replace(File dst, File src) throws IOException {
verifyChecksums(plugin.getSha1(), getComputedSHA1(), src);
if (!site.getId().equals(ID_UPLOAD)) {
verifyChecksums(this, plugin, src);
}
File bak = Util.changeExtension(dst, ".bak");
bak.delete();
......@@ -2094,8 +2215,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
@Override
protected void replace(File dst, File src) throws IOException {
String expectedSHA1 = site.getData().core.getSha1();
verifyChecksums(expectedSHA1, getComputedSHA1(), src);
verifyChecksums(this, site.getData().core, src);
Lifecycle.get().rewriteHudsonWar(src);
}
}
......
......@@ -616,6 +616,12 @@ public class UpdateSite {
@Restricted(NoExternalUse.class)
/* final */ String sha1;
@Restricted(NoExternalUse.class)
/* final */ String sha256;
@Restricted(NoExternalUse.class)
/* final */ String sha512;
public Entry(String sourceId, JSONObject o) {
this(sourceId, o, null);
}
......@@ -628,6 +634,8 @@ public class UpdateSite {
// Trim this to prevent issues when the other end used Base64.encodeBase64String that added newlines
// to the end in old commons-codec. Not the case on updates.jenkins-ci.org, but let's be safe.
this.sha1 = Util.fixEmptyAndTrim(o.optString("sha1"));
this.sha256 = Util.fixEmptyAndTrim(o.optString("sha256"));
this.sha512 = Util.fixEmptyAndTrim(o.optString("sha512"));
String url = o.getString("url");
if (!URI.create(url).isAbsolute()) {
......@@ -649,6 +657,24 @@ public class UpdateSite {
return sha1;
}
/**
* The hex string encoded SHA-256 checksum of the file.
* Can be null if not provided by the update site.
* @since TODO
*/
public String getSha256() {
return sha256;
}
/**
* The hex string encoded SHA-512 checksum of the file.
* Can be null if not provided by the update site.
* @since TODO
*/
public String getSha512() {
return sha512;
}
/**
* Checks if the specified "current version" is older than the version of this entry.
*
......
......@@ -2,10 +2,15 @@ package jenkins.util;
import com.trilead.ssh2.crypto.Base64;
import hudson.util.FormValidation;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.jvnet.hudson.crypto.CertificateUtil;
......@@ -20,7 +25,9 @@ import java.io.OutputStreamWriter;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
......@@ -78,64 +85,151 @@ public class JSONSignatureValidator {
CertificateUtil.validatePath(certs, loadTrustAnchors(cf));
}
// this is for computing a digest to check sanity
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1);
// this is for computing a signature
Signature sig = Signature.getInstance("SHA1withRSA");
if (certs.isEmpty()) {
return FormValidation.error("No certificate found in %s. Cannot verify the signature", name);
} else {
sig.initVerify(certs.get(0));
}
SignatureOutputStream sos = new SignatureOutputStream(sig);
// until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature)
// that only covers the earlier portion of the file. This was caused by the lack of close() call
// in the canonical writing, which apparently leave some bytes somewhere that's not flushed to
// the digest output stream. This affects Jenkins [1.424,1,431].
// Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it
// compute the correct digest, but it breaks all the existing UC json metadata out there. We then
// quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature,
// it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*).
//
// In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature"
// pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated
// correctly.
//
// Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows
// the attacker to inject a fragment at the end of the json.
o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")).close();
// did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
// (which is more likely than someone tampering with update center), we can tell
String computedDigest = new String(Base64.encode(sha1.digest()));
String providedDigest = signature.optString("correct_digest");
if (providedDigest==null) {
return FormValidation.error("No correct_digest parameter in "+name+". This metadata appears to be old.");
}
if (!computedDigest.equalsIgnoreCase(providedDigest)) {
String msg = "Digest mismatch: computed=" + computedDigest + " vs expected=" + providedDigest + " in " + name;
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.severe(msg);
LOGGER.severe(o.toString(2));
// check the better digest first
FormValidation resultSha512 = null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
Signature sig = Signature.getInstance("SHA512withRSA");
sig.initVerify(certs.get(0));
resultSha512 = checkSpecificSignature(o, signature, digest, "correct_digest512", sig, "correct_signature512", "SHA-512");
switch (resultSha512.kind) {
case ERROR:
return resultSha512;
case WARNING:
LOGGER.log(Level.INFO, "JSON data source '" + name + "' does not provide a SHA-512 content checksum or signature. Looking for SHA-1.");
break;
case OK:
// fall through
}
return FormValidation.error(msg);
} catch (NoSuchAlgorithmException nsa) {
LOGGER.log(Level.WARNING, "Failed to verify potential SHA-512 digest/signature, falling back to SHA-1", nsa);
}
String providedSignature = signature.getString("correct_signature");
if (!sig.verify(Base64.decode(providedSignature.toCharArray()))) {
return FormValidation.error("Signature in the update center doesn't match with the certificate in "+name);
// if we get here, SHA-512 passed, wasn't provided, or the JRE is terrible.
MessageDigest digest = MessageDigest.getInstance("SHA1");
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(certs.get(0));
FormValidation resultSha1 = checkSpecificSignature(o, signature, digest, "correct_digest", sig, "correct_signature", "SHA-1");
switch (resultSha1.kind) {
case ERROR:
return resultSha1;
case WARNING:
if (resultSha512.kind == FormValidation.Kind.WARNING) {
// neither signature provided
return FormValidation.error("No correct_signature or correct_signature512 entry found in '" + name + "'.");
}
case OK:
// fall through
}
if (warning!=null) return warning;
return FormValidation.ok();
} catch (GeneralSecurityException e) {
return FormValidation.error(e,"Signature verification failed in "+name);
return FormValidation.error(e, "Signature verification failed in "+name);
}
}
/**
* Computes the specified {@code digest} and {@code signature} for the provided {@code json} object and checks whether they match {@code digestEntry} and {@signatureEntry} in the provided {@code signatureJson} object.
*
* @param json the full update-center.json content
* @param signatureJson signature block from update-center.json
* @param digest digest to compute
* @param digestEntry key of the digest entry in {@code signatureJson} to check
* @param signature signature to compute
* @param signatureEntry key of the signature entry in {@code signatureJson} to check
* @param digestName name of the digest used for log/error messages
* @return {@link FormValidation.Kind#WARNING} if digest or signature are not provided, {@link FormValidation.Kind#OK} if check is successful, {@link FormValidation.Kind#ERROR} otherwise.
* @throws IOException if this somehow fails to write the canonical JSON representation to an in-memory stream.
*/
private FormValidation checkSpecificSignature(JSONObject json, JSONObject signatureJson, MessageDigest digest, String digestEntry, Signature signature, String signatureEntry, String digestName) throws IOException {
// this is for computing a digest to check sanity
DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(), digest);
SignatureOutputStream sos = new SignatureOutputStream(signature);
String providedDigest = signatureJson.optString(digestEntry, null);
if (providedDigest == null) {
return FormValidation.warning("No '" + digestEntry + "' found");
}
String providedSignature = signatureJson.optString(signatureEntry, null);
if (providedSignature == null) {
return FormValidation.warning("No '" + signatureEntry + "' found");
}
// until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature)
// that only covers the earlier portion of the file. This was caused by the lack of close() call
// in the canonical writing, which apparently leave some bytes somewhere that's not flushed to
// the digest output stream. This affects Jenkins [1.424,1,431].
// Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it
// compute the correct digest, but it breaks all the existing UC json metadata out there. We then
// quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature,
// it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*).
//
// In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature"
// pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated
// correctly.
//
// Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows
// the attacker to inject a fragment at the end of the json.
json.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos), Charsets.UTF_8)).close();
// did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
// (which is more likely than someone tampering with update center), we can tell
if (!digestMatches(digest.digest(), providedDigest)) {
String msg = digestName + " digest mismatch: expected=" + providedDigest + " in '" + name + "'";
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.severe(msg);
LOGGER.severe(json.toString(2));
}
return FormValidation.error(msg);
}
if (!verifySignature(signature, providedSignature)) {
return FormValidation.error(digestName + " based signature in the update center doesn't match with the certificate in '"+name + "'");
}
return FormValidation.ok();
}
/**
* Utility method supporting both possible signature formats: Base64 and Hex
*/
private boolean verifySignature(Signature signature, String providedSignature) {
try {
if (signature.verify(Base64.decode(providedSignature.toCharArray()))) {
return true;
}
} catch (SignatureException|IOException ignore) {
// ignore
}
try {
if (signature.verify(Hex.decodeHex(providedSignature.toCharArray()))) {
return true;
}
} catch (SignatureException|DecoderException ignore) {
// ignore
}
return false;
}
/**
* Utility method supporting both possible digest formats: Base64 and Hex
*/
private boolean digestMatches(byte[] digest, String providedDigest) {
return providedDigest.equalsIgnoreCase(Hex.encodeHexString(digest)) || providedDigest.equalsIgnoreCase(new String(Base64.encode(digest)));
}
protected Set<TrustAnchor> loadTrustAnchors(CertificateFactory cf) throws IOException {
// if we trust default root CAs, we end up trusting anyone who has a valid certificate,
// which isn't useful at all
......
package hudson.model;
import net.sf.json.JSONObject;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class UpdateCenterTest {
......@@ -41,4 +49,139 @@ public class UpdateCenterTest {
"file://./foo.jar!update-center.json").toExternalForm(),
is("file://./foo.jar!update-center.json"));
}
@Test
public void noChecksums() {
try {
UpdateCenter.verifyChecksums(new MockDownloadJob(null, null, null), buildEntryWithExpectedChecksums(null, null, null), new File("example"));
fail();
} catch (IOException ex) {
assertEquals("Unable to confirm integrity of downloaded file, refusing installation", ex.getMessage());
}
}
@Test
public void sha1Match() throws Exception {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, null, null),
buildEntryWithExpectedChecksums(EMPTY_SHA1, null, null), new File("example"));
}
@Test
public void sha1Mismatch() {
try {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1.replace('k', 'f'), null, null),
buildEntryWithExpectedChecksums(EMPTY_SHA1, null, null), new File("example"));
fail();
} catch (IOException ex) {
assertTrue(ex.getMessage().contains("does not match expected SHA-1, expected '2jmj7l5rSw0yVb/vlWAYkK/YBwk=', actual '2jmj7l5rSw0yVb/vlWAYfK/YBwf='"));
}
}
@Test
public void sha512ProvidedOnly() throws IOException {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, EMPTY_SHA256, EMPTY_SHA512),
buildEntryWithExpectedChecksums(null, null, EMPTY_SHA512), new File("example"));
}
@Test
public void sha512and256IgnoreCase() throws IOException {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, EMPTY_SHA256.toUpperCase(Locale.US), EMPTY_SHA512.toUpperCase(Locale.US)),
buildEntryWithExpectedChecksums(null, EMPTY_SHA256, EMPTY_SHA512), new File("example"));
}
@Test
public void sha1DoesNotIgnoreCase() {
try {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, EMPTY_SHA256, EMPTY_SHA512),
buildEntryWithExpectedChecksums(EMPTY_SHA1.toUpperCase(Locale.US), null, null), new File("example"));
fail();
} catch (Exception e) {
assertTrue(e.getMessage().contains("does not match expected SHA-1, expected '2JMJ7L5RSW0YVB/VLWAYKK/YBWK=', actual '2jmj7l5rSw0yVb/vlWAYkK/YBwk='"));
}
}
@Test
public void noOverlapForComputedAndProvidedChecksums() {
try {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, EMPTY_SHA256, null),
buildEntryWithExpectedChecksums(null, null, EMPTY_SHA512), new File("example"));
fail();
} catch (Exception e) {
assertTrue(e.getMessage().equals("Unable to confirm integrity of downloaded file, refusing installation"));
}
}
@Test
public void noOverlapForComputedAndProvidedChecksumsForSpecIncompliantJVM() {
try {
UpdateCenter.verifyChecksums(
new MockDownloadJob(EMPTY_SHA1, null, null),
buildEntryWithExpectedChecksums(null, EMPTY_SHA256, EMPTY_SHA512), new File("example"));
fail();
} catch (Exception e) {
assertTrue(e.getMessage().equals("Unable to confirm integrity of downloaded file, refusing installation"));
}
}
private static String EMPTY_SHA1 = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
private static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
private static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";
private static UpdateSite.Entry buildEntryWithExpectedChecksums(String expectedSHA1, String expectedSHA256, String expectedSHA512) {
JSONObject o = new JSONObject();
o.put("name", "unnamed");
o.put("version", "unspecified");
o.put("url", "https://example.invalid");
if (expectedSHA1 != null) {
o.put("sha1", expectedSHA1);
}
if (expectedSHA256 != null) {
o.put("sha256", expectedSHA256);
}
if (expectedSHA512 != null) {
o.put("sha512", expectedSHA512);
}
return new MockEntry(o);
}
private static class MockEntry extends UpdateSite.Entry {
MockEntry(JSONObject o) {
// needs name, version, url, optionally sha1, sha256, sha512
super("default", o);
}
}
private static class MockDownloadJob implements UpdateCenter.WithComputedChecksums {
private final String computedSHA1;
private final String computedSHA256;
private final String computedSHA512;
public MockDownloadJob(String computedSHA1, String computedSHA256, String computedSHA512) {
this.computedSHA1 = computedSHA1;
this.computedSHA256 = computedSHA256;
this.computedSHA512 = computedSHA512;
}
public String getComputedSHA1() {
return this.computedSHA1;
}
public String getComputedSHA256() {
return computedSHA256;
}
public String getComputedSHA512() {
return computedSHA512;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册