Skip to content

Instantly share code, notes, and snippets.

@jpawlowski
Last active March 19, 2025 17:34
Show Gist options
  • Select an option

  • Save jpawlowski/b5c789980f59206b76a4d0f9809a8755 to your computer and use it in GitHub Desktop.

Select an option

Save jpawlowski/b5c789980f59206b76a4d0f9809a8755 to your computer and use it in GitHub Desktop.

Revisions

  1. jpawlowski revised this gist Mar 19, 2025. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -16,18 +16,18 @@ function Get-HmacSignedHeaders {
    Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings:
    1️⃣ Azure Key Vault (Recommended for cloud environments)
    1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments)
    Connect-AzAccount -Identity
    $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue
    2️⃣ Azure Automation Encrypted Variable (Automation Account)
    2️⃣ Azure Automation Encrypted Variable (inside Automation Account only)
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    3️⃣ Windows Credential Manager (with SecretManagement module)
    3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module)
    Import-Module Microsoft.PowerShell.SecretManagement
    $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    4️⃣ Encrypted local file with ACL (only in controlled environments)
    4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments)
    $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force
    ❌ For demonstration purposes only (NEVER hardcode secrets in production):
  2. jpawlowski revised this gist Mar 19, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -27,7 +27,7 @@ function Get-HmacSignedHeaders {
    Import-Module Microsoft.PowerShell.SecretManagement
    $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    4️⃣ Encrypted local file (only in controlled environments)
    4️⃣ Encrypted local file with ACL (only in controlled environments)
    $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force
    ❌ For demonstration purposes only (NEVER hardcode secrets in production):
  3. jpawlowski revised this gist Mar 19, 2025. 2 changed files with 49 additions and 58 deletions.
    103 changes: 47 additions & 56 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -4,15 +4,49 @@ function Test-HmacAuthorization {
    Verifies HMAC signature of incoming Azure Automation webhook requests.
    .DESCRIPTION
    Validates HMAC signature including timestamp, nonce, and body hash.
    Uses SecureString for shared secret, with proper secure cleanup. Supports SHA256 and SHA512.
    Nonce uniqueness checking is left open for future enhancement.
    Validates HMAC signature based on signed request headers (timestamp, nonce, content hash) and a shared secret.
    Supports both HMACSHA256 and HMACSHA512 algorithms.
    .FUNCTIONALITY
    Security, HMAC Authentication
    The shared secret is passed securely as a SecureString, converted as late as possible, and cleared from memory immediately after use.
    Signature verification includes:
    - Timestamp freshness validation (anti-replay window configurable via AllowedTimeDriftMinutes)
    - Host
    - Body content hash integrity check
    - Nonce inclusion for replay protection (future extension to store/check nonce is left open)
    .EXAMPLE
    # ✅ Example 1: Production (Azure Automation)
    # Retrieve encrypted variable securely
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    # Verify signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    [void]$sharedSecret.Dispose()
    throw "Unauthorized: Signature verification failed."
    }
    # Dispose secret after use
    [void]$sharedSecret.Dispose()
    .EXAMPLE
    Test-HmacAuthorization -SharedSecret (Get-AutomationVariable -Name 'SharedSecret') -WebhookData $WebhookData
    # ✅ Example 2: Local Development / Testing
    # Use test shared secret (DO NOT use hardcoded secrets in production)
    $sharedSecret = ConvertTo-SecureString -String "MyTestSecretKey" -AsPlainText -Force
    # Verify signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    Write-Output "❌ Signature invalid."
    } else {
    Write-Output "✅ Signature valid."
    }
    # Dispose secret
    [void]$sharedSecret.Dispose()
    .FUNCTIONALITY
    Security, HMAC Authentication
    .NOTES
    Author: Julian Pawlowski
    @@ -65,6 +99,12 @@ function Test-HmacAuthorization {
    }
    }

    # Host header check
    if ([string]::IsNullOrEmpty($headers.'Host')) {
    Write-Error 'Host header required'
    return $false
    }

    # Timestamp freshness check
    if ([string]::IsNullOrEmpty($headers.'x-ms-date')) {
    Write-Error 'x-ms-date header required'
    @@ -138,53 +178,4 @@ function Test-HmacAuthorization {
    [Text.Encoding]::UTF8.GetBytes($computedHmac),
    [Text.Encoding]::UTF8.GetBytes($receivedHmac)
    )
    }

    # ================================
    # 🔐 HMAC Authorization Verification Usage
    # ================================

    # ----------------------------------------
    # ✅ Example 1: Production (Azure Automation)
    # ----------------------------------------

    # Retrieve encrypted variable and convert immediately to SecureString
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force

    # WebhookData is automatically passed by Azure Automation webhook trigger

    # Verify HMAC signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    [void]$sharedSecret.Dispose()
    throw "Unauthorized: HMAC signature verification failed."
    }

    # Clear secret from memory
    [void]$sharedSecret.Dispose()

    # Proceed securely
    Write-Output "HMAC verification passed."

    # ----------------------------------------
    # ✅ Example 2: Local Development / Testing
    # ----------------------------------------

    # Retrieve secret securely from local source:
    # - From Credential Manager (example):
    # Import-Module Microsoft.PowerShell.SecretManagement
    # $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password

    # - From encrypted local file (example):
    # $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt") | ConvertTo-SecureString -AsPlainText -Force

    # - For testing only (❌ never in production!):
    $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force

    # Run verification
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    Write-Output "❌ Local Test: Signature invalid."
    } else {
    Write-Output "✅ Local Test: Signature valid!"
    }

    # ================================
    }
    4 changes: 2 additions & 2 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -10,9 +10,9 @@ function Get-HmacSignedHeaders {
    This function is intended to be used when calling Azure Automation webhooks or other APIs requiring signed requests for enhanced security.
    ----------------------------
    ------------------------------------------
    🔐 Secure Shared Secret Retrieval Options:
    ----------------------------
    ------------------------------------------
    Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings:
  4. jpawlowski revised this gist Mar 19, 2025. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@
    function Get-HmacSignedHeaders {
    function Get-HmacSignedHeaders {
    <#
    .SYNOPSIS
  5. jpawlowski revised this gist Mar 19, 2025. 1 changed file with 47 additions and 67 deletions.
    114 changes: 47 additions & 67 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -1,18 +1,59 @@
    function Get-HmacSignedHeaders {
    function Get-HmacSignedHeaders {
    <#
    .SYNOPSIS
    Generates signed headers for HMAC authentication for Azure Automation webhook requests.
    .DESCRIPTION
    Generates signed headers for HMAC authentication based on a shared secret (SecureString), URL, and request body.
    Includes timestamp, nonce, and content hash, all covered in the signature.
    SecureString is converted as late as possible and securely cleaned up after use.
    Generates signed headers for HMAC authentication based on a shared secret (provided as SecureString), webhook URL, and request body content.
    Includes timestamp, nonce, and content hash, all covered in the HMAC signature.
    SecureString is converted as late as possible and securely cleared after use.
    This function is intended to be used when calling Azure Automation webhooks or other APIs requiring signed requests for enhanced security.
    .FUNCTIONALITY
    Security, HMAC Authentication
    ----------------------------
    🔐 Secure Shared Secret Retrieval Options:
    ----------------------------
    Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings:
    1️⃣ Azure Key Vault (Recommended for cloud environments)
    Connect-AzAccount -Identity
    $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue
    2️⃣ Azure Automation Encrypted Variable (Automation Account)
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    3️⃣ Windows Credential Manager (with SecretManagement module)
    Import-Module Microsoft.PowerShell.SecretManagement
    $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    4️⃣ Encrypted local file (only in controlled environments)
    $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force
    ❌ For demonstration purposes only (NEVER hardcode secrets in production):
    $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force
    .EXAMPLE
    $headers = Get-HmacSignedHeaders -SharedSecret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) -WebhookUrl "https://example.com/webhooks" -Body '{"foo":"bar"}'
    # Example usage:
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    $webhookUrl = "https://<your-webhook-url>/webhooks"
    $body = '{"param1":"value1"}'
    $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret `
    -WebhookUrl $webhookUrl `
    -Body $body
    [void]$sharedSecret.Dispose()
    Invoke-RestMethod -Method POST `
    -Uri $webhookUrl `
    -Headers $headers `
    -Body $body `
    -ContentType 'application/json; charset=utf-8'
    .FUNCTIONALITY
    Security, HMAC Authentication
    .NOTES
    Author: Julian Pawlowski
    @@ -100,64 +141,3 @@ function Get-HmacSignedHeaders {
    return $headers
    }

    # ================================
    # 🔐 Example Usage: HMAC Header Generation
    # ================================

    # IMPORTANT:
    # NEVER hardcode secrets in plaintext inside scripts.
    # Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings:

    # ----------------------------
    # Secure Shared Secret Retrieval Options:
    # ----------------------------

    # 1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments)
    # Requires: Az module & Managed Identity or Service Principal authentication
    Connect-AzAccount -Identity
    $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue
    # --> Result: SecureString ✅

    # 2️⃣ Azure Automation Encrypted Variable (inside Automation Account only)
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    # --> Result: SecureString ✅

    # 3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module)
    Import-Module Microsoft.PowerShell.SecretManagement
    $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    # --> Result: SecureString ✅

    # 4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments)
    $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force
    # --> Result: SecureString ✅

    # ❌ For demonstration purposes only (NEVER do this in production!)
    $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force
    # --> Result: SecureString (but insecure in practice!)

    # ----------------------------
    # Webhook Configuration:
    # ----------------------------

    # Webhook URL (replace with your actual URL)
    $webhookUrl = "https://<your-webhook-url>/webhooks"

    # Sample body (can be JSON or plain text)
    $body = '{"param1":"value1"}'

    # Generate signed headers
    $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret `
    -WebhookUrl $webhookUrl `
    -Body $body

    # 🚫 Cleanup: SecureString Handling
    [void]$sharedSecret.Dispose()

    # Send POST request
    $null = Invoke-RestMethod -Method POST `
    -Uri $webhookUrl `
    -Headers $headers `
    -Body $body `
    -ContentType 'application/json; charset=utf-8'

    # ================================
  6. jpawlowski revised this gist Mar 18, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -28,8 +28,8 @@ function Test-HmacAuthorization {
    # The full webhook request data object passed by Azure Automation (includes headers and body)
    [Parameter(Mandatory)][object]$WebhookData,

    # Allowed time difference in minutes for timestamp validation (default: 3 minutes)
    [int]$AllowedTimeDriftMinutes = 3,
    # Allowed time difference in minutes for timestamp validation (default: 5 minutes)
    [int]$AllowedTimeDriftMinutes = 5,

    # Expected request path (used in canonical message construction)
    [string]$ExpectedPath = '/webhooks'
  7. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 3 additions and 13 deletions.
    7 changes: 1 addition & 6 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -113,13 +113,11 @@ function Test-HmacAuthorization {
    $canonicalMessage = "$method`n$path`n" + ($headerValues -join ';')

    $unsecureSecret = $null
    $unsecurePtr = [IntPtr]::Zero
    try {
    $hmac = New-Object ("System.Security.Cryptography.$algorithm")

    # SecureString → plaintext
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)
    $unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password

    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)
    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    @@ -133,9 +131,6 @@ function Test-HmacAuthorization {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    # Final comparison (constant-time)
    9 changes: 2 additions & 7 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -68,13 +68,11 @@ function Get-HmacSignedHeaders {
    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash;$Nonce"

    $unsecureSecret = $null
    $unsecurePtr = [IntPtr]::Zero
    try {
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")

    # Convert SecureString to unmanaged pointer
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)
    # SecureString → plaintext
    $unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password

    # Set key and compute signature
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)
    @@ -89,9 +87,6 @@ function Get-HmacSignedHeaders {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    # Prepare headers (Host excluded intentionally)
  8. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 55 additions and 21 deletions.
    42 changes: 28 additions & 14 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -4,9 +4,9 @@ function Test-HmacAuthorization {
    Verifies HMAC signature of incoming Azure Automation webhook requests.
    .DESCRIPTION
    Validates the HMAC signature of a webhook request by reconstructing the canonical message and comparing the computed HMAC.
    Uses SecureString for the shared secret and ensures secure cleanup using finally blocks.
    Supports SHA256 and SHA512 algorithms.
    Validates HMAC signature including timestamp, nonce, and body hash.
    Uses SecureString for shared secret, with proper secure cleanup. Supports SHA256 and SHA512.
    Nonce uniqueness checking is left open for future enhancement.
    .FUNCTIONALITY
    Security, HMAC Authentication
    @@ -20,10 +20,18 @@ function Test-HmacAuthorization {
    Created: 2025-03-17
    Updated: 2025-03-18
    #>

    param(
    # The shared secret key used for HMAC calculation (SecureString)
    [Parameter(Mandatory)][securestring]$SharedSecret,

    # The full webhook request data object passed by Azure Automation (includes headers and body)
    [Parameter(Mandatory)][object]$WebhookData,
    [int]$AllowedTimeDriftMinutes = 5,

    # Allowed time difference in minutes for timestamp validation (default: 3 minutes)
    [int]$AllowedTimeDriftMinutes = 3,

    # Expected request path (used in canonical message construction)
    [string]$ExpectedPath = '/webhooks'
    )

    @@ -57,6 +65,7 @@ function Test-HmacAuthorization {
    }
    }

    # Timestamp freshness check
    if ([string]::IsNullOrEmpty($headers.'x-ms-date')) {
    Write-Error 'x-ms-date header required'
    return $false
    @@ -76,6 +85,7 @@ function Test-HmacAuthorization {
    }
    }

    # Body hash check
    if ([string]::IsNullOrEmpty($headers.'x-ms-content-sha256')) {
    Write-Error "x-ms-content-sha256 header required"
    return $false
    @@ -89,6 +99,14 @@ function Test-HmacAuthorization {
    }
    }

    # Nonce check
    if ([string]::IsNullOrEmpty($headers.'x-ms-nonce')) {
    Write-Error "x-ms-nonce header required"
    return $false
    }
    # Optional future: Implement nonce storage to prevent re-use attacks

    # Canonical message construction
    $method = 'POST'
    $path = $ExpectedPath
    $headerValues = foreach ($header in $signedHeaders) { $headers.$header }
    @@ -99,32 +117,28 @@ function Test-HmacAuthorization {
    try {
    $hmac = New-Object ("System.Security.Cryptography.$algorithm")

    # Convert SecureString to unmanaged pointer
    # SecureString → plaintext
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    # Copy unmanaged pointer to managed string
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)

    # Set HMAC key
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)

    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    }
    catch {
    Write-Error "Error during HMAC computation"
    return $false

    # Cleanup HMAC key
    [Array]::Clear($hmac.Key, 0, $hmac.Key.Length)
    $hmac = $null
    }
    finally {
    # Cleanup plaintext secret
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    # Cleanup unmanaged memory (VERY IMPORTANT!)
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    # Final comparison (constant-time)
    return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
    [Text.Encoding]::UTF8.GetBytes($computedHmac),
    [Text.Encoding]::UTF8.GetBytes($receivedHmac)
    34 changes: 27 additions & 7 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,8 @@ function Get-HmacSignedHeaders {
    .DESCRIPTION
    Generates signed headers for HMAC authentication based on a shared secret (SecureString), URL, and request body.
    SecureString is converted as late as possible and cleaned up immediately after use.
    Includes timestamp, nonce, and content hash, all covered in the signature.
    SecureString is converted as late as possible and securely cleaned up after use.
    .FUNCTIONALITY
    Security, HMAC Authentication
    @@ -19,6 +20,7 @@ function Get-HmacSignedHeaders {
    Created: 2025-03-17
    Updated: 2025-03-18
    #>

    param(
    # Shared secret used for HMAC signature (SecureString)
    [Parameter(Mandatory)][securestring]$SharedSecret,
    @@ -30,55 +32,73 @@ function Get-HmacSignedHeaders {
    [Parameter(Mandatory)][string]$Body,

    # Algorithm to use for HMAC signature (default: HMACSHA256)
    [string]$Algorithm = "HMACSHA256"
    [string]$Algorithm = "HMACSHA256",

    # Optional: Provide custom nonce (for testing/debugging); defaults to a new random GUID if omitted
    [string]$Nonce
    )

    if ($Algorithm -notin @('HMACSHA256', 'HMACSHA512')) {
    throw "Unsupported algorithm: $Algorithm"
    }

    # Parse URL components
    $uri = [System.Uri]$WebhookUrl
    $method = "POST"
    $path = $uri.AbsolutePath
    $webHost = $uri.Host

    # Timestamp header (RFC1123 format)
    $date = (Get-Date).ToUniversalTime().ToString("R")

    # Compute body hash (SHA256)
    $bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body)
    $contentHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes))

    $signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256')
    # Generate nonce if not provided
    if (-not $Nonce) {
    $Nonce = [Guid]::NewGuid().ToString()
    }

    # Define signed headers and order
    $signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256', 'x-ms-nonce')
    $signedHeaders = ($signedHeadersList -join ';')

    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash"
    # Build canonical message
    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash;$Nonce"

    $unsecureSecret = $null
    $unsecurePtr = [IntPtr]::Zero
    try {
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")

    # Convert SecureString to unmanaged memory
    # Convert SecureString to unmanaged pointer
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)

    # Set key and compute signature
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)
    $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))

    # Cleanup HMAC key
    [Array]::Clear($hmac.Key, 0, $hmac.Key.Length)
    $hmac = $null
    }
    finally {
    # Cleanup managed copy
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    # Cleanup unmanaged memory
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    # Prepare headers (Host excluded intentionally)
    $headers = @{
    'x-ms-date' = $date
    'x-ms-content-sha256' = $contentHash
    'x-ms-nonce' = $Nonce
    'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature"
    }

  9. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 2 additions and 0 deletions.
    1 change: 1 addition & 0 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -18,6 +18,7 @@ function Test-HmacAuthorization {
    Author: Julian Pawlowski
    Company Name: Workoho GmbH
    Created: 2025-03-17
    Updated: 2025-03-18
    #>
    param(
    [Parameter(Mandatory)][securestring]$SharedSecret,
    1 change: 1 addition & 0 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -17,6 +17,7 @@ function Get-HmacSignedHeaders {
    Author: Julian Pawlowski
    Company Name: Workoho GmbH
    Created: 2025-03-17
    Updated: 2025-03-18
    #>
    param(
    # Shared secret used for HMAC signature (SecureString)
  10. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 22 additions and 14 deletions.
    23 changes: 12 additions & 11 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -16,24 +16,17 @@ function Test-HmacAuthorization {
    .NOTES
    Author: Julian Pawlowski
    Company Name: Workoho GmbH
    Created: 2025-03-17
    #>
    param(
    # The shared secret key used for HMAC calculation (SecureString)
    [Parameter(Mandatory)][securestring]$SharedSecret,

    # The full webhook request data object passed by Azure Automation (includes headers and body)
    [Parameter(Mandatory)][object]$WebhookData,

    # Allowed time difference in minutes for timestamp validation (default: 5 minutes)
    [int]$AllowedTimeDriftMinutes = 5,

    # Expected request path (used in canonical message construction)
    [string]$ExpectedPath = '/webhooks'
    )

    $allowedAlgorithms = @('HMACSHA256', 'HMACSHA512')

    $headers = $WebhookData.RequestHeader
    $authHeader = $headers.'x-authorization'

    @@ -101,13 +94,16 @@ function Test-HmacAuthorization {
    $canonicalMessage = "$method`n$path`n" + ($headerValues -join ';')

    $unsecureSecret = $null
    $unsecurePtr = [IntPtr]::Zero
    try {
    $hmac = New-Object ("System.Security.Cryptography.$algorithm")

    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    )
    # Convert SecureString to unmanaged pointer
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    # Copy unmanaged pointer to managed string
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)

    # Set HMAC key
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)

    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    @@ -117,10 +113,15 @@ function Test-HmacAuthorization {
    return $false
    }
    finally {
    # Cleanup plaintext secret
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    # Cleanup unmanaged memory (VERY IMPORTANT!)
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
    13 changes: 10 additions & 3 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -15,6 +15,7 @@ function Get-HmacSignedHeaders {
    .NOTES
    Author: Julian Pawlowski
    Company Name: Workoho GmbH
    Created: 2025-03-17
    #>
    param(
    @@ -51,21 +52,27 @@ function Get-HmacSignedHeaders {
    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash"

    $unsecureSecret = $null
    $unsecurePtr = [IntPtr]::Zero
    try {
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")

    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    )
    # Convert SecureString to unmanaged memory
    $unsecurePtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto($unsecurePtr)

    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)
    $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    }
    finally {
    # Cleanup managed copy
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    # Cleanup unmanaged memory
    if ($unsecurePtr -ne [IntPtr]::Zero) {
    [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($unsecurePtr)
    }
    }

    $headers = @{
  11. jpawlowski revised this gist Mar 18, 2025. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -70,7 +70,6 @@ function Get-HmacSignedHeaders {

    $headers = @{
    'x-ms-date' = $date
    'Host' = $webHost
    'x-ms-content-sha256' = $contentHash
    'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature"
    }
  12. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 3 additions and 3 deletions.
    4 changes: 2 additions & 2 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -144,12 +144,12 @@ $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-Secure

    # Verify HMAC signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    $sharedSecret.Dispose()
    [void]$sharedSecret.Dispose()
    throw "Unauthorized: HMAC signature verification failed."
    }

    # Clear secret from memory
    $sharedSecret.Dispose()
    [void]$sharedSecret.Dispose()

    # Proceed securely
    Write-Output "HMAC verification passed."
    2 changes: 1 addition & 1 deletion PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -129,7 +129,7 @@ $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret `
    -Body $body

    # 🚫 Cleanup: SecureString Handling
    $sharedSecret.Dispose()
    [void]$sharedSecret.Dispose()

    # Send POST request
    $null = Invoke-RestMethod -Method POST `
  13. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 26 additions and 11 deletions.
    4 changes: 4 additions & 0 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -144,9 +144,13 @@ $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-Secure

    # Verify HMAC signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    $sharedSecret.Dispose()
    throw "Unauthorized: HMAC signature verification failed."
    }

    # Clear secret from memory
    $sharedSecret.Dispose()

    # Proceed securely
    Write-Output "HMAC verification passed."

    33 changes: 22 additions & 11 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -78,34 +78,40 @@ function Get-HmacSignedHeaders {
    return $headers
    }

    # 🔽 Example Usage:
    # ----------------------------
    # ================================
    # 🔐 Example Usage: HMAC Header Generation
    # ================================

    # IMPORTANT:
    # NEVER hardcode secrets in plaintext inside scripts.
    # Always retrieve secrets securely from encrypted sources like:
    # Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings:

    # ----------------------------
    # Secure Shared Secret Retrieval Options:
    # ----------------------------

    # 1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments)
    # Connect-AzAccount -Identity
    # $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue
    # Requires: Az module & Managed Identity or Service Principal authentication
    Connect-AzAccount -Identity
    $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue
    # --> Result: SecureString ✅

    # 2️⃣ Azure Automation Encrypted Variable (inside Automation Account only)
    # $sharedSecret = Get-AutomationVariable -Name "SharedSecret"
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force
    # --> Result: SecureString ✅

    # 3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module)
    # Import-Module Microsoft.PowerShell.SecretManagement
    # $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    Import-Module Microsoft.PowerShell.SecretManagement
    $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password
    # --> Result: SecureString ✅

    # 4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments)
    # $secretString = Get-Content -Path "C:\Secrets\HmacSecret.txt"
    # $sharedSecret = ConvertTo-SecureString -String $secretString -AsPlainText -Force
    $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force
    # --> Result: SecureString ✅

    # ❌ For demonstration purposes only (NEVER do this in production!)
    # $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force
    $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force
    # --> Result: SecureString (but insecure in practice!)

    # ----------------------------
    # Webhook Configuration:
    @@ -122,9 +128,14 @@ $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret `
    -WebhookUrl $webhookUrl `
    -Body $body

    # 🚫 Cleanup: SecureString Handling
    $sharedSecret.Dispose()

    # Send POST request
    $null = Invoke-RestMethod -Method POST `
    -Uri $webhookUrl `
    -Headers $headers `
    -Body $body `
    -ContentType 'application/json; charset=utf-8'

    # ================================
  14. jpawlowski revised this gist Mar 18, 2025. No changes.
  15. jpawlowski revised this gist Mar 18, 2025. 1 changed file with 45 additions and 0 deletions.
    45 changes: 45 additions & 0 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -128,3 +128,48 @@ function Test-HmacAuthorization {
    [Text.Encoding]::UTF8.GetBytes($receivedHmac)
    )
    }

    # ================================
    # 🔐 HMAC Authorization Verification Usage
    # ================================

    # ----------------------------------------
    # ✅ Example 1: Production (Azure Automation)
    # ----------------------------------------

    # Retrieve encrypted variable and convert immediately to SecureString
    $sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force

    # WebhookData is automatically passed by Azure Automation webhook trigger

    # Verify HMAC signature
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    throw "Unauthorized: HMAC signature verification failed."
    }

    # Proceed securely
    Write-Output "HMAC verification passed."

    # ----------------------------------------
    # ✅ Example 2: Local Development / Testing
    # ----------------------------------------

    # Retrieve secret securely from local source:
    # - From Credential Manager (example):
    # Import-Module Microsoft.PowerShell.SecretManagement
    # $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password

    # - From encrypted local file (example):
    # $sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt") | ConvertTo-SecureString -AsPlainText -Force

    # - For testing only (❌ never in production!):
    $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force

    # Run verification
    if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) {
    Write-Output "❌ Local Test: Signature invalid."
    } else {
    Write-Output "✅ Local Test: Signature valid!"
    }

    # ================================
  16. jpawlowski revised this gist Mar 18, 2025. 2 changed files with 98 additions and 46 deletions.
    48 changes: 23 additions & 25 deletions PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -4,25 +4,23 @@ function Test-HmacAuthorization {
    Verifies HMAC signature of incoming Azure Automation webhook requests.
    .DESCRIPTION
    This function validates the HMAC signature of a webhook request by reconstructing the canonical message and comparing the computed HMAC.
    It checks timestamp freshness, signed header presence, and request body integrity. It supports both SHA256 and SHA512 algorithms.
    Use together with Get-HmacSignedHeaders to sign and verify HMAC-authenticated webhook calls.
    Validates the HMAC signature of a webhook request by reconstructing the canonical message and comparing the computed HMAC.
    Uses SecureString for the shared secret and ensures secure cleanup using finally blocks.
    Supports SHA256 and SHA512 algorithms.
    .FUNCTIONALITY
    Security, HMAC Authentication
    .EXAMPLE
    Test-HmacAuthorization -SharedSecret "MySecretKey" -WebhookData $WebhookData
    Test-HmacAuthorization -SharedSecret (Get-AutomationVariable -Name 'SharedSecret') -WebhookData $WebhookData
    .NOTES
    Author: Julian Pawlowski
    Created: 2025-03-17
    Updated: 2025-03-17
    #>
    param(
    # The shared secret key used for HMAC calculation
    [Parameter(Mandatory)][string]$SharedSecret,
    # The shared secret key used for HMAC calculation (SecureString)
    [Parameter(Mandatory)][securestring]$SharedSecret,

    # The full webhook request data object passed by Azure Automation (includes headers and body)
    [Parameter(Mandatory)][object]$WebhookData,
    @@ -34,10 +32,8 @@ function Test-HmacAuthorization {
    [string]$ExpectedPath = '/webhooks'
    )

    # Allowed HMAC algorithms
    $allowedAlgorithms = @('HMACSHA256', 'HMACSHA512')

    # Extract request headers
    $headers = $WebhookData.RequestHeader
    $authHeader = $headers.'x-authorization'

    @@ -46,13 +42,11 @@ function Test-HmacAuthorization {
    return $false
    }

    # Parse x-authorization header (expecting format: HMAC-ALGO SignedHeaders=...&Signature=...)
    if ($authHeader -notmatch '^HMAC-(?<Algorithm>[A-Z0-9]+)\s+SignedHeaders=(?<SignedHeaders>[^&]+)&Signature=(?<Signature>.+)$') {
    Write-Error 'Invalid x-authorization header format'
    return $false
    }

    # Extract algorithm and signature details
    $algorithm = "HMAC$($matches['Algorithm'])"
    if ($allowedAlgorithms -notcontains $algorithm) {
    Write-Error "Algorithm $algorithm not allowed"
    @@ -62,15 +56,13 @@ function Test-HmacAuthorization {
    $signedHeaders = $matches['SignedHeaders'].Split(';')
    $receivedHmac = $matches['Signature']

    # Validate that all signed headers are present in the request
    foreach ($header in $signedHeaders) {
    if ([string]::IsNullOrEmpty($headers.$header)) {
    Write-Error "Missing signed header: $header"
    return $false
    }
    }

    # Validate x-ms-date header freshness (protection against replay attacks)
    if ([string]::IsNullOrEmpty($headers.'x-ms-date')) {
    Write-Error 'x-ms-date header required'
    return $false
    @@ -90,7 +82,6 @@ function Test-HmacAuthorization {
    }
    }

    # Validate x-ms-content-sha256 header (ensures body integrity)
    if ([string]::IsNullOrEmpty($headers.'x-ms-content-sha256')) {
    Write-Error "x-ms-content-sha256 header required"
    return $false
    @@ -104,27 +95,34 @@ function Test-HmacAuthorization {
    }
    }

    # Construct canonical message (method + path + signed header values)
    $method = 'POST'
    $path = $ExpectedPath
    $headerValues = foreach ($header in $signedHeaders) {
    $headers.$header
    }
    $headerValues = foreach ($header in $signedHeaders) { $headers.$header }
    $canonicalMessage = "$method`n$path`n" + ($headerValues -join ';')

    # Compute HMAC signature
    $unsecureSecret = $null
    try {
    $hmac = New-Object ("System.Security.Cryptography.$algorithm")
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($SharedSecret)

    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    )

    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)

    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    }
    catch {
    Write-Error "Unsupported algorithm $algorithm"
    Write-Error "Error during HMAC computation"
    return $false
    }
    finally {
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    }

    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))

    # Secure, constant-time comparison to prevent timing attacks
    return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
    [Text.Encoding]::UTF8.GetBytes($computedHmac),
    [Text.Encoding]::UTF8.GetBytes($receivedHmac)
    96 changes: 75 additions & 21 deletions PSFunction2_Get-HmacSignedHeaders.ps1
    Original file line number Diff line number Diff line change
    @@ -4,26 +4,22 @@ function Get-HmacSignedHeaders {
    Generates signed headers for HMAC authentication for Azure Automation webhook requests.
    .DESCRIPTION
    This function generates signed headers for HMAC authentication based on the provided shared secret, webhook URL, and request body content.
    It uses the specified HMAC algorithm (SHA256 or SHA512) and constructs the canonical message according to Azure Communication Services style.
    See the complementary function Test-HmacAuthorization for signature verification within Azure Automation runbooks.
    Generates signed headers for HMAC authentication based on a shared secret (SecureString), URL, and request body.
    SecureString is converted as late as possible and cleaned up immediately after use.
    .FUNCTIONALITY
    Security, HMAC Authentication
    .EXAMPLE
    $headers = Get-HmacSignedHeaders -SharedSecret "MySecretKey" -WebhookUrl "https://example.com/webhooks?token=12345" -Body '{"foo":"bar"}'
    Generates signed headers for HMAC-protected request.
    $headers = Get-HmacSignedHeaders -SharedSecret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) -WebhookUrl "https://example.com/webhooks" -Body '{"foo":"bar"}'
    .NOTES
    Author: Julian Pawlowski
    Created: 2025-03-17
    Updated: 2025-03-17
    #>
    param(
    # Shared secret used for HMAC signature (plain text, matches server config)
    [Parameter(Mandatory)][string]$SharedSecret,
    # Shared secret used for HMAC signature (SecureString)
    [Parameter(Mandatory)][securestring]$SharedSecret,

    # Full webhook URL to which the request will be sent
    [Parameter(Mandatory)][string]$WebhookUrl,
    @@ -35,42 +31,100 @@ function Get-HmacSignedHeaders {
    [string]$Algorithm = "HMACSHA256"
    )

    # Validate allowed algorithm
    if ($Algorithm -notin @('HMACSHA256', 'HMACSHA512')) {
    throw "Unsupported algorithm: $Algorithm"
    }

    # Parse URL components
    $uri = [System.Uri]$WebhookUrl
    $method = "POST"
    $path = $uri.AbsolutePath # URL path without query string since the webhook token is removed by the Azure Automation service
    $path = $uri.AbsolutePath
    $webHost = $uri.Host

    # x-ms-date header: RFC1123 timestamp (UTC)
    $date = (Get-Date).ToUniversalTime().ToString("R")

    # Compute body hash (x-ms-content-sha256 header, Base64-encoded SHA256)
    $bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body)
    $contentHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes))

    # Define signed headers and their order (must match server config)
    $signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256')
    $signedHeaders = ($signedHeadersList -join ';')

    # Canonical message: method, path, signed header values (semicolon-separated)
    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash"

    # Compute HMAC signature
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($SharedSecret)
    $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    $unsecureSecret = $null
    try {
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")

    $unsecureSecret = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SharedSecret)
    )

    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret)
    $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))
    }
    finally {
    if ($unsecureSecret) {
    [Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length)
    $unsecureSecret = $null
    }
    }

    # Prepare signed headers
    $headers = @{
    'x-ms-date' = $date
    'Host' = $webHost
    'x-ms-content-sha256' = $contentHash
    'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature"
    }

    return $headers
    }

    # 🔽 Example Usage:
    # ----------------------------

    # IMPORTANT:
    # NEVER hardcode secrets in plaintext inside scripts.
    # Always retrieve secrets securely from encrypted sources like:

    # ----------------------------
    # Secure Shared Secret Retrieval Options:
    # ----------------------------

    # 1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments)
    # Connect-AzAccount -Identity
    # $sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue

    # 2️⃣ Azure Automation Encrypted Variable (inside Automation Account only)
    # $sharedSecret = Get-AutomationVariable -Name "SharedSecret"

    # 3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module)
    # Import-Module Microsoft.PowerShell.SecretManagement
    # $sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password

    # 4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments)
    # $secretString = Get-Content -Path "C:\Secrets\HmacSecret.txt"
    # $sharedSecret = ConvertTo-SecureString -String $secretString -AsPlainText -Force

    # ❌ For demonstration purposes only (NEVER do this in production!)
    # $sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force

    # ----------------------------
    # Webhook Configuration:
    # ----------------------------

    # Webhook URL (replace with your actual URL)
    $webhookUrl = "https://<your-webhook-url>/webhooks"

    # Sample body (can be JSON or plain text)
    $body = '{"param1":"value1"}'

    # Generate signed headers
    $headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret `
    -WebhookUrl $webhookUrl `
    -Body $body

    # Send POST request
    $null = Invoke-RestMethod -Method POST `
    -Uri $webhookUrl `
    -Headers $headers `
    -Body $body `
    -ContentType 'application/json; charset=utf-8'
  17. jpawlowski revised this gist Mar 17, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion PSFunction1_Test-HmacAuthorization.ps1
    Original file line number Diff line number Diff line change
    @@ -21,7 +21,7 @@ function Test-HmacAuthorization {
    Updated: 2025-03-17
    #>
    param(
    # The shared secret key used for HMAC calculation (Base64-encoded string)
    # The shared secret key used for HMAC calculation
    [Parameter(Mandatory)][string]$SharedSecret,

    # The full webhook request data object passed by Azure Automation (includes headers and body)
  18. jpawlowski revised this gist Mar 17, 2025. 2 changed files with 0 additions and 0 deletions.
  19. jpawlowski revised this gist Mar 17, 2025. 2 changed files with 0 additions and 0 deletions.
  20. jpawlowski revised this gist Mar 17, 2025. 1 changed file with 76 additions and 0 deletions.
    76 changes: 76 additions & 0 deletions Get-HmacSignedHeaders.function.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,76 @@
    function Get-HmacSignedHeaders {
    <#
    .SYNOPSIS
    Generates signed headers for HMAC authentication for Azure Automation webhook requests.
    .DESCRIPTION
    This function generates signed headers for HMAC authentication based on the provided shared secret, webhook URL, and request body content.
    It uses the specified HMAC algorithm (SHA256 or SHA512) and constructs the canonical message according to Azure Communication Services style.
    See the complementary function Test-HmacAuthorization for signature verification within Azure Automation runbooks.
    .FUNCTIONALITY
    Security, HMAC Authentication
    .EXAMPLE
    $headers = Get-HmacSignedHeaders -SharedSecret "MySecretKey" -WebhookUrl "https://example.com/webhooks?token=12345" -Body '{"foo":"bar"}'
    Generates signed headers for HMAC-protected request.
    .NOTES
    Author: Julian Pawlowski
    Created: 2025-03-17
    Updated: 2025-03-17
    #>
    param(
    # Shared secret used for HMAC signature (plain text, matches server config)
    [Parameter(Mandatory)][string]$SharedSecret,

    # Full webhook URL to which the request will be sent
    [Parameter(Mandatory)][string]$WebhookUrl,

    # Request body content as a string (usually JSON or similar)
    [Parameter(Mandatory)][string]$Body,

    # Algorithm to use for HMAC signature (default: HMACSHA256)
    [string]$Algorithm = "HMACSHA256"
    )

    # Validate allowed algorithm
    if ($Algorithm -notin @('HMACSHA256', 'HMACSHA512')) {
    throw "Unsupported algorithm: $Algorithm"
    }

    # Parse URL components
    $uri = [System.Uri]$WebhookUrl
    $method = "POST"
    $path = $uri.AbsolutePath # URL path without query string since the webhook token is removed by the Azure Automation service
    $webHost = $uri.Host

    # x-ms-date header: RFC1123 timestamp (UTC)
    $date = (Get-Date).ToUniversalTime().ToString("R")

    # Compute body hash (x-ms-content-sha256 header, Base64-encoded SHA256)
    $bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body)
    $contentHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes))

    # Define signed headers and their order (must match server config)
    $signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256')
    $signedHeaders = ($signedHeadersList -join ';')

    # Canonical message: method, path, signed header values (semicolon-separated)
    $canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash"

    # Compute HMAC signature
    $hmac = New-Object ("System.Security.Cryptography.$Algorithm")
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($SharedSecret)
    $signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))

    # Prepare signed headers
    $headers = @{
    'x-ms-date' = $date
    'x-ms-content-sha256' = $contentHash
    'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature"
    }

    return $headers
    }
  21. jpawlowski created this gist Mar 17, 2025.
    132 changes: 132 additions & 0 deletions Test-HmacAuthorization.function.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,132 @@
    function Test-HmacAuthorization {
    <#
    .SYNOPSIS
    Verifies HMAC signature of incoming Azure Automation webhook requests.
    .DESCRIPTION
    This function validates the HMAC signature of a webhook request by reconstructing the canonical message and comparing the computed HMAC.
    It checks timestamp freshness, signed header presence, and request body integrity. It supports both SHA256 and SHA512 algorithms.
    Use together with Get-HmacSignedHeaders to sign and verify HMAC-authenticated webhook calls.
    .FUNCTIONALITY
    Security, HMAC Authentication
    .EXAMPLE
    Test-HmacAuthorization -SharedSecret "MySecretKey" -WebhookData $WebhookData
    .NOTES
    Author: Julian Pawlowski
    Created: 2025-03-17
    Updated: 2025-03-17
    #>
    param(
    # The shared secret key used for HMAC calculation (Base64-encoded string)
    [Parameter(Mandatory)][string]$SharedSecret,

    # The full webhook request data object passed by Azure Automation (includes headers and body)
    [Parameter(Mandatory)][object]$WebhookData,

    # Allowed time difference in minutes for timestamp validation (default: 5 minutes)
    [int]$AllowedTimeDriftMinutes = 5,

    # Expected request path (used in canonical message construction)
    [string]$ExpectedPath = '/webhooks'
    )

    # Allowed HMAC algorithms
    $allowedAlgorithms = @('HMACSHA256', 'HMACSHA512')

    # Extract request headers
    $headers = $WebhookData.RequestHeader
    $authHeader = $headers.'x-authorization'

    if (-not $authHeader) {
    Write-Error 'Missing x-authorization header'
    return $false
    }

    # Parse x-authorization header (expecting format: HMAC-ALGO SignedHeaders=...&Signature=...)
    if ($authHeader -notmatch '^HMAC-(?<Algorithm>[A-Z0-9]+)\s+SignedHeaders=(?<SignedHeaders>[^&]+)&Signature=(?<Signature>.+)$') {
    Write-Error 'Invalid x-authorization header format'
    return $false
    }

    # Extract algorithm and signature details
    $algorithm = "HMAC$($matches['Algorithm'])"
    if ($allowedAlgorithms -notcontains $algorithm) {
    Write-Error "Algorithm $algorithm not allowed"
    return $false
    }

    $signedHeaders = $matches['SignedHeaders'].Split(';')
    $receivedHmac = $matches['Signature']

    # Validate that all signed headers are present in the request
    foreach ($header in $signedHeaders) {
    if ([string]::IsNullOrEmpty($headers.$header)) {
    Write-Error "Missing signed header: $header"
    return $false
    }
    }

    # Validate x-ms-date header freshness (protection against replay attacks)
    if ([string]::IsNullOrEmpty($headers.'x-ms-date')) {
    Write-Error 'x-ms-date header required'
    return $false
    }
    else {
    try {
    $requestTime = [datetime]::Parse($headers.'x-ms-date').ToUniversalTime()
    $currentTime = (Get-Date).ToUniversalTime()
    if ([math]::Abs(($currentTime - $requestTime).TotalMinutes) -gt $AllowedTimeDriftMinutes) {
    Write-Error 'Request timestamp expired'
    return $false
    }
    }
    catch {
    Write-Error 'Invalid timestamp format'
    return $false
    }
    }

    # Validate x-ms-content-sha256 header (ensures body integrity)
    if ([string]::IsNullOrEmpty($headers.'x-ms-content-sha256')) {
    Write-Error "x-ms-content-sha256 header required"
    return $false
    }
    else {
    $bodyBytes = [Text.Encoding]::UTF8.GetBytes($WebhookData.RequestBody)
    $computedBodyHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes))
    if ($headers.'x-ms-content-sha256' -ne $computedBodyHash) {
    Write-Error "Content hash mismatch"
    return $false
    }
    }

    # Construct canonical message (method + path + signed header values)
    $method = 'POST'
    $path = $ExpectedPath
    $headerValues = foreach ($header in $signedHeaders) {
    $headers.$header
    }
    $canonicalMessage = "$method`n$path`n" + ($headerValues -join ';')

    # Compute HMAC signature
    try {
    $hmac = New-Object ("System.Security.Cryptography.$algorithm")
    $hmac.Key = [Text.Encoding]::UTF8.GetBytes($SharedSecret)
    }
    catch {
    Write-Error "Unsupported algorithm $algorithm"
    return $false
    }

    $computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage)))

    # Secure, constant-time comparison to prevent timing attacks
    return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals(
    [Text.Encoding]::UTF8.GetBytes($computedHmac),
    [Text.Encoding]::UTF8.GetBytes($receivedHmac)
    )
    }