forked from jenkinsci/github-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGHWebhookSignature.java
More file actions
136 lines (120 loc) · 4.76 KB
/
Copy pathGHWebhookSignature.java
File metadata and controls
136 lines (120 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package org.jenkinsci.plugins.github.webhook;
import hudson.util.Secret;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Utility class for dealing with signatures of incoming requests.
*
* @see <a href=https://developer.github.com/webhooks/#payloads>API documentation</a>
* @since 1.21.0
*/
public class GHWebhookSignature {
private static final Logger LOGGER = LoggerFactory.getLogger(GHWebhookSignature.class);
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
public static final String INVALID_SIGNATURE = "COMPUTED_INVALID_SIGNATURE";
private final String payload;
private final Secret secret;
private GHWebhookSignature(String payload, Secret secret) {
this.payload = payload;
this.secret = secret;
}
/**
* @param payload Clear-text to create signature of.
* @param secret Key to sign with.
*/
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);
}
/**
* Computes a RFC 2104-compliant HMAC digest using SHA1 of a payloadFrom with a given key (secret).
*
* @deprecated Use {@link #sha256()} for enhanced security
* @return HMAC digest of payloadFrom using secret as key. Will return COMPUTED_INVALID_SIGNATURE
* on any exception during computation.
*/
@Deprecated
public String sha1() {
return computeSignature(HMAC_SHA1_ALGORITHM);
}
/**
* Computes a RFC 2104-compliant HMAC digest using SHA256 of a payload with a given key (secret).
* This is the recommended method for webhook signature validation.
*
* @return HMAC digest of payload using secret as key. Will return COMPUTED_INVALID_SIGNATURE
* on any exception during computation.
* @since 1.45.0
*/
public String sha256() {
return computeSignature(HMAC_SHA256_ALGORITHM);
}
/**
* Computes HMAC signature using the specified algorithm.
*
* @param algorithm The HMAC algorithm to use (e.g., "HmacSHA1", "HmacSHA256")
* @return HMAC 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);
} catch (Exception e) {
LOGGER.error("Error computing {} signature", algorithm, e);
return INVALID_SIGNATURE;
}
}
/**
* @param digest computed signature from external place (GitHub)
*
* @return true if computed and provided signatures identical
* @deprecated Use {@link #matches(String, SignatureAlgorithm)} for explicit algorithm selection
*/
@Deprecated
public boolean matches(String digest) {
return matches(digest, SignatureAlgorithm.SHA1);
}
/**
* Validates a signature using the specified algorithm.
* Uses constant-time comparison to prevent timing attacks.
*
* @param digest the signature to validate (without algorithm prefix)
* @param algorithm the signature algorithm to use
* @return true if computed and provided signatures match
* @since 1.45.0
*/
public boolean matches(String digest, SignatureAlgorithm algorithm) {
String computed;
switch (algorithm) {
case SHA256:
computed = sha256();
break;
case SHA1:
computed = sha1();
break;
default:
LOGGER.warn("Unsupported signature algorithm: {}", algorithm);
return false;
}
LOGGER.trace("Signature validation: algorithm={} calculated={} provided={}",
algorithm, computed, digest);
if (digest == null && computed == null) {
return true;
} else if (digest == null || computed == null) {
return false;
} else {
// Use constant-time comparison to prevent timing attacks
return MessageDigest.isEqual(computed.getBytes(UTF_8), digest.getBytes(UTF_8));
}
}
}