2

How can I validate a X-HW-SIGNATURE in PHP?

The documentation for request parameters reads:

Message header signature, which is mandatory, indicating the
signature information sent to your server that receives uplink messages.

There's also example data:

timestamp=1563105451261; nonce=:; value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c=

The keys are:

  • timestamp: standard Unix timestamp
  • nonce: colon
  • value: character string to be encrypted

This here is the part which I don't understand:

timestamp + nonce + Uplink message content: obtained after the encryption using the set password in HMAC-SHA256 algorithm and encoding in Base64.


How can I validate the message payload against the header signature?

What I've tried so far basically is:

private function parse_request_body(): void {
    $this->rawBody = stream_get_contents(STDIN);
    if (isset($_SERVER['X-HW-SIGNATURE']) && !empty($_SERVER['X-HW-SIGNATURE'])) {
        if (! $this->hmac_verify( $this->rawBody, $_SERVER['X-HW-SIGNATURE'] )) {
            // spoof message
        }
    }
}

private function hmac_verify( string $payload, string $signature ): bool {
    // the problem obviously lies here ...
    return true;
}
Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • Its a shame that the doc has *none* under the example, i hate that smh... Do you have a secret key within your account? If so could you do something like `$signed = hash_hmac("sha256", $payload, $secretKey);` then base64 it and check against `return $signed === $signature`... Just giving ideas here.. – Bossman Jan 12 '22 at 23:20
  • I've just came across `hash_hmac()`, too. But still not sure a) if `base64_encode()` happens before or after ...and if the `$secret` might be the client secret - or where else to obtain the encryption key? Cannot even test this right now (locally). – Martin Zeitler Jan 12 '22 at 23:28
  • I thought exactly the same when reading the doc, (also there must be a key somewhere) I would think like this `$signed = base64_encode(hash_hmac("sha256", $payload, $secretKey));` Also you may need to pass the `X-HW-TIMESTAMP` too as it is **"timestamp + nonce + Uplink message content"** or *timestamp+nonce+payload*.. I think you're going to have to experiment and try to match the signature.. – Bossman Jan 12 '22 at 23:33
  • hi@Martin Zeitler, may i confirm why you need to validate `X-HW-SIGNATURE`? – zhangxaochen Jan 13 '22 at 07:23
  • Hey read this article, may be helpful you. I already use in the Twilio webhook. https://www.twilio.com/docs/usage/security – Rishabh Rawat Jan 13 '22 at 07:27
  • @shirley When a message is being signed on the sending side (as the docs describe it), one would assume that best practice would be to validate the given signature on the receiving side again. I know this from eg. Stripe API web-hooks, Twilio API web-hooks may be similar. Here's the whole class: [`UpstreamMessage.php`](https://github.com/syslogic/php-hms/blob/master/src/PushKit/UpstreamMessage.php). – Martin Zeitler Jan 14 '22 at 13:52

3 Answers3

2

This is how i would go about verifying the signature. From my understanding from the doc. However it isn't 100% clear as they do not provide an example, which is a shame...

You should have (or be able to create one) a secret key within your Huawei account somewhere.

private function hmac_verify( string $payload, string $signature ): bool
{
    $secretKey = 'yoursecretkey';
    $parsedSignature = str_replace(';', '&', $signature); //'timestamp=1563105451261& nonce=:& value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c='
    parse_str($parsedSignature, $signatureParts);

    // $signatureParts
    //
    // array(3) {
    //  ["timestamp"]=>
    //  string(13) "1563105451261"
    //  ["nonce"]=>
    //  string(1) ":"
    //  ["value"]=>
    //  string(44) "E4YeOsnMtHZ6592U8B9S37238E Hwtjfrmpf8AQXF c="
    // }

    $signed = hash_hmac("sha256", $signatureParts['timestamp'] + $signatureParts['nonce'] + $payload, $secretKey);

    return base64_encode($signed) === $signatureParts['value'];
}
Bossman
  • 1,416
  • 1
  • 11
  • 17
  • I think the issue is, that `string $payload` is only the `data` payload, not the raw JSON... I've only found out after looking how such an upstream message is even being sent. Will test furthermore, by simply sending upstream messages - then I'll see (can log) what the actual payload would be. – Martin Zeitler Jan 13 '22 at 16:59
0

On another page of the documentation (X-HUAWEI-CALLBACK-ID), I've found a similar description:

Base64-encoded string that has been HMAC-SHA256 encrypted using the callback key. The string before encryption consists of the value of timestamp, value of nonce, and callback user name, without plus signs.


And here it's being described how to send push.hcm.upstream messages on Android. Sending an upstream message might be the best chance to obtain the payload, in order to validate a signature. The server-side procedure upon send as following:

When receiving the uplink message, the Push Kit server will:

  • Combine the receiving timestamp, colon (:), and uplink message into a character array to be encrypted (for example, 123456789:your_data).
  • Encrypt the character array in HMAC-SHA256 mode using an HMAC signature verification key, and encode the encrypted result in Base64 to generate a signature.
  • Transfer the signature and timestamp information to your app server through the X-HW-SIGNATURE and X-HW-TIMESTAMP fields in the HTTPS request header.

    Your app server needs to use the HTTPS request header and HMAC signature verification key to verify the validity of the uplink message.

Whatever "an HMAC signature verification key" may be; placeholder your_data sounds alike, as if (likely not yet base64 encoded) $payload->data would have been used to generate the signature:

/** Concatenate the input string, generate HMAC hash with SHA256 algorithm, then encode as base64. */
private function generate_signature( int $timestamp, string $nonce, string $data_str, string $secret_key): string {
    $input = $timestamp.$nonce.$data_str;
    $hmac = hash_hmac( 'sha256', $input, $secret_key );
    return base64_encode( $hmac );
}

/** Convert the received signature string to object. */
private function to_object( string $signature ): stdClass {
    $input = str_getcsv( $signature, '; ' );
    $data = new stdClass();
    $data->timestamp = (int) str_replace('timestamp=', '', $input[0]);
    $data->nonce  = (string) str_replace(   ' nonce=', '', $input[1]);
    $data->value  = (string) str_replace(   ' value=', '', $input[2]);
    return $data;
}

public function hmac_verify( string $raw_body, string $secret_key, string $signature ): bool {

    /* Extract data-string from the raw body. */
    $payload = json_decode( $raw_body );
    $data_str = base64_decode( $payload->data );

    /* Convert the received signature string to object. */
    $signature = $this->to_object( $signature );

    /* Generate a signature which to compare to. */
    $generated = $this->generate_signature( $signature->timestamp, $signature->nonce, $data_str, $secret_key);

    /* Compare the generated with the received signature. */
    return $generated === $signature->value;
}

Need to test this once with an actual $_POST ...


The "HMAC signature verification key" (per web-hook) can be obtained from the PushKit console:

screenshot

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
0

Thank you for providing the information regarding this issue. We are very sorry that it brings you inconvinience and are now organizing R&D team to supplement the sample code.

The X-HW-SIGNATURE field is used to check whether the message is from Huawei service.

enter image description here

enter image description here

Usage:

Timestamp + nonce + Uplink message content are combined into a character string. Use the configured HMAC HMAC-SHA256 algorithm and encoding in Base64 to compare the obtained value with the value sent by the push service. If they are the same, the message is from the push service, and you do not need to parse the specific value of this field.

zhangxaochen
  • 32,744
  • 15
  • 77
  • 108