diff --git a/core/pom.xml b/core/pom.xml index e6f3f078102c9fda6f4a6835c78a9c40fb1915f5..dc78025fa4e9215210dd12b298acd8009d768dab 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -298,6 +298,11 @@ THE SOFTWARE. cli ${project.version} + + org.jvnet.hudson + crypto-util + 1.0 + org.jvnet.hudson jtidy diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 3d7c2f6e48cee6313030b2fbf4065720437d59bb..1f69e66b88909a603fca0bf7f4628c0f52236d6c 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -41,9 +41,12 @@ import org.acegisecurity.Authentication; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.io.output.TeeOutputStream; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.jvnet.hudson.crypto.SignatureOutputStream; +import org.jvnet.hudson.crypto.CertificateUtil; import javax.servlet.ServletException; import javax.net.ssl.SSLHandshakeException; @@ -52,6 +55,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.OutputStreamWriter; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; @@ -70,6 +75,14 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; +import java.security.MessageDigest; +import java.security.DigestOutputStream; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import com.trilead.ssh2.crypto.Base64; /** * Controls update center capability. @@ -181,7 +194,7 @@ public class UpdateCenter extends AbstractModelObject { /** * This is the endpoint that receives the update center data file from the browser. */ - public void doPostBack(StaplerRequest req) throws IOException { + public void doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException { dataTimestamp = System.currentTimeMillis(); String p = req.getParameter("json"); JSONObject o = JSONObject.fromObject(p); @@ -192,10 +205,63 @@ public class UpdateCenter extends AbstractModelObject { return; } + if (signatureCheck) + verifySignature(o); + LOGGER.info("Obtained the latest update center data file"); getDataFile().write(p); } + /** + * Verifies the signature in the update center data file. + */ + private boolean verifySignature(JSONObject o) throws GeneralSecurityException, IOException { + JSONObject signature = o.getJSONObject("signature"); + if (signature.isNullObject()) { + LOGGER.severe("No signature block found"); + return false; + } + o.remove("signature"); + + List certs = new ArrayList(); + {// load and verify certificates + CertificateFactory cf = CertificateFactory.getInstance("X509"); + for (Object cert : o.getJSONArray("certificates")) { + X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray()))); + c.checkValidity(); + certs.add(c); + } + CertificateUtil.validatePath(certs); + } + + // 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"); + sig.initVerify(certs.get(0)); + SignatureOutputStream sos = new SignatureOutputStream(sig); + + o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")); + + // 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.getString("digest"); + if (!computedDigest.equalsIgnoreCase(providedDigest)) { + LOGGER.severe("Digest mismatch: "+computedDigest+" vs "+providedDigest); + return false; + } + + if (!sig.verify(Base64.decode(signature.getString("signature").toCharArray()))) { + LOGGER.severe("Signature in the update center doesn't match with the certificate"); + return false; + } + + return true; + } + /** * Schedules a Hudson upgrade. */ @@ -1016,4 +1082,9 @@ public class UpdateCenter extends AbstractModelObject { private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName()); public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never"); + + /** + * Off by default until we know this is reasonably working. + */ + public static boolean signatureCheck = Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck"); }