Skip to content

Commit

Permalink
Merge pull request #140 from jenkinsci/feature/whitelist-hmac
Browse files Browse the repository at this point in the history
Whitelist and HMAC #139
  • Loading branch information
tomasbjerre committed Oct 20, 2019
2 parents 80a1176 + d39f81c commit d2dd767
Show file tree
Hide file tree
Showing 21 changed files with 624 additions and 0 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,21 @@ Changelog of Generic Webhook Plugin.
[e02cb6ccfd88325](https://github.com/jenkinsci/generic-webhook-trigger-plugin/commit/e02cb6ccfd88325) Tomas Bjerre *2019-09-29 09:18:16*


### GitHub [#139](https://github.com/jenkinsci/generic-webhook-trigger-plugin/issues/139) Whitelist hosts in global config *enhancement*

**Whitelist and HMAC #139**


[c6a7a3c7c527f29](https://github.com/jenkinsci/generic-webhook-trigger-plugin/commit/c6a7a3c7c527f29) Tomas Bjerre *2019-10-20 15:45:48*


### No issue

**Taking care of some sca**


[a1066f64da803cc](https://github.com/jenkinsci/generic-webhook-trigger-plugin/commit/a1066f64da803cc) Tomas Bjerre *2019-10-14 16:39:13*

**doc**


Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ The token can be supplied as a:
* *Authorization* header of type *Bearer* : `curl -vs -H "Authorization: Bearer abc123" http://localhost:8080/jenkins/generic-webhook-trigger/invoke 2>&1`


## Whitelist hosts

A [whitelist](/sandbox/whitelist.png) can be configured in Jenkins global configuration page. The whitelist will block any request to the plugin that is not configured in this list. The hosts can optionally also be verified with [HMAC](https://en.wikipedia.org/wiki/HMAC).


## Troubleshooting

If you want to fiddle with the plugin, you may use this repo: https://github.com/tomasbjerre/jenkins-configuration-as-code-sandbox
Expand Down
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ Changelog of Generic Webhook Plugin.


<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plain-credentials</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
Expand Down
Binary file added sandbox/hmac-bitbucket-server.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added sandbox/whitelist.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.jenkinsci.plugins.gwt.jobfinder.JobFinder;
import org.jenkinsci.plugins.gwt.whitelist.WhitelistVerifier;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;

Expand Down Expand Up @@ -52,6 +53,18 @@ public HttpResponse doInvoke(final StaplerRequest request) {
LOGGER.log(SEVERE, "", e);
}

if (!WhitelistVerifier.verifyWhitelist(request.getRemoteHost(), headers, postContent)) {
final Map<String, Object> response = new HashMap<>();
response.put(
"triggerResults",
"Sender, "
+ request.getRemoteHost()
+ ", with headers "
+ headers
+ " did not pass whitelist.");
return okJSON(response);
}

final String givenToken = getGivenToken(headers, parameterMap);
return doInvoke(headers, parameterMap, postContent, givenToken);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.jenkinsci.plugins.gwt.global;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Strings.isNullOrEmpty;

import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.google.common.base.Optional;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.model.Item;
import hudson.model.Queue;
import hudson.model.queue.Tasks;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

public class CredentialsHelper {

@SuppressFBWarnings("NP_NULL_PARAM_DEREF")
public static ListBoxModel doFillCredentialsIdItems(final Item item, final String credentialsId) {
final StandardListBoxModel result = new StandardListBoxModel();
if (item == null) {
if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
return result.includeCurrentValue(credentialsId);
}
} else {
if (!item.hasPermission(Item.EXTENDED_READ)
&& !item.hasPermission(CredentialsProvider.USE_ITEM)) {
return result.includeCurrentValue(credentialsId);
}
}
return result //
.includeEmptyValue() //
.includeMatchingAs(
item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM,
item,
StringCredentials.class,
new ArrayList<DomainRequirement>(),
CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StringCredentials.class)))
.includeCurrentValue(credentialsId);
}

public static FormValidation doCheckFillCredentialsId(final String credentialsId) {
if (isNullOrEmpty(credentialsId)) {
return FormValidation.ok();
}
if (!findCredentials(credentialsId).isPresent()) {
return FormValidation.error("Cannot find currently selected credentials");
}
return FormValidation.ok();
}

public static Optional<StringCredentials> findCredentials(final String credentialsId) {
if (isNullOrEmpty(credentialsId)) {
return absent();
}
final Item item2 = null;
final Authentication authentication = null;
final ArrayList<DomainRequirement> domainRequirements = new ArrayList<DomainRequirement>();
final List<StringCredentials> lookupCredentials =
CredentialsProvider.lookupCredentials(
StringCredentials.class, item2, authentication, domainRequirements);
final CredentialsMatcher allOf =
CredentialsMatchers.allOf(
CredentialsMatchers.withId(credentialsId),
CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StringCredentials.class)));
return fromNullable(CredentialsMatchers.firstOrNull(lookupCredentials, allOf));
}
}
59 changes: 59 additions & 0 deletions src/main/java/org/jenkinsci/plugins/gwt/global/Whitelist.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.jenkinsci.plugins.gwt.global;

import com.google.common.annotations.VisibleForTesting;
import hudson.Extension;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import jenkins.model.GlobalConfiguration;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.StaplerRequest;

@Extension
public class Whitelist extends GlobalConfiguration implements Serializable {

private static final long serialVersionUID = -2832851253933848205L;

public static Whitelist get() {
return GlobalConfiguration.all().get(Whitelist.class);
}

private boolean enabled;
private List<WhitelistItem> whitelistItems = new ArrayList<>();

@VisibleForTesting
public Whitelist(final boolean enabled, final List<WhitelistItem> whitelistItems) {
this.enabled = enabled;
this.whitelistItems = whitelistItems;
}

public Whitelist() {
load();
}

@Override
public boolean configure(final StaplerRequest req, final JSONObject json) throws FormException {
req.bindJSON(this, json);
save();
return true;
}

@DataBoundSetter
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}

public boolean isEnabled() {
return enabled;
}

@DataBoundSetter
public void setWhitelistItems(final List<WhitelistItem> whitelistItems) {
this.whitelistItems = whitelistItems;
}

public List<WhitelistItem> getWhitelistItems() {
return whitelistItems;
}
}
104 changes: 104 additions & 0 deletions src/main/java/org/jenkinsci/plugins/gwt/global/WhitelistItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.jenkinsci.plugins.gwt.global;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

public class WhitelistItem extends AbstractDescribableImpl<WhitelistItem> implements Serializable {

public static final String HMAC_MD5 = "HmacMD5";
public static final String HMAC_SHA1 = "HmacSHA1";
public static final String HMAC_SHA256 = "HmacSHA256";
private static final long serialVersionUID = 1176246137502450635L;
private String host;
private boolean hmacEnabled;
private String hmacHeader;
private String hmacCredentialId;
private String hmacAlgorithm;
private static final List<String> MAC_ALGORITHMS =
Arrays.asList(HMAC_MD5, HMAC_SHA1, HMAC_SHA256);

public WhitelistItem() {}

@DataBoundConstructor
public WhitelistItem(final String host) {
this.host = host;
}

public String getHost() {
return host;
}

public String getHmacAlgorithm() {
return hmacAlgorithm;
}

@DataBoundSetter
public void setHmacAlgorithm(final String hmacAlgorithm) {
this.hmacAlgorithm = hmacAlgorithm;
}

public String getHmacCredentialId() {
return hmacCredentialId;
}

@DataBoundSetter
public void setHmacCredentialId(final String hmacCredentialId) {
this.hmacCredentialId = hmacCredentialId;
}

public boolean isHmacEnabled() {
return hmacEnabled;
}

@DataBoundSetter
public void setHmacEnabled(final boolean hmacEnabled) {
this.hmacEnabled = hmacEnabled;
}

public String getHmacHeader() {
return hmacHeader;
}

@DataBoundSetter
public void setHmacHeader(final String hmacHeader) {
this.hmacHeader = hmacHeader;
}

@Extension
public static class DescriptorImpl extends Descriptor<WhitelistItem> {
@NonNull
@Override
public String getDisplayName() {
return "Whitelist item";
}

public ListBoxModel doFillHmacAlgorithmItems() {
final ListBoxModel listBoxModel = new ListBoxModel();
for (final String a : MAC_ALGORITHMS) {
listBoxModel.add(a);
}
return listBoxModel;
}

public ListBoxModel doFillHmacCredentialIdItems(
@AncestorInPath final Item item, @QueryParameter final String credentialsId) {
return CredentialsHelper.doFillCredentialsIdItems(item, credentialsId);
}

public FormValidation doCheckHmacCredentialIdItems(@QueryParameter final String value) {
return CredentialsHelper.doCheckFillCredentialsId(value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.jenkinsci.plugins.gwt.whitelist;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class HMACVerifier {

public static boolean hmacVerify(
final Map<String, List<String>> headers,
final String postContent,
final String hmacHeader,
final String hmacSecret,
final String algorithm) {
final String headerValue = getHeaderValue(hmacHeader, headers);
final String calculateHmac = getCalculatedHmac(postContent, hmacSecret, algorithm);
return headerValue.equalsIgnoreCase(calculateHmac);
}

private static String getCalculatedHmac(
final String postContent, final String hmacSecret, final String algorithm) {
try {
final byte[] byteKey = hmacSecret.getBytes("UTF-8");
final Mac sha512_HMAC = Mac.getInstance(algorithm);
final SecretKeySpec keySpec = new SecretKeySpec(byteKey, algorithm);
sha512_HMAC.init(keySpec);
final byte[] mac_data = sha512_HMAC.doFinal(postContent.getBytes(UTF_8));
return bytesToHex(mac_data);
} catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}

private static String bytesToHex(final byte[] bytes) {
final char[] hexArray = "0123456789ABCDEF".toCharArray();
final char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
final int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}

private static String getHeaderValue(
final String hmacHeader, final Map<String, List<String>> headers) {
for (final Entry<String, List<String>> ck : headers.entrySet()) {
final boolean sameHeader = ck.getKey().equalsIgnoreCase(hmacHeader);
final boolean oneValue = ck.getValue().size() == 1;
if (sameHeader && oneValue) {
final String value = ck.getValue().get(0);
if (value.contains("=")) {
// To handle X-Hub-Signature: sha256=87e3e7...
return value.split("=")[1];
}
return value;
}
}
throw new RuntimeException(
"Was unable to find header with name \"" + hmacHeader + "\" among " + headers);
}
}
Loading

0 comments on commit d2dd767

Please sign in to comment.