Challenge translating C# code to Powershell involving Cryptography

This topic contains 4 replies, has 3 voices, and was last updated by Profile photo of Dave Wyatt Dave Wyatt 1 year, 9 months ago.

  • Author
    Posts
  • #30576
    Profile photo of Mike Jenne
    Mike Jenne
    Participant

    I'm seeing some weird behavior when trying to take some relatively simple C# code and implementing it in Windows PowerShell.

    My preference is to NOT use the embedded C# code in my script, and I have sample script below that demonstrates the issue I'm seeing.

    The goal is to create SAS Tokens to be used with Azure REST API calls. I need to be able to do this without any external dependencies, hence the need to create these SAS tokens by hand.

    The sample script below shows two methods to generate these. The C# code works, meaning the token generated is valid and accepted by Azure. The token generated by the PowerShell function is not valid, meaning the signature portion of the token is bad.

    Running the sample script will spit out two tokens so the difference is obvious. I can't seem to get the PowerShell function to create the signature correctly. When running under a debugger, and the C# code running under a debugger in Visual Studio, I can see the HMACSHA256 object being created correctly in both environments, the Key fields are correct in both, but the resultant HASH coming out are NOT the same. The only thing I can attribute this to is PowerShell is doing something that I'm not expecting.

    The process is to
    1. build the string to be signed
    2. create a HMACSHA256 signature
    3. Build the token string including the signature.

    I can confirm that the strings being signed are exactly the same, meaning the byte array representation of the two are identical.

    Here's the script I'm using to test this:

    Set-StrictMode -Version 2
    
    $source = @"
    using System;
    using System.Text;
    using System.Security.Cryptography;
    using System.Globalization;
    using System.Net;
    
    public class SASTokenGenerator
        {
    
            public static string createToken(string resourceUri, string keyName, string key)
            {
                return createToken(resourceUri, keyName, key, 0);
            }
            public static string createToken(string resourceUri, string keyName, string key, int expiry)
            {
    
                if (0 == expiry)
                {
                    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
                    expiry = (int)sinceEpoch.TotalSeconds + 3600; //EXPIRES in 1h
                }
                string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + Convert.ToString(expiry);
                HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
    
                var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
                var sasToken = String.Format(CultureInfo.InvariantCulture,
                "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                    WebUtility.UrlEncode(resourceUri), WebUtility.UrlEncode(signature), expiry, keyName);
    
                return sasToken;
            }
    
        }
       
    "@
    
    
    Function New-MJAzureSASToken {
    
    
    param (
        $ResourceURI,
        $KeyName,
        $Key,
        $expiry = 0
        )
    
    
    if ($expiry -eq 0) {
    
    
        [TimeSpan]$ts = (Get-Date).ToUniversalTime() - (get-date -year 1970 -Month 1 -Date 1)
    
        [int]$expiry = ([int]($ts.TotalSeconds) + 3600).ToString()
    }
    
    [string]$stringToSign = [System.Net.WebUtility]::UrlEncode($ResourceURI) + "\n" + $expiry; 
    
    [Byte[]]$KeyBytes = [System.Text.Encoding]::UTF8.GetBytes($Key)
    
    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    
    $signature = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($stringToSign))
    
    $signature = [System.Convert]::ToBase64String($signature)
    
    
    [string]$sasToken = [System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture,"SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                        [System.Net.WebUtility]::UrlEncode($ResourceURI),
                        [System.Net.WebUtility]::UrlEncode($signature),
                        $expiry,
                        $KeyName)
    
    return $sasToken
    
    
    }
    
    Function New-MJAzureEventHubMessage {
    
    param (
    
        $payload,
        $namespace,
        $eventhub,
        $key,
        $keyname,
        $devicename
    )
    
        $b = $payload | ConvertTo-Json
    
        #create http request
    
        $Method = "POST"
    
        $Headers = @{}
    
        $Headers.Add("Content-Length", $b.length )
    
        $URI = "https://$namespace" + ".servicebus.windows.net/$eventhub/publishers/$devicename/messages"
    
        $SAS = New-MJAzureSASToken -ResourceURI ($URI) -KeyName "Send" -Key "LfrDxs7GdyhKw2E7dwIJZsLAOyqmLZ7PDMuDOT48qQ8="
    
        Invoke-RestMethod -Method $Method -Uri $URI -Body $b -Headers $Headers -Verbose -Debug
    
    }
    
    Add-Type -TypeDefinition $source -ErrorAction SilentlyContinue
    
    $testuri = "https://mjenne1010.servicebus.windows.net/hub1010/publishers/edison01.local/messages"
    $testkeyname = "Send"
    $testkey = "LfrDxs7GdyhKw2E7dwIJZsLAOyqmLZ7PDMuDOT48qQ8="
    
    [SASTokenGenerator]::createToken($testuri, $testkeyname ,$testkey,1444349270);
    
    New-MJAzureSASToken -ResourceURI $testuri -KeyName $testkeyname -Key $testkey -expiry 1444349270
    

    Note that the 1444349270 value in the above lines is used to make sure both token generators run with the exact same expiry value. This value represents the expiration time as expressed in the number of seconds since Jan 1, 1970.

    Any comments or feedback on this would be appreciated.

  • #30578
    Profile photo of Sebastian Neumann
    Sebastian Neumann
    Participant

    Please wrap your code in pre tags (as explained at the bottom of the topic).

  • #30581
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    I spotted two main problems with your PowerShell conversion:

    1) The "\n" token in C# is a newline character. In PowerShell, you would use "`n" instead (so the strings to sign were technically different).

    2) You weren't passing anything to the constructor of your HMACSHA256 object; you need to pass in your byte array.

    Here's modified code which produces the same signatures as your C# version:

    Function New-MJAzureSASToken {
        param (
            [string]$ResourceURI,
            [string]$KeyName,
            [string]$Key,
            [int] $expiry = 0
        )
    
    
        if ($expiry -eq 0) {
            [TimeSpan]$ts = (Get-Date).ToUniversalTime() - (get-date -year 1970 -Month 1 -Date 1)
            $expiry = $ts.TotalSeconds + 3600
        }
    
        [string]$stringToSign = [System.Net.WebUtility]::UrlEncode($ResourceURI) + "`n" + $expiry; 
    
        [Byte[]]$KeyBytes = [System.Text.Encoding]::UTF8.GetBytes($Key)
    
        $hmac = New-Object System.Security.Cryptography.HMACSHA256 (,$KeyBytes)
    
        $signature = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($stringToSign))
    
        $signature = [System.Convert]::ToBase64String($signature)
    
    
        [string]$sasToken = [System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture,"SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                            [System.Net.WebUtility]::UrlEncode($ResourceURI),
                            [System.Net.WebUtility]::UrlEncode($signature),
                            $expiry,
                            $KeyName)
    
        return $sasToken
    }
    
  • #30585
    Profile photo of Mike Jenne
    Mike Jenne
    Participant

    Thanks Dave.

    One more question, in your line where you call the constructor for HMACSHA256, what does the comma before $KeyBytes do? More specifically, what does it tell Windows PowerShell to do?

  • #30586
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    When I call this:

    New-Object SomeTypeName($constructorArguments)

    What PowerShell is really doing is this:

    New-Object -TypeName SomeTypeName -ArgumentList $constructorArguments

    The ArgumentList parameter takes an array of objects, and the constructor for HmacSHA256 takes an array of bytes. The tricky part is, if you just pass your array of bytes to -ArgumentList, then PowerShell thinks you're trying to call a constructor that takes a whole bunch of parameters (one for each byte in the array), instead of a constructor that takes a single argument which happens to be an array.

    The unary comma operator solves that problem. ,$arrayOfBytes creates a single-element array, and the first element of that array is the value of $arrayOfBytes (with however many elements it contains).

You must be logged in to reply to this topic.