Home » Php » How to solve SHA-512 hash differences from the implementations of PHP and C#?

How to solve SHA-512 hash differences from the implementations of PHP and C#?

Posted by: admin February 25, 2020 Leave a comment

Questions:

To use an API for one of my projects I need to verify a SHA-512 hash (sha_sign) which is supplied in the request. The API’s manufacturer provides a PHP sample script, and I need to port it to C# (ASP.NET Core 2.1).

I have the following PHP source code:

function compute_signature( $secret, $array)
{
    unset($array[ 'sha_sign' ]);

    $keys = array_keys($array);
    sort($keys);

    $sha_string = "";

    foreach ($keys as $key)
    {
        $value = html_entity_decode( $array[ $key ] );

        $value = $array[ $key ];

        $is_empty = !isset($value) || $value === "" || $value === false;

        if ($is_empty)
        {
            continue;
        }

        $sha_string .= "$key=$value$secret";
    }

    $sha_sign = strtoupper(hash("sha512", $sha_string));

    return $sha_sign;
}

You probably noticed, that $value is decoded once, but then overwritten with the raw value.

I need to port this to C#; this is what I have produced:

// Validate the hash signature of the request.
{
    string secret = "1234";

    // Parse HTTP query string manually (need the raw string for hash computation).
    Regex httpParamRegex = new Regex(@"^([^=]+)=(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    SortedList<string, string> array = new SortedList<string, string>();
    foreach (string pairStr in Request.QueryString.Value.Substring(1).Split('&'))
    {
        Match match = httpParamRegex.Match(pairStr);
        if (match.Success)
        {
            array.Add(
                match.Groups[1].Value.ToString(CultureInfo.InvariantCulture),
                match.Groups[2].Value.ToString(CultureInfo.InvariantCulture)
            );
        }
    }

    // Remove the submitted hash.
    array.Remove("sha_sign");

    // Extract the keys, and sort them.
    List<string> keys = new List<string>(array.Keys.ToArray<string>());
    keys.Sort();

    string sha_string = "";

    foreach (string key in keys)
    {
        string value = array[key];

        bool is_empty = string.IsNullOrEmpty(value);

        if (is_empty)
        {
            continue;
        }

        sha_string += $"{key}={value}{secret}";
    }

    using (SHA512 sha = SHA512.Create())
    {
        sha_string = BitConverter.ToString(
            sha.ComputeHash(
                Encoding.UTF8.GetBytes(
                    sha_string
                )
            )
        ).Replace("-", "", StringComparison.InvariantCultureIgnoreCase);
    }

    if (sha_string.Equals(Request.Query["sha_sign"][0], StringComparison.InvariantCultureIgnoreCase))
    {
        return new ContentResult()
        {
            StatusCode = 200,
            ContentType = @"text/plain",
            Content = @"OK"
        };
    }
    else
    {
        return new ContentResult()
        {
            StatusCode = 401, // 401 (Unauthorized)
            ContentType = @"text/plain",
            Content = @"Error: sha_sign invalid"
        };
    }
}

The issue is that the C# hash is always different than the PHP one. The PHP script comes from the manufacturer as an API usage example. What did I do wrong?

Here are some test data:

secret is: 1234

HTTP GET Query String

order_id=9R27Q7B8&quantity=1&email=e%40mail.invalid&language=de%2Cen&buyer_id=10226794&variant_id=V2&variant_name=Variante+2&address_first_name=FirstName&address_last_name=LastName&merchant_id=354530&country=DE&affiliate_id=&orderform_id=98318&campaignkey=&currency=EUR&amount=30.00&vat_amount=4.79&vat_rate=0.00&monthly_amount=0.00&monthly_vat_amount=0.00&number_of_installments=0&billing_type=single_payment&product_id=308152&product_language=de%2Cen&product_name=Demonstrationsprodukt+%28Dauerhafte+Lizenz%29&product_delivery_type=digital&address_id=12019159&address_street=StreetName+StreetNo&address_street2=2ndAddressLine&address_city=CityName&address_state=&address_zipcode=12345&address_country=DE&address_phone_no=%2B49123456789&address_mobile_no=&address_company=Company&address_salutation=M&address_title=Title&address_street_name=StreetName&address_street_number=StreetNo&sha_sign=DF2B0F587FA05D495CC61B7F74ADC5C709110A83D89655675F4DBBB9D9E85063212EAA7EC96661AB2449CBA30F44B339B0437B98781EA724F90A429BCE8DD0EE

Debug outputs:

Keys before remove: address_city, address_company, address_country, address_first_name, address_id, address_last_name, address_mobile_no, address_phone_no, address_salutation, address_state, address_street, address_street_name, address_street_number, address_street2, address_title, address_zipcode, affiliate_id, amount, billing_type, buyer_id, campaignkey, country, currency, email, language, merchant_id, monthly_amount, monthly_vat_amount, number_of_installments, order_id, orderform_id, product_delivery_type, product_id, product_language, product_name, quantity, sha_sign, variant_id, variant_name, vat_amount, vat_rate
Keys after remove: address_city, address_company, address_country, address_first_name, address_id, address_last_name, address_mobile_no, address_phone_no, address_salutation, address_state, address_street, address_street_name, address_street_number, address_street2, address_title, address_zipcode, affiliate_id, amount, billing_type, buyer_id, campaignkey, country, currency, email, language, merchant_id, monthly_amount, monthly_vat_amount, number_of_installments, order_id, orderform_id, product_delivery_type, product_id, product_language, product_name, quantity, variant_id, variant_name, vat_amount, vat_rate
Keys after sort: address_city, address_company, address_country, address_first_name, address_id, address_last_name, address_mobile_no, address_phone_no, address_salutation, address_state, address_street, address_street_name, address_street_number, address_street2, address_title, address_zipcode, affiliate_id, amount, billing_type, buyer_id, campaignkey, country, currency, email, language, merchant_id, monthly_amount, monthly_vat_amount, number_of_installments, order_id, orderform_id, product_delivery_type, product_id, product_language, product_name, quantity, variant_id, variant_name, vat_amount, vat_rate
Key address_city(True): CityName
Key address_company(True): Company
Key address_country(True): DE
Key address_first_name(True): FirstName
Key address_id(True): 12019159
Key address_last_name(True): LastName
Key address_mobile_no(False): 
Key address_phone_no(True): %2B49123456789
Key address_salutation(True): M
Key address_state(False): 
Key address_street(True): StreetName+StreetNo
Key address_street_name(True): StreetName
Key address_street_number(True): StreetNo
Key address_street2(True): 2ndAddressLine
Key address_title(True): Title
Key address_zipcode(True): 12345
Key affiliate_id(False): 
Key amount(True): 30.00
Key billing_type(True): single_payment
Key buyer_id(True): 10226794
Key campaignkey(False): 
Key country(True): DE
Key currency(True): EUR
Key email(True): e%40mail.invalid
Key language(True): de%2Cen
Key merchant_id(True): 354530
Key monthly_amount(True): 0.00
Key monthly_vat_amount(True): 0.00
Key number_of_installments(True): 0
Key order_id(True): 9R27Q7B8
Key orderform_id(True): 98318
Key product_delivery_type(True): digital
Key product_id(True): 308152
Key product_language(True): de%2Cen
Key product_name(True): Demonstrationsprodukt+%28Dauerhafte+Lizenz%29
Key quantity(True): 1
Key variant_id(True): V2
Key variant_name(True): Variante+2
Key vat_amount(True): 4.79
Key vat_rate(True): 0.00
Compare sha_sign(DF2B0F587FA05D495CC61B7F74ADC5C709110A83D89655675F4DBBB9D9E85063212EAA7EC96661AB2449CBA30F44B339B0437B98781EA724F90A429BCE8DD0EE) to computed hash(5532253C8F64EC5F97004778109375F1B92C6994ECE5C7D2FF003007A35DC062D5149131FC4B1BB403B98EC4CB1795A63835131DBC8CF5FC6A5A15570071E446).
How to&Answers:

Using only your SHA512 hashing code from both languages, I am able to produce the same hash for your example input after trimming the sha_sign value off of the end.

PHP:

<?php
$sha_sign = strtoupper(hash("sha512", "order_id=9R27Q7B8&quantity=1&email=e%40mail.invalid&language=de%2Cen&buyer_id=10226794&variant_id=V2&variant_name=Variante+2&address_first_name=FirstName&address_last_name=LastName&merchant_id=354530&country=DE&affiliate_id=&orderform_id=98318&campaignkey=&currency=EUR&amount=30.00&vat_amount=4.79&vat_rate=0.00&monthly_amount=0.00&monthly_vat_amount=0.00&number_of_installments=0&billing_type=single_payment&product_id=308152&product_language=de%2Cen&product_name=Demonstrationsprodukt+%28Dauerhafte+Lizenz%29&product_delivery_type=digital&address_id=12019159&address_street=StreetName+StreetNo&address_street2=2ndAddressLine&address_city=CityName&address_state=&address_zipcode=12345&address_country=DE&address_phone_no=%2B49123456789&address_mobile_no=&address_company=Company&address_salutation=M&address_title=Title&address_street_name=StreetName&address_street_number=StreetNo"));
echo $sha_sign;
?>

PHP Output:
3FD8F9AB34A6AA9F892B6D013A1D120D466FFA6C04281FF86D9DB05168AAA292C027E44503CFB9234A0B316C2D13416650F1D41A5864124516D9903EEAAA8292

C#:

using System;
using System.Text;
using System.Security.Cryptography;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            string sha_string = string.Empty;
            using (SHA512 sha = SHA512.Create())
            {
                sha_string = BitConverter.ToString(
                    sha.ComputeHash(
                        Encoding.UTF8.GetBytes(
                            "order_id=9R27Q7B8&quantity=1&email=e%40mail.invalid&language=de%2Cen&buyer_id=10226794&variant_id=V2&variant_name=Variante+2&address_first_name=FirstName&address_last_name=LastName&merchant_id=354530&country=DE&affiliate_id=&orderform_id=98318&campaignkey=&currency=EUR&amount=30.00&vat_amount=4.79&vat_rate=0.00&monthly_amount=0.00&monthly_vat_amount=0.00&number_of_installments=0&billing_type=single_payment&product_id=308152&product_language=de%2Cen&product_name=Demonstrationsprodukt+%28Dauerhafte+Lizenz%29&product_delivery_type=digital&address_id=12019159&address_street=StreetName+StreetNo&address_street2=2ndAddressLine&address_city=CityName&address_state=&address_zipcode=12345&address_country=DE&address_phone_no=%2B49123456789&address_mobile_no=&address_company=Company&address_salutation=M&address_title=Title&address_street_name=StreetName&address_street_number=StreetNo"
                        )
                    )
                ).Replace("-", "", StringComparison.InvariantCultureIgnoreCase);
            }
            Console.WriteLine(sha_string);
        }
    }
}

C# Output:
3FD8F9AB34A6AA9F892B6D013A1D120D466FFA6C04281FF86D9DB05168AAA292C027E44503CFB9234A0B316C2D13416650F1D41A5864124516D9903EEAAA8292

This leads me to believe that there is a difference in the input provided to the SHA512 hash call, so I propose you print out the input to that call before making it to compare the two solutions and ensure the two inputs are identical.