50
47
import org.kohsuke.stapler.export.ExportedBean;
51
48
import org.kohsuke.stapler.interceptor.RequirePOST;
53
import java.io.ByteArrayInputStream;
54
50
import java.io.File;
55
import java.io.FileInputStream;
56
51
import java.io.IOException;
57
import java.io.OutputStreamWriter;
58
import java.security.DigestOutputStream;
52
import java.io.InputStream;
55
import java.net.URLConnection;
56
import java.net.URLEncoder;
59
57
import java.security.GeneralSecurityException;
60
import java.security.MessageDigest;
61
import java.security.Signature;
62
import java.security.cert.CertificateExpiredException;
63
import java.security.cert.CertificateFactory;
64
import java.security.cert.CertificateNotYetValidException;
65
import java.security.cert.TrustAnchor;
66
import java.security.cert.X509Certificate;
67
58
import java.util.ArrayList;
68
59
import java.util.Collections;
69
60
import java.util.HashMap;
70
import java.util.HashSet;
71
61
import java.util.List;
72
62
import java.util.Map;
73
63
import java.util.Set;
74
64
import java.util.TreeMap;
65
import java.util.concurrent.Callable;
75
66
import java.util.concurrent.Future;
76
67
import java.util.logging.Level;
77
68
import java.util.logging.Logger;
158
143
public long getDataTimestamp() {
144
assert dataTimestamp >= 0;
159
145
return dataTimestamp;
149
* Update the data file from the given URL if the file
150
* does not exist, or is otherwise due for update.
151
* Accepted formats are JSONP or HTML with {@code postMessage}, not raw JSON.
152
* @param signatureCheck whether to enforce the signature (may be off only for testing!)
153
* @return null if no updates are necessary, or the future result
156
public Future<FormValidation> updateDirectly(final boolean signatureCheck) {
157
if (! getDataFile().exists() || isDue()) {
158
return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable<FormValidation>() {
160
public FormValidation call() throws Exception {
161
URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8")
162
+ "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8"));
163
URLConnection conn = ProxyConfiguration.open(src);
164
InputStream is = conn.getInputStream();
166
String uncleanJson = IOUtils.toString(is,"UTF-8");
167
int jsonStart = uncleanJson.indexOf("{\"");
168
if (jsonStart >= 0) {
169
uncleanJson = uncleanJson.substring(jsonStart);
170
int end = uncleanJson.lastIndexOf('}');
172
uncleanJson = uncleanJson.substring(0,end+1);
173
return updateData(uncleanJson, signatureCheck);
175
throw new IOException("Could not find json in content of " +
176
"update center from url: "+src.toExternalForm());
163
189
* This is the endpoint that receives the update center data file from the browser.
165
191
public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
192
return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true);
195
private FormValidation updateData(String json, boolean signatureCheck)
166
198
dataTimestamp = System.currentTimeMillis();
167
String json = IOUtils.toString(req.getInputStream(),"UTF-8");
168
200
JSONObject o = JSONObject.fromObject(json);
170
int v = o.getInt("updateCenterVersion");
172
throw new IllegalArgumentException("Unrecognized update center version: "+v);
203
int v = o.getInt("updateCenterVersion");
205
throw new IllegalArgumentException("Unrecognized update center version: " + v);
207
} catch (JSONException x) {
208
throw new IllegalArgumentException("Could not find (numeric) updateCenterVersion in " + json, x);
174
211
if (signatureCheck) {
175
212
FormValidation e = verifySignature(o);
176
213
if (e.kind!=Kind.OK) {
177
LOGGER.severe(e.renderHtml());
214
LOGGER.severe(e.toString());
193
230
* Verifies the signature in the update center data file.
195
232
private FormValidation verifySignature(JSONObject o) throws IOException {
197
FormValidation warning = null;
199
JSONObject signature = o.getJSONObject("signature");
200
if (signature.isNullObject()) {
201
return FormValidation.error("No signature block found in update center '"+id+"'");
203
o.remove("signature");
205
List<X509Certificate> certs = new ArrayList<X509Certificate>();
206
{// load and verify certificates
207
CertificateFactory cf = CertificateFactory.getInstance("X509");
208
for (Object cert : signature.getJSONArray("certificates")) {
209
X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray())));
212
} catch (CertificateExpiredException e) { // even if the certificate isn't valid yet, we'll proceed it anyway
213
warning = FormValidation.warning(e,String.format("Certificate %s has expired in update center '%s'",cert.toString(),id));
214
} catch (CertificateNotYetValidException e) {
215
warning = FormValidation.warning(e,String.format("Certificate %s is not yet valid in update center '%s'",cert.toString(),id));
220
// if we trust default root CAs, we end up trusting anyone who has a valid certificate,
221
// which isn't useful at all
222
Set<TrustAnchor> anchors = new HashSet<TrustAnchor>(); // CertificateUtil.getDefaultRootCAs();
223
Jenkins j = Jenkins.getInstance();
224
for (String cert : (Set<String>) j.servletContext.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
225
if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation
226
anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(j.servletContext.getResourceAsStream(cert)),null));
228
File[] cas = new File(j.root, "update-center-rootCAs").listFiles();
230
for (File cert : cas) {
231
if (cert.getName().endsWith(".txt")) continue; // skip text files that are meant to be documentation
232
FileInputStream in = new FileInputStream(cert);
234
anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(in),null));
240
CertificateUtil.validatePath(certs,anchors);
243
// this is for computing a digest to check sanity
244
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
245
DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1);
247
// this is for computing a signature
248
Signature sig = Signature.getInstance("SHA1withRSA");
249
sig.initVerify(certs.get(0));
250
SignatureOutputStream sos = new SignatureOutputStream(sig);
252
// until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature)
253
// that only covers the earlier portion of the file. This was caused by the lack of close() call
254
// in the canonical writing, which apparently leave some bytes somewhere that's not flushed to
255
// the digest output stream. This affects Jenkins [1.424,1,431].
256
// Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it
257
// compute the correct digest, but it breaks all the existing UC json metadata out there. We then
258
// quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature,
259
// it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*).
261
// In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature"
262
// pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated
265
// Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows
266
// the attacker to inject a fragment at the end of the json.
267
o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")).close();
269
// did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
270
// (which is more likely than someone tampering with update center), we can tell
271
String computedDigest = new String(Base64.encode(sha1.digest()));
272
String providedDigest = signature.optString("correct_digest");
273
if (providedDigest==null) {
274
return FormValidation.error("No correct_digest parameter in update center '"+id+"'. This metadata appears to be old.");
276
if (!computedDigest.equalsIgnoreCase(providedDigest)) {
277
return FormValidation.error("Digest mismatch: "+computedDigest+" vs "+providedDigest+" in update center '"+id+"'");
280
String providedSignature = signature.getString("correct_signature");
281
if (!sig.verify(Base64.decode(providedSignature.toCharArray()))) {
282
return FormValidation.error("Signature in the update center doesn't match with the certificate in update center '"+id+"'");
285
if (warning!=null) return warning;
286
return FormValidation.ok();
287
} catch (GeneralSecurityException e) {
288
return FormValidation.error(e,"Signature verification failed in the update center '"+id+"'");
233
return new JSONSignatureValidator("update site '"+id+"'").verifySignature(o);
770
725
* @param dynamicLoad
771
726
* If true, the plugin will be dynamically loaded into this Jenkins. If false,
772
727
* the plugin will only take effect after the reboot.
728
* See {@link UpdateCenter#isRestartRequiredForCompletion()}
774
730
public Future<UpdateCenterJob> deploy(boolean dynamicLoad) {
775
731
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
776
732
UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
777
733
for (Plugin dep : getNeededDependencies()) {
778
LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
779
dep.deploy(dynamicLoad);
734
UpdateCenter.InstallationJob job = uc.getJob(dep);
735
if (job == null || job.status instanceof UpdateCenter.DownloadJob.Failure) {
736
LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
737
dep.deploy(dynamicLoad);
739
LOGGER.log(Level.WARNING, "Dependent install of " + dep.name + " for plugin " + name + " already added, skipping");
781
742
return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad));