Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
*/
public static GHWebhookSignature webhookSignature(String payload, Secret secret) {
checkNotNull(payload, "Payload can't be null");
checkNotNull(secret, "Secret should be defined to compute sign");
return new GHWebhookSignature(payload, secret);
}

Expand Down Expand Up @@ -69,19 +68,34 @@
return computeSignature(HMAC_SHA256_ALGORITHM);
}
/**
* Computes HMAC signature using the specified algorithm.
* Computes HMAC or plain hash signature depending on whether secret is set.
*
* @param algorithm The HMAC algorithm to use (e.g., "HmacSHA1", "HmacSHA256")
* @return HMAC digest as hex string, or INVALID_SIGNATURE on error
* @param algorithm The algorithm to use (e.g., "HmacSHA1", "HmacSHA256", "SHA-1", "SHA-256")
* @return digest as hex string, or INVALID_SIGNATURE on error
*/
private String computeSignature(String algorithm) {
try {
final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), algorithm);
final Mac mac = Mac.getInstance(algorithm);
mac.init(keySpec);
final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(UTF_8));

return Hex.encodeHexString(rawHMACBytes);
byte[] rawBytes;

if (secret != null) {

Check warning on line 80 in src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 80 is only partially covered, one branch is missing
// Use HMAC
final Mac mac = Mac.getInstance(algorithm);
final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), algorithm);
mac.init(keySpec);
rawBytes = mac.doFinal(payload.getBytes(UTF_8));
} else {
// Fall back to plain hash
final MessageDigest digest;
if (algorithm.startsWith("Hmac")) {
// map HmacSHA256 -> SHA-256, HmacSHA1 -> SHA-1, etc.
digest = MessageDigest.getInstance(algorithm.replace("Hmac", ""));
} else {
digest = MessageDigest.getInstance(algorithm);
}
rawBytes = digest.digest(payload.getBytes(UTF_8));

Check warning on line 95 in src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 89-95 are not covered by tests
}

return Hex.encodeHexString(rawBytes);
} catch (Exception e) {
LOGGER.error("Error computing {} signature", algorithm, e);
return INVALID_SIGNATURE;
Expand Down
52 changes: 50 additions & 2 deletions src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.github.GHEvent;

Expand All @@ -30,8 +31,7 @@
import static org.apache.commons.lang3.ClassUtils.PACKAGE_SEPARATOR;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue;
import static org.jenkinsci.plugins.github.test.HookSecretHelper.removeSecretIn;
import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn;
import static org.jenkinsci.plugins.github.test.HookSecretHelper.*;
import static org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload.Processor.*;

/**
Expand Down Expand Up @@ -88,6 +88,54 @@ public void shouldParseJsonWebHookFromGH() throws Exception {
.expect().log().all().statusCode(SC_OK).request().post(getPath());
}

@Test
@Issue("JENKINS-76080")
public void shouldWorkWithoutSecretAndNoSignatureHeaderWebHookFromGH() throws Exception {
removeSecretIn(config);
given().spec(spec)
.header(eventHeader(GHEvent.PUSH))
.header(JSON_CONTENT_TYPE)
.body(classpath("payloads/push.json"))
.log().all()
.expect().log().all().statusCode(SC_OK).request().post(getPath());
}

@Test
@Issue("JENKINS-76080")
public void shouldNotWorkWithoutSecretAndNoSignatureHeaderWebHookFromGH() throws Exception {
removeSecretIn(config);
storeGitHubPluginConfigWithNullSecret(config);
given().spec(spec)
.header(eventHeader(GHEvent.PUSH))
.header(JSON_CONTENT_TYPE)
.body(classpath("payloads/push.json"))
.log().all()
.expect().log().all().statusCode(SC_BAD_REQUEST).request().post(getPath());
}

/**
* <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads">Github Webhook Docs</a>
* X-Hub-Signature and X-Hub-Signature-256 are only sent when a webhook is configured with a secret
* Therefor if Jenkins Github plugin is configured with without a secret and receives a webhook with
* X-Hub-Signature and X-Hub-Signature-256 headers, it should fail
*/
@Test
@Issue("JENKINS-76080")
public void shouldNotWorkWithoutSecretParseJsonWebHookFromGH() throws Exception {
String hash = "notused";
String hash256 = "a17ea241b16bc285513afd659651a2456e7c44273abe3c3d0c08febe3ef10063";
removeSecretIn(config);
storeGitHubPluginConfigWithNullSecret(config);
given().spec(spec)
.header(eventHeader(GHEvent.PUSH))
.header(JSON_CONTENT_TYPE)
.header(SIGNATURE_HEADER, format("sha1=%s", hash))
.header(SIGNATURE_HEADER_SHA256, format("%s%s", SHA256_PREFIX, hash256))
.body(classpath("payloads/push.json"))
.log().all()
.expect().log().all().statusCode(SC_BAD_REQUEST).request().post(getPath());
}


@Test
public void shouldParseJsonWebHookFromGHWithSignHeader() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.config.HookSecretConfig;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -32,6 +33,12 @@ private HookSecretHelper() {
* @param secretText The secret/key.
*/
public static void storeSecretIn(GitHubPluginConfig config, final String secretText) {
final StringCredentialsImpl credentials = createCredentail(secretText);

config.setHookSecretConfigs(Collections.singletonList(new HookSecretConfig(credentials.getId())));
}

private static @NotNull StringCredentialsImpl createCredentail(String secretText) {
final StringCredentialsImpl credentials = new StringCredentialsImpl(
CredentialsScope.GLOBAL,
UUID.randomUUID().toString(),
Expand All @@ -53,10 +60,9 @@ public void run() {
}
}
});

config.setHookSecretConfigs(Collections.singletonList(new HookSecretConfig(credentials.getId())));
return credentials;
}

/**
* Stores the secret and sets it as the current hook secret.
* @param secretText The secret/key.
Expand All @@ -65,6 +71,13 @@ public static void storeSecret(final String secretText) {
storeSecretIn(Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class), secretText);
}

/**
* Stores the secret and sets it as the current hook secret.
*/
public static void storeGitHubPluginConfigWithNullSecret(GitHubPluginConfig config) {
config.setHookSecretConfigs(Collections.singletonList(new HookSecretConfig(null)));
}

/**
* Unsets the current hook secret.
*
Expand Down