Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0f5541f
Trigger secret won't use a default value
jorge-romero Jul 9, 2026
1aa6307
Add validation for cross-project pull requests in webhook handler
jorge-romero Jul 9, 2026
d60bd7a
Add test for rejecting cross-project pull requests in webhook handler
jorge-romero Jul 9, 2026
20d006b
Update Dockerfile to use specific image digest for ubi-micro and adju…
jorge-romero Jul 9, 2026
521785c
Remove trigger secret from logs.
jorge-romero Jul 9, 2026
cfa2090
Enhance validation and escaping in webhook handler and template proce…
jorge-romero Jul 10, 2026
58d9579
Add validation for jenkinsfile_path parameter in webhook handler
jorge-romero Jul 10, 2026
ceecf02
Validate target project in webhook handler and update test payloads f…
jorge-romero Jul 10, 2026
40347d9
Fix as the cluster assign an user and provides conflicts
jorge-romero Jul 10, 2026
ebd40e9
Add redirect handling to secure HTTP client to prevent token leakage
jorge-romero Jul 13, 2026
f6e8c18
Enhance branch protection checks to be case-insensitive in webhook ha…
jorge-romero Jul 13, 2026
a9d1501
Use constant-time comparison for trigger_secret in webhook handler to…
jorge-romero Jul 13, 2026
9890feb
Edit Changelog
jorge-romero Jul 13, 2026
f5caae5
Fix linting problems in test file.
jorge-romero Jul 13, 2026
0ab87a7
Add IP range validation for webhook requests
jorge-romero Jul 13, 2026
0fefb19
Enhance security
jorge-romero Jul 20, 2026
49f39a2
Implement HMAC secret generation and verification for Bitbucket webho…
jorge-romero Jul 22, 2026
874f347
Add WEBHOOK_HMAC_SECRET_B64 parameter and reference in webhook proxy …
jorge-romero Jul 22, 2026
813bfbb
Add WEBHOOK_HMAC_SECRET to configuration and implement HMAC signing f…
jorge-romero Jul 23, 2026
9407636
Add webhook HMAC secret handling to Jenkins setup scripts
jorge-romero Jul 27, 2026
5724e02
Add HMAC secret handling to Jenkins test scripts and update dependencies
jorge-romero Jul 27, 2026
8391324
Add script to migrate Bitbucket webhooks to HMAC secret handling
jorge-romero Jul 28, 2026
f0a5ff0
Add script to migrate webhook-proxy secrets with HMAC support
jorge-romero Jul 28, 2026
2e3d8f8
Add migration scripts for webhook proxy and Jira properties
jorge-romero Jul 29, 2026
ef1fc68
Receive Hmac key and pass it to tailor on cd namespace creation
jafarre-bi Jul 29, 2026
1f93ea9
Receive b64-encoded hmac secret
jafarre-bi Jul 30, 2026
4892cd3
Pass webhook allowed IP ranges to tailor
jafarre-bi Aug 2, 2026
2ad6494
Merge branch 'feature/EDPC-5530' into feature/hmac
jafarre-bi Aug 2, 2026
393577a
Rename tailor template parameters
jafarre-bi Aug 3, 2026
96eefda
Remove UUID validation for trigger secret and enforce non-empty check
jorge-romero Aug 3, 2026
3f305f6
Revert the removal of the CD_PASSWORD env variable
jafarre-bi Aug 6, 2026
38ab97e
Set the allowed IP ranges and fix the Hmac secret
jafarre-bi Aug 19, 2026
c449989
Fix webhook migration scripts to work in windows and avoid writing th…
jafarre-bi Aug 20, 2026
96e2dd6
Migrate provapp data
jafarre-bi Aug 23, 2026
02db3a9
Omit the output of curl to hide secret
jafarre-bi Aug 24, 2026
b857dc6
Rollout the webhook proxy and the provapp after migration
jafarre-bi Aug 26, 2026
9113ba4
Ignore errors when rolling out apps
jafarre-bi Aug 27, 2026
a92184b
Add the env var WEBHOOK_HMAC_SECRET to the webhook-proxy
jafarre-bi Aug 27, 2026
5aa37ee
Update change log
jafarre-bi Aug 28, 2026
3d23ab6
Fix tests
jafarre-bi Aug 28, 2026
1d74cda
Fix tests
jafarre-bi Aug 28, 2026
1e896f5
Fix create-cd-jenkins and tests
jafarre-bi Aug 28, 2026
3ac17a5
Fix tests
jafarre-bi Aug 28, 2026
9bc7848
Fix tests
jafarre-bi Aug 28, 2026
62c2027
Fix tests
jafarre-bi Aug 28, 2026
63ba5c3
Merge branch 'master' into feature/hmac
jafarre-bi Aug 28, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add client credentials support to external services templates in the ODS API Service ([#1388](https://github.com/opendevstack/ods-core/pull/1388))

### Changed
- Adapt webhook proxy to use HMAC ([#1403](https://github.com/opendevstack/ods-core/pull/1403))

### Fixed
- Fixes VIT0089540 ([#1395](https://github.com/opendevstack/ods-core/pull/1395))
Expand Down
4 changes: 4 additions & 0 deletions configuration-sample/ods-core.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ OPENSHIFT_APPS_BASEDOMAIN=.192.168.56.101.nip.io
PIPELINE_TRIGGER_SECRET_B64=changeme_base64
PIPELINE_TRIGGER_SECRET=changeme

# Webhook HMAC Secret used to sign and verify requests to/from the webhook proxy.
# Use a high-entropy random value (e.g. generated by generate_hmac_secret.sh).
WEBHOOK_HMAC_SECRET=changeme

###########
# Storage #
###########
Expand Down
7 changes: 6 additions & 1 deletion create-projects/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def projectId = env.PROJECT_ID
def projectAdmins = env.PROJECT_ADMIN ?: ''
def projectGroups = env.PROJECT_GROUPS ?: ''
def pipelineTriggerSecret = env.PIPELINE_TRIGGER_SECRET
def webhookHmacKey = env.WEBHOOK_HMAC_KEY
def webhookAllowedIpRanges = env.WEBHOOK_ALLOWED_IP_RANGES
def cdUserIdB64 = env.CD_USER_ID_B64
def cdUserType = env.CD_USER_TYPE
if (!(projectId ==~ /^[A-Za-z\d]+$/)) {
Expand Down Expand Up @@ -102,6 +104,8 @@ podTemplate(
--ods-image-tag=${odsImageTag} \
--ods-bitbucket-project ${odsBitbucketProject} \
--pipeline-trigger-secret-b64=${pipelineTriggerSecret} \
--webhook-hmac-key-b64=${webhookHmacKey} \
--webhook-allowed-ip-ranges=${webhookAllowedIpRanges} \
--cd-user-type=${cdUserType} \
--cd-user-id-b64=${cdUserIdB64}""",
label: 'Setup Jenkins in CD project'
Expand Down Expand Up @@ -178,7 +182,7 @@ podTemplate(
env.POST_SCRIPTS_ENABLED = 'false'
return
}

echo("Scripts repository: ${scriptsRepoUrl}")
echo("Scripts branch: ${scriptsBranch}")
echo("Scripts order: ${scriptsOrder}")
Expand Down Expand Up @@ -251,6 +255,7 @@ podTemplate(
"BITBUCKET_URL=${bitbucketUrl}",
"DOCKER_REGISTRY=${dockerRegistry}",
"CD_USERNAME=${username}",
"CD_PASSWORD=${password}",
"CD_USER_CREDENTIALS_ID=${credentialsId}"
]) {
echo("Environment variables for scripts:")
Expand Down
22 changes: 22 additions & 0 deletions create-projects/create-cd-jenkins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ PROJECT_ID=""
CD_USER_TYPE=""
CD_USER_ID_B64=""
PIPELINE_TRIGGER_SECRET_B64=""
WEBHOOK_HMAC_KEY_B64=""
WEBHOOK_ALLOWED_IP_RANGES=""
TAILOR_VERBOSE=""
TAILOR_NON_INTERACTIVE=""

Expand All @@ -31,6 +33,8 @@ function usage {
printf "\t--ods-image-tag\t\t\tThe image tag to use. Default: %s\n" "${ODS_IMAGE_TAG}"
printf "\t--ods-bitbucket-project\t\t\tThe Bitbucket project to use. Default: %s\n" "${ODS_BITBUCKET_PROJECT}"
printf "\t--pipeline-trigger-secret-b64\tTrigger secret for pipelines (base64 encoded)\n"
printf "\t--webhook-hmac-key-b64\tHMAC key for the webhook proxy (base64 encoded)\n"
printf "\t--webhook-allowed-ip-ranges\tAllowed IP ranges for the webhook proxy\n"
printf "\t--cd-user-type\t\t\tWhether CD user is general or project specific\n"
printf "\t--cd-user-id-b64\t\tName of CD user (base64 encoded)\n"
}
Expand Down Expand Up @@ -60,6 +64,12 @@ while [[ "$#" -gt 0 ]]; do case $1 in
--pipeline-trigger-secret-b64=*) PIPELINE_TRIGGER_SECRET_B64="${1#*=}";;
--pipeline-trigger-secret-b64) PIPELINE_TRIGGER_SECRET_B64="$2"; shift;;

--webhook-hmac-key-b64=*) WEBHOOK_HMAC_KEY_B64="${1#*=}";;
--webhook-hmac-key-b64) WEBHOOK_HMAC_KEY_B64="$2"; shift;;

--webhook-allowed-ip-ranges=*) WEBHOOK_ALLOWED_IP_RANGES="${1#*=}";;
--webhook-allowed-ip-ranges) WEBHOOK_ALLOWED_IP_RANGES="$2"; shift;;

--cd-user-type=*) CD_USER_TYPE="${1#*=}";;
--cd-user-type) CD_USER_TYPE="$2"; shift;;

Expand All @@ -81,6 +91,16 @@ if [ -z "${PIPELINE_TRIGGER_SECRET_B64}" ]; then
exit 1
fi

if [ -z "${WEBHOOK_HMAC_KEY_B64}" ]; then
echo "--webhook-hmac-key-b64 is missing, but required"; usage
exit 1
fi

if [ -z "${WEBHOOK_ALLOWED_IP_RANGES}" ]; then
echo "--webhook-allowed-ip-ranges is missing, but required"; usage
exit 1
fi

if [ -z "${CD_USER_TYPE}" ]; then
echo "--cd-user-type is missing, but required"; usage
exit 1
Expand Down Expand Up @@ -111,6 +131,8 @@ cd "${ODS_CORE_DIR}/jenkins/ocp-config/deploy"
${TAILOR} ${TAILOR_VERBOSE} ${TAILOR_NON_INTERACTIVE} apply \
"--namespace=${PROJECT_ID}-cd" \
"--param=PIPELINE_TRIGGER_SECRET_B64=${PIPELINE_TRIGGER_SECRET_B64}" \
"--param=WEBHOOK_HMAC_KEY_B64=${WEBHOOK_HMAC_KEY_B64}" \
"--param=WEBHOOK_ALLOWED_IP_RANGES=${WEBHOOK_ALLOWED_IP_RANGES}" \
"--param=PROJECT=${PROJECT_ID}" \
"--param=CD_USER_ID_B64=${CD_USER_ID_B64}" \
"--param=ODS_NAMESPACE=${ODS_NAMESPACE}" \
Expand Down
8 changes: 6 additions & 2 deletions create-projects/tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ echo "=== create-cd-jenkins: With general CD user ==="

tailor mock --receive='version' --stdout='1.3.4'

tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELINE_TRIGGER_SECRET_B64=czNjcjN0 --param=PROJECT=foo --param=CD_USER_ID_B64=Y2RfdXNlcg== --param=ODS_NAMESPACE=bar --param=ODS_IMAGE_TAG=3.x --param=ODS_BITBUCKET_PROJECT=opendevstack --selector template=ods-jenkins-template' --times 1
tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELINE_TRIGGER_SECRET_B64=czNjcjN0 --param=WEBHOOK_HMAC_KEY_B64=d2ViaG9vay1zZWNyZXQ= --param=WEBHOOK_ALLOWED_IP_RANGES=0.0.0.0/0 --param=PROJECT=foo --param=CD_USER_ID_B64=Y2RfdXNlcg== --param=ODS_NAMESPACE=bar --param=ODS_IMAGE_TAG=3.x --param=ODS_BITBUCKET_PROJECT=opendevstack --selector template=ods-jenkins-template' --times 1

../create-cd-jenkins.sh \
--project foo \
Expand All @@ -106,6 +106,8 @@ tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELI
--ods-image-tag=3.x \
--ods-bitbucket-project=opendevstack \
--pipeline-trigger-secret-b64=$(echo -n "s3cr3t" | base64) \
--webhook-hmac-key-b64=$(echo -n "webhook-secret" | base64) \
--webhook-allowed-ip-ranges='0.0.0.0/0' \
--cd-user-type=general \
--cd-user-id-b64=$(echo -n "cd_user" | base64) \

Expand All @@ -116,7 +118,7 @@ echo "=== create-cd-jenkins: With project-specific CD user ==="

tailor mock --receive='version' --stdout='1.3.4'

tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELINE_TRIGGER_SECRET_B64=czNjcjN0 --param=PROJECT=foo --param=CD_USER_ID_B64=Zm9v --param=ODS_NAMESPACE=bar --param=ODS_IMAGE_TAG=3.x --param=ODS_BITBUCKET_PROJECT=opendevstack --param=CD_USER_PWD_B64=Y2hhbmdlbWU= --selector template=ods-jenkins-template' --times 1
tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELINE_TRIGGER_SECRET_B64=czNjcjN0 --param=WEBHOOK_HMAC_KEY_B64=d2ViaG9vay1zZWNyZXQ= --param=WEBHOOK_ALLOWED_IP_RANGES=0.0.0.0/0 --param=PROJECT=foo --param=CD_USER_ID_B64=Zm9v --param=ODS_NAMESPACE=bar --param=ODS_IMAGE_TAG=3.x --param=ODS_BITBUCKET_PROJECT=opendevstack --param=CD_USER_PWD_B64=Y2hhbmdlbWU= --selector template=ods-jenkins-template' --times 1

../create-cd-jenkins.sh \
--project foo \
Expand All @@ -125,6 +127,8 @@ tailor mock --receive='--non-interactive apply --namespace=foo-cd --param=PIPELI
--ods-image-tag=3.x \
--ods-bitbucket-project=opendevstack \
--pipeline-trigger-secret-b64=$(echo -n "s3cr3t" | base64) \
--webhook-hmac-key-b64=$(echo -n "webhook-secret" | base64) \
--webhook-allowed-ip-ranges='0.0.0.0/0' \
--cd-user-type=specific \
--cd-user-id-b64=$(echo -n "foo" | base64) \

Expand Down
12 changes: 10 additions & 2 deletions jenkins/ocp-config/deploy/jenkins-webhook-proxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ parameters:
required: true
- name: PIPELINE_TRIGGER_SECRET_B64
required: true
- name: WEBHOOK_HMAC_KEY_B64
required: true
- name: ODS_BITBUCKET_PROJECT
required: true
- name: MAX_DELETION_CHECKS
value: '10'
required: true
- name: ALLOWED_WEBHOOK_IP_RANGES
- name: WEBHOOK_ALLOWED_IP_RANGES
description: >-
Comma-separated list of allowed source IP addresses or CIDR ranges for
incoming webhook requests (e.g. "185.166.140.0/22,10.0.0.1").
Expand Down Expand Up @@ -111,12 +113,17 @@ objects:
- name: MAX_DELETION_CHECKS
value: ${MAX_DELETION_CHECKS}
- name: ALLOWED_WEBHOOK_IP_RANGES
value: ${ALLOWED_WEBHOOK_IP_RANGES}
value: ${WEBHOOK_ALLOWED_IP_RANGES}
- name: TRIGGER_SECRET
valueFrom:
secretKeyRef:
key: trigger-secret
name: webhook-proxy
- name: WEBHOOK_HMAC_SECRET
valueFrom:
secretKeyRef:
key: webhook-hmac-secret
name: webhook-proxy
image: '${DOCKER_REGISTRY}/${ODS_NAMESPACE}/jenkins-webhook-proxy:${ODS_IMAGE_TAG}'
imagePullPolicy: Always
name: webhook-proxy
Expand Down Expand Up @@ -150,3 +157,4 @@ objects:
type: opaque
data:
trigger-secret: ${PIPELINE_TRIGGER_SECRET_B64}
webhook-hmac-secret: ${WEBHOOK_HMAC_KEY_B64}
30 changes: 30 additions & 0 deletions jenkins/webhook-proxy/generate_hmac_secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail

# Generates a high-entropy secret suitable for WEBHOOK_HMAC_SECRET.
# Usage:
# bash generate_hmac_secret.sh
# bash generate_hmac_secret.sh 64

length="${1:-64}"

if ! [[ "$length" =~ ^[0-9]+$ ]]; then
echo "error: length must be a positive integer" >&2
exit 1
fi

if (( length < 32 )); then
echo "error: length must be at least 32 characters" >&2
exit 1
fi

if command -v openssl >/dev/null 2>&1; then
secret=$(openssl rand -hex $(((length + 1) / 2)) | cut -c1-"$length")
elif [[ -r /dev/urandom ]]; then
secret=$(tr -dc 'a-f0-9' </dev/urandom | head -c "$length")
else
echo "error: neither openssl nor /dev/urandom is available" >&2
exit 1
fi

printf 'WEBHOOK_HMAC_SECRET=%s\n' "$secret"
84 changes: 72 additions & 12 deletions jenkins/webhook-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package main

import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"log"
"math/rand"
Expand All @@ -33,9 +36,6 @@ var (
// which implicitly rejects absolute paths ("/..."), path traversal ("../"),
// and hidden-file tricks (".").
safeJenkinsfilePathRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*(?:/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$`)
// safeTriggerSecretRegex matches a UUID as produced by Java's
// UUID.randomUUID().toString(): 8-4-4-4-12 lowercase hex digits.
safeTriggerSecretRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
)

const (
Expand All @@ -46,6 +46,7 @@ const (
pipelineConfigFilename = "pipeline.json.tmpl"
repoBaseEnvVar = "REPO_BASE"
triggerSecretEnvVar = "TRIGGER_SECRET"
webhookHMACSecretEnvVar = "WEBHOOK_HMAC_SECRET"
jenkinsfilePathDefault = "Jenkinsfile"
protectedBranchesEnvVar = "PROTECTED_BRANCHES"
protectedBranchesDefault = "master,develop,production,staging,release/"
Expand All @@ -62,6 +63,7 @@ const (
allowedChangeRefTypesEnvVar = "ALLOWED_CHANGE_REF_TYPES"
allowedChangeRefTypesDefault = "BRANCH"
allowedWebhookIPRangesEnvVar = "ALLOWED_WEBHOOK_IP_RANGES"
bitbucketSignatureHeader = "X-Hub-Signature"
namespaceSuffix = "-cd"
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
Expand Down Expand Up @@ -143,6 +145,7 @@ type Server struct {
Client Client
Namespace string
Project string
WebhookHMACSecret string
TriggerSecret string
ProtectedBranches []string
AcceptedEvents []string
Expand Down Expand Up @@ -194,8 +197,13 @@ func main() {
}

triggerSecret := os.Getenv(triggerSecretEnvVar)
if !safeTriggerSecretRegex.MatchString(triggerSecret) {
log.Fatalln("Trigger secret must be a valid UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).")
if len(triggerSecret) == 0 {
log.Fatalln(triggerSecretEnvVar, "must be set")
}

webhookHMACSecret := os.Getenv(webhookHMACSecretEnvVar)
if len(webhookHMACSecret) == 0 {
log.Fatalln(webhookHMACSecretEnvVar, "must be set")
}

openShiftAPIHost := os.Getenv(openShiftAPIHostEnvVar)
Expand Down Expand Up @@ -295,6 +303,7 @@ func main() {
Client: client,
Namespace: namespace,
Project: project,
WebhookHMACSecret: webhookHMACSecret,
TriggerSecret: triggerSecret,
ProtectedBranches: protectedBranches,
AcceptedEvents: acceptedEvents,
Expand Down Expand Up @@ -373,14 +382,22 @@ func (s *Server) HandleRoot() http.HandlerFunc {
return
}

queryValues := r.URL.Query()
triggerSecretParam := queryValues.Get("trigger_secret")
if subtle.ConstantTimeCompare([]byte(triggerSecretParam), []byte(s.TriggerSecret)) != 1 {
log.Println(requestID, "trigger_secret param not given / not matching")
body, readErr := io.ReadAll(r.Body)
if readErr != nil {
log.Println(requestID, "Cannot read request body:", readErr)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}

signatureErr := verifyBitbucketPayloadSignature(s.WebhookHMACSecret, body, r.Header.Get(bitbucketSignatureHeader))
if signatureErr != nil {
log.Println(requestID, "Bitbucket payload signature rejected:", signatureErr)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

queryValues := r.URL.Query()

jenkinsfilePath := jenkinsfilePathDefault
jenkinsfilePathParam := queryValues.Get("jenkinsfile_path")
if jenkinsfilePathParam != "" {
Expand All @@ -399,7 +416,7 @@ func (s *Server) HandleRoot() http.HandlerFunc {

if strings.HasPrefix(r.URL.Path, "/build") {
req := &requestBuild{}
err := json.NewDecoder(r.Body).Decode(req)
err := json.Unmarshal(body, req)
if err != nil {
msg := fmt.Sprintf("Cannot parse JSON: %s", err)
log.Println(requestID, msg)
Expand Down Expand Up @@ -431,9 +448,8 @@ func (s *Server) HandleRoot() http.HandlerFunc {
}

} else if r.URL.Path == "/" {

req := &requestBitbucket{}
err := json.NewDecoder(r.Body).Decode(req)
err := json.Unmarshal(body, req)
if err != nil {
msg := fmt.Sprintf("Cannot parse JSON: %s", err)
log.Println(requestID, msg)
Expand Down Expand Up @@ -1121,6 +1137,50 @@ func makePipelineName(project string, component string, branch string) string {
return pipeline
}

func verifyBitbucketPayloadSignature(secret string, payload []byte, signatureHeader string) error {
if signatureHeader == "" {
return errors.New("missing signature header")
}

parts := strings.SplitN(signatureHeader, "=", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return errors.New("invalid signature header format")
}

algorithm := strings.ToLower(parts[0])
expectedDigest, err := computeHMACDigest(algorithm, secret, payload)
if err != nil {
return err
}

providedDigest := strings.ToLower(parts[1])
if subtle.ConstantTimeCompare([]byte(providedDigest), []byte(expectedDigest)) != 1 {
return errors.New("signature mismatch")
}

return nil
}

func computeHMACDigest(algorithm string, secret string, payload []byte) (string, error) {
var mac hash.Hash

switch algorithm {
case "sha256":
mac = hmac.New(sha256.New, []byte(secret))
case "sha1":
mac = hmac.New(sha1.New, []byte(secret))
default:
return "", fmt.Errorf("unsupported signature algorithm: %s", algorithm)
}

_, err := mac.Write(payload)
if err != nil {
return "", err
}

return fmt.Sprintf("%x", mac.Sum(nil)), nil
}

func isProtectedBranch(protectedBranches []string, branch string) bool {
for _, b := range protectedBranches {
if b == "*" {
Expand Down
Loading
Loading