Skip to content

Commit 6b2e03e

Browse files
🩹 [Patch]: Enhance Test-GitHubWebhookSignature to support a full request object + Context bump (#482)
## Description This pull request introduces several updates across multiple files, focusing on enhancing functionality, improving documentation, and updating dependencies. The most significant changes include updates to the `Test-GitHubWebhookSignature` function for better flexibility and security and the upgrade of required module versions. ### Functional Updates * `Test-GitHubWebhookSignature`: - Added support for validating webhook requests using the entire `Request` object, enabling automatic extraction of body and headers. - Updated descriptions to clarify the use of SHA-256 and added examples demonstrating validation with the `Request` object. ### Dependency Updates - Updated `#Requires` statements across multiple files to require version `8.1.1` of the `Context` module. The update fixes an issue where the GitHub module attempted to save a context with null values would throw a null-pointer exception. ### Test Enhancements - Expanded test coverage for `Test-GitHubWebhookSignature`, including scenarios for valid signatures, invalid signatures, and missing headers in the `Request` object. ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [x] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas
1 parent 465671b commit 6b2e03e

File tree

10 files changed

+140
-52
lines changed

10 files changed

+140
-52
lines changed

.github/PSModule.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
Test:
22
CodeCoverage:
33
PercentTarget: 50
4+
# TestResults:
5+
# Skip: true
6+
# SourceCode:
7+
# Skip: true
8+
# PSModule:
9+
# Skip: true
10+
# Module:
11+
# Windows:
12+
# Skip: true
13+
# MacOS:
14+
# Skip: true
15+
# Build:
16+
# Docs:
17+
# Skip: true

src/functions/private/Auth/Context/Remove-GitHubContext.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@
4242
Write-Debug "[$stackPath] - End"
4343
}
4444
}
45-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
45+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/private/Auth/Context/Set-GitHubContext.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,4 @@
168168
Write-Debug "[$stackPath] - End"
169169
}
170170
}
171-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
171+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/private/Config/Initialize-GitHubConfig.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@
7575
Write-Debug "[$stackPath] - End"
7676
}
7777
}
78-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
78+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/public/Auth/Context/Get-GitHubContext.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@
9292
Write-Debug "[$stackPath] - End"
9393
}
9494
}
95-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
95+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/public/Config/Get-GitHubConfig.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@
4747
Write-Debug "[$stackPath] - End"
4848
}
4949
}
50-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
50+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/public/Config/Remove-GitHubConfig.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@
4444
Write-Debug "[$stackPath] - End"
4545
}
4646
}
47-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
47+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/public/Config/Set-GitHubConfig.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@
5454
Write-Debug "[$stackPath] - End"
5555
}
5656
}
57-
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.0' }
57+
#Requires -Modules @{ ModuleName = 'Context'; RequiredVersion = '8.1.1' }

src/functions/public/Webhooks/Test-GitHubWebhookSignature.ps1

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
66
.DESCRIPTION
77
This function validates the integrity and authenticity of a GitHub webhook request by comparing
8-
the received HMAC SHA-256 signature against a computed hash of the payload using a shared secret.
9-
It uses a constant-time comparison to mitigate timing attacks and returns a boolean indicating
10-
whether the signature is valid.
8+
the received HMAC signature against a computed hash of the payload using a shared secret.
9+
It uses the SHA-256 algorithm and employs a constant-time comparison to mitigate
10+
timing attacks. The function returns a boolean indicating whether the signature is valid.
1111
1212
.EXAMPLE
1313
Test-GitHubWebhookSignature -Secret $env:WEBHOOK_SECRET -Body $Request.RawBody -Signature $Request.Headers['X-Hub-Signature-256']
@@ -19,6 +19,16 @@
1919
2020
Validates the provided webhook payload against the HMAC SHA-256 signature using the given secret.
2121
22+
.EXAMPLE
23+
Test-GitHubWebhookSignature -Secret $env:WEBHOOK_SECRET -Request $Request
24+
25+
Output:
26+
```powershell
27+
True
28+
```
29+
30+
Validates the webhook request using the entire request object, automatically extracting the body and signature.
31+
2232
.OUTPUTS
2333
bool
2434
@@ -29,11 +39,12 @@
2939
.LINK
3040
https://psmodule.io/GitHub/Functions/Webhooks/Test-GitHubWebhookSignature
3141
32-
.LINK
33-
https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries
42+
.NOTES
43+
[Validating Webhook Deliveries | GitHub Docs](https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries)
44+
[Webhook events and payloads | GitHub Docs](https://docs.github.com/en/webhooks/webhook-events-and-payloads)
3445
#>
3546
[OutputType([bool])]
36-
[CmdletBinding()]
47+
[CmdletBinding(DefaultParameterSetName = 'ByBody')]
3748
param (
3849
# The secret key used to compute the HMAC hash.
3950
# Example: 'mysecret'
@@ -43,25 +54,59 @@
4354
# The JSON body of the GitHub webhook request.
4455
# This must be the compressed JSON payload received from GitHub.
4556
# Example: '{"action":"opened"}'
46-
[Parameter(Mandatory)]
57+
[Parameter(Mandatory, ParameterSetName = 'ByBody')]
4758
[string] $Body,
4859

4960
# The signature received from GitHub to compare against.
5061
# Example: 'sha256=abc123...'
51-
[Parameter(Mandatory)]
52-
[string] $Signature
62+
[Parameter(Mandatory, ParameterSetName = 'ByBody')]
63+
[string] $Signature,
64+
65+
# The entire request object containing RawBody and Headers.
66+
# Used in Azure Function Apps or similar environments.
67+
[Parameter(Mandatory, ParameterSetName = 'ByRequest')]
68+
[PSObject] $Request
5369
)
5470

55-
$keyBytes = [Text.Encoding]::UTF8.GetBytes($Secret)
56-
$payloadBytes = [Text.Encoding]::UTF8.GetBytes($Body)
71+
begin {
72+
$stackPath = Get-PSCallStackPath
73+
Write-Debug "[$stackPath] - Start"
74+
}
5775

58-
$hmac = [System.Security.Cryptography.HMACSHA256]::new()
59-
$hmac.Key = $keyBytes
60-
$hashBytes = $hmac.ComputeHash($payloadBytes)
61-
$computedSignature = 'sha256=' + (($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '')
76+
process {
77+
# Handle parameter sets
78+
if ($PSCmdlet.ParameterSetName -eq 'ByRequest') {
79+
$Body = $Request.RawBody
80+
$Signature = $Request.Headers['X-Hub-Signature-256']
6281

63-
[System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
64-
[Text.Encoding]::UTF8.GetBytes($computedSignature),
65-
[Text.Encoding]::UTF8.GetBytes($Signature)
66-
)
82+
# If signature not found, throw an error
83+
if (-not $Signature) {
84+
throw "No webhook signature found in request headers. Expected 'X-Hub-Signature-256' for SHA256 algorithm."
85+
}
86+
}
87+
88+
$keyBytes = [Text.Encoding]::UTF8.GetBytes($Secret)
89+
$payloadBytes = [Text.Encoding]::UTF8.GetBytes($Body)
90+
91+
# Create HMAC SHA256 object
92+
$hmac = [System.Security.Cryptography.HMACSHA256]::new()
93+
$algorithmPrefix = 'sha256='
94+
95+
$hmac.Key = $keyBytes
96+
$hashBytes = $hmac.ComputeHash($payloadBytes)
97+
$computedSignature = $algorithmPrefix + (($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '')
98+
99+
# Dispose of the HMAC object
100+
$hmac.Dispose()
101+
102+
[System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
103+
[Text.Encoding]::UTF8.GetBytes($computedSignature),
104+
[Text.Encoding]::UTF8.GetBytes($Signature)
105+
)
106+
}
107+
108+
end {
109+
Write-Debug "[$stackPath] - End"
110+
}
67111
}
112+

tests/GitHub.Tests.ps1

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,37 @@ Describe 'Auth' {
179179
}
180180
}
181181

182+
Describe 'Anonymous - Functions that can run anonymously' {
183+
It 'Get-GithubRateLimit - Using -Anonymous' {
184+
$rateLimit = Get-GitHubRateLimit -Anonymous
185+
LogGroup 'Rate Limit' {
186+
Write-Host ($rateLimit | Format-Table | Out-String)
187+
}
188+
$rateLimit | Should -Not -BeNullOrEmpty
189+
}
190+
It 'Invoke-GitHubAPI - Using -Anonymous' {
191+
$rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Anonymous | Select-Object -ExpandProperty Response
192+
LogGroup 'Rate Limit' {
193+
Write-Host ($rateLimit | Format-Table | Out-String)
194+
}
195+
$rateLimit | Should -Not -BeNullOrEmpty
196+
}
197+
It 'Get-GithubRateLimit - Using -Context Anonymous' {
198+
$rateLimit = Get-GitHubRateLimit -Context Anonymous
199+
LogGroup 'Rate Limit' {
200+
Write-Host ($rateLimit | Format-List | Out-String)
201+
}
202+
$rateLimit | Should -Not -BeNullOrEmpty
203+
}
204+
It 'Invoke-GitHubAPI - Using -Context Anonymous' {
205+
$rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Context Anonymous | Select-Object -ExpandProperty Response
206+
LogGroup 'Rate Limit' {
207+
Write-Host ($rateLimit | Format-Table | Out-String)
208+
}
209+
$rateLimit | Should -Not -BeNullOrEmpty
210+
}
211+
}
212+
182213
Describe 'GitHub' {
183214
Context 'Config' {
184215
It 'Get-GitHubConfig - Gets the module configuration' {
@@ -780,42 +811,40 @@ Describe 'Emojis' {
780811
}
781812

782813
Describe 'Webhooks' {
783-
It 'Test-GitHubWebhookSignature - Validates the webhook payload using known correct signature' {
814+
BeforeAll {
784815
$secret = "It's a Secret to Everybody"
785816
$payload = 'Hello, World!'
786817
$signature = 'sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17'
818+
}
819+
820+
It 'Test-GitHubWebhookSignature - Validates the webhook payload using known correct signature (SHA256)' {
787821
$result = Test-GitHubWebhookSignature -Secret $secret -Body $payload -Signature $signature
788822
$result | Should -Be $true
789823
}
790-
}
791824

792-
Describe 'Anonymous - Functions that can run anonymously' {
793-
It 'Get-GithubRateLimit - Using -Anonymous' {
794-
$rateLimit = Get-GitHubRateLimit -Anonymous
795-
LogGroup 'Rate Limit' {
796-
Write-Host ($rateLimit | Format-Table | Out-String)
797-
}
798-
$rateLimit | Should -Not -BeNullOrEmpty
799-
}
800-
It 'Invoke-GitHubAPI - Using -Anonymous' {
801-
$rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Anonymous | Select-Object -ExpandProperty Response
802-
LogGroup 'Rate Limit' {
803-
Write-Host ($rateLimit | Format-Table | Out-String)
825+
It 'Test-GitHubWebhookSignature - Validates the webhook using Request object' {
826+
$mockRequest = [PSCustomObject]@{
827+
RawBody = $payload
828+
Headers = @{
829+
'X-Hub-Signature-256' = $signature
830+
}
804831
}
805-
$rateLimit | Should -Not -BeNullOrEmpty
832+
$result = Test-GitHubWebhookSignature -Secret $secret -Request $mockRequest
833+
$result | Should -Be $true
806834
}
807-
It 'Get-GithubRateLimit - Using -Context Anonymous' {
808-
$rateLimit = Get-GitHubRateLimit -Context Anonymous
809-
LogGroup 'Rate Limit' {
810-
Write-Host ($rateLimit | Format-List | Out-String)
811-
}
812-
$rateLimit | Should -Not -BeNullOrEmpty
835+
836+
It 'Test-GitHubWebhookSignature - Should fail with invalid signature' {
837+
$invalidSignature = 'sha256=invalid'
838+
$result = Test-GitHubWebhookSignature -Secret $secret -Body $payload -Signature $invalidSignature
839+
$result | Should -Be $false
813840
}
814-
It 'Invoke-GitHubAPI - Using -Context Anonymous' {
815-
$rateLimit = Invoke-GitHubAPI -ApiEndpoint '/rate_limit' -Context Anonymous | Select-Object -ExpandProperty Response
816-
LogGroup 'Rate Limit' {
817-
Write-Host ($rateLimit | Format-Table | Out-String)
841+
842+
It 'Test-GitHubWebhookSignature - Should throw when signature header is missing from request' {
843+
$mockRequest = [PSCustomObject]@{
844+
RawBody = $payload
845+
Headers = @{}
818846
}
819-
$rateLimit | Should -Not -BeNullOrEmpty
847+
848+
{ Test-GitHubWebhookSignature -Secret $secret -Request $mockRequest } | Should -Throw
820849
}
821850
}

0 commit comments

Comments
 (0)