Verify GitHub webhook signature

This commit is contained in:
Stéphane Bidoul 2022-01-29 16:32:39 +01:00
parent b89a4a0fe8
commit dff8bbed02
No known key found for this signature in database
GPG key ID: BCAB2555446B5B92
6 changed files with 44 additions and 2 deletions

View file

@ -7,4 +7,5 @@ RUNBOAT_BUILD_ENV={}
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"} RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"}
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"} RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"}
RUNBOAT_GITHUB_TOKEN= RUNBOAT_GITHUB_TOKEN=
RUNBOAT_GITHUB_WEBHOOK_SECRET=
RUNBOAT_LOG_CONFIG=log-config.yaml RUNBOAT_LOG_CONFIG=log-config.yaml

View file

@ -25,6 +25,7 @@ dynamic = ["version", "description"]
[project.optional-dependencies] [project.optional-dependencies]
test = [ test = [
"pytest", "pytest",
"pytest-asyncio",
"pytest-cov", "pytest-cov",
"pytest-dotenv", "pytest-dotenv",
"pytest-mock", "pytest-mock",
@ -42,7 +43,7 @@ profile = 'black'
[tool.pytest.ini_options] [tool.pytest.ini_options]
env_override_existing_values = 1 env_override_existing_values = 1
env_files = [".env.test"] env_files = [".env.test"]
asyncio_mode = "strict"
# flake8 config is in .flake8 # flake8 config is in .flake8
[tool.mypy] [tool.mypy]

View file

@ -7,6 +7,7 @@ pluggy==1.0.0
py==1.11.0 py==1.11.0
pyparsing==3.0.7 pyparsing==3.0.7
pytest==6.2.5 pytest==6.2.5
pytest-asyncio==0.17.2
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-dotenv==0.5.2 pytest-dotenv==0.5.2
pytest-mock==3.6.1 pytest-mock==3.6.1

View file

@ -54,6 +54,8 @@ class Settings(BaseSettings):
# The token to use for the GitHub api calls (to query branches and pull requests, # The token to use for the GitHub api calls (to query branches and pull requests,
# and report build statuses). # and report build statuses).
github_token: str | None github_token: str | None
# The secret used to verify GitHub webhook signatures
github_webhook_secret: bytes | None
# The file with the python logging configuration to use for the runboat controller. # The file with the python logging configuration to use for the runboat controller.
log_config: str | None log_config: str | None
# The base url where the runboat UI and API is exposed on internet. # The base url where the runboat UI and API is exposed on internet.

View file

@ -1,3 +1,4 @@
import hmac
import logging import logging
from fastapi import APIRouter, BackgroundTasks, Header, Request from fastapi import APIRouter, BackgroundTasks, Header, Request
@ -11,13 +12,33 @@ _logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
def _verify_github_signature(
x_hub_signature_256: str | None, secret: bytes | None, body: bytes
) -> bool:
if not secret:
return True
if not x_hub_signature_256:
_logger.warning("Got payload without X-Hub-Signature-256")
return False
signature = "sha256=" + hmac.new(secret, body, "sha256").hexdigest()
if not hmac.compare_digest(signature, x_hub_signature_256):
_logger.warning("Got payload with invalid X-Hub-Signature-256")
return False
return True
@router.post("/webhooks/github") @router.post("/webhooks/github")
async def receive_payload( async def receive_payload(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
request: Request, request: Request,
x_github_event: str = Header(...), x_github_event: str = Header(...),
x_hub_signature_256: str | None = Header(None),
) -> None: ) -> None:
# TODO check x-hub-signature body = await request.body()
if not _verify_github_signature(
x_hub_signature_256, settings.github_webhook_secret, body
):
return
payload = await request.json() payload = await request.json()
if x_github_event == "pull_request": if x_github_event == "pull_request":
repo = payload["repository"]["full_name"] repo = payload["repository"]["full_name"]

View file

@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
from runboat.app import app from runboat.app import app
from runboat.controller import controller from runboat.controller import controller
from runboat.github import CommitInfo from runboat.github import CommitInfo
from runboat.webhooks import _verify_github_signature
client = TestClient(app) client = TestClient(app)
@ -137,3 +138,18 @@ def test_webhook_github_pr_close(mocker: MockerFixture) -> None:
repo="oca/mis-builder", repo="oca/mis-builder",
pr=381, pr=381,
) )
def test_verify_github_signature() -> None:
assert _verify_github_signature(None, None, b"body") # no secret configured, ok
assert not _verify_github_signature(
None, b"secret", b"body"
) # no X-Hub-Signature-256
assert not _verify_github_signature(
"sha256=invalid-sig", b"secret", b"body"
) # no X-Hub-Signature-256
assert _verify_github_signature(
"sha256=dc46983557fea127b43af721467eb9b3fde2338fe3e14f51952aa8478c13d355",
b"secret",
b"body",
)