Score:1

Verifying ECDSA-SHA256 HTTP Signature

zw flag

With PHP, I'm trying to setup a HTTP signature verification for webhook requests coming from BlockCypher: https://www.blockcypher.com/dev/bitcoin/?php#webhook-signing

This is their public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==

This is an HTTP request that I've collected using RequestCatcher:

POST / HTTP/1.1
Host: 2fgv7uy.requestcatcher.com
Accept-Encoding: gzip
Content-Length: 1551
Content-Type: application/json
Date: Wed, 15 Feb 2023 17:39:27 UTC
Digest: SHA-256=3TRiAfWYaoL0rYitfzGY7+prCTyS+UZsaVkBufCV7C4=
Signature: keyId="https://www.blockcypher.com/dev/bitcoin/#webhook-signing",algorithm="ecdsa-sha256",signature="njro4EF9wgn+Rph/3LIHGmd5al08oooDuRhVqoDmG3/TS6B6XkqKjCk19M4UAN1Xt1L67ybyfj8bdMChPHKcJA==",headers="(request-target) digest date"
User-Agent: BlockCypher HTTP Invoker
X-Eventid: 2cb85bdf-6181-49e6-9d9a-5e419898ee33
X-Eventtype: unconfirmed-tx
X-Ratelimit-Remaining: 174
{
  "block_height": -1,
  "block_index": -1,
  "hash": "67e5f9555f39280c0ea4d6b008b4f55a88c5c0e245e68106b7f4cdb6144b6bc0",
  "addresses": [
    "CFr99841LyMkyX5ZTGepY58rjXJhyNGXHf",
    "bcy1qz4uwhd2r9lv4x66xfxqzpw549c9rln7qa36ld5"
  ],
  "total": 624880000,
  "fees": 10000,
  "size": 222,
  "vsize": 222,
  "preference": "low",
  "relayed_by": "127.0.0.1:59162",
  "received": "2023-02-15T17:39:27.69Z",
  "ver": 1,
  "double_spend": false,
  "vin_sz": 1,
  "vout_sz": 2,
  "confirmations": 0,
  "inputs": [
    {
      "prev_hash": "98b1d29b6b89818e6db11bc6ab78423df953647b8acc465b5a386730dc34b0f2",
      "output_index": 1,
      "script": "4730440220542e880c02732bc6ab440c6664b32a97c38d72d91588459fcd8919d5ba11f44602206e17833ee1ce13d6c4a2a73a088538d9fa8b8704f06ba0e208e8e85ceb1c4bf5012102a44f60c94b840854db8c673e280dbc76b2975c6cf10e351ef6208f7f546e2130",
      "output_value": 624890000,
      "sequence": 4294967295,
      "addresses": [
        "CFr99841LyMkyX5ZTGepY58rjXJhyNGXHf"
      ],
      "script_type": "pay-to-pubkey-hash",
      "age": 677781
    }
  ],
  "outputs": [
    {
      "value": 100000,
      "script": "00141578ebb5432fd9536b46498020ba952e0a3fcfc0",
      "addresses": [
        "bcy1qz4uwhd2r9lv4x66xfxqzpw549c9rln7qa36ld5"
      ],
      "script_type": "pay-to-witness-pubkey-hash"
    },
    {
      "value": 624780000,
      "script": "76a914f93d302789520e8ca07affb76d4ba4b74ca3b3e688ac",
      "addresses": [
        "CFr99841LyMkyX5ZTGepY58rjXJhyNGXHf"
      ],
      "script_type": "pay-to-pubkey-hash"
    }
  ]
}

From what I can gather from the specification, and taking into account the request above, this would be the signing string ($signingString):

(request-target): post /\n
digest: SHA-256=3TRiAfWYaoL0rYitfzGY7+prCTyS+UZsaVkBufCV7C4=\n
date: Wed, 15 Feb 2023 17:39:27 UTC

EDIT I've also tried having the date in Unix time: 1676483082

I've successfully verified the digest like so (using Laravel):

$contentHash = hash('sha256', $request->getContent(), true);
$contentHashEncode = base64_encode($contentHash);
$expectedDigest = "SHA-256=$contentHashEncode";
$providedDigest = $request->header('digest');

$digestMatch = hash_equals($expectedDigest, $providedDigest);

dump("digestMatch\n$digestMatch"); // true

if (! $digestMatch)
    return abort(401);

As for the actual signature verification, I've been stuck for the past two days. I've tried PHP's openssl_verify, but it doesn't work.

I've tried phpseclib3 without success, I have no clue why. I'm new to cryptography so most of it goes above my head.

use phpseclib3\Crypt\PublicKeyLoader;

$signatureHeader = $request->header('signature');
$signatureHeaderArray = explode(',', $signatureHeader);

$providedSignature = Str::of($signatureHeaderArray[2])->after('signature="')->beforeLast('"');
// providedSignature: njro4EF9wgn+Rph/3LIHGmd5al08oooDuRhVqoDmG3/TS6B6XkqKjCk19M4UAN1Xt1L67ybyfj8bdMChPHKcJA==
// I've also tried base64_decode(), then bin2hex(), then base64_encode() the signature:
// base64: OWUzYWU4ZTA0MTdkYzIwOWZlNDY5ODdmZGNiMjA3MWE2Nzc5NmE1ZDNjYTI4YTAzYjkxODU1YWE4MGU2MWI3ZmQzNGJhMDdhNWU0YThhOGMyOTM1ZjRjZTE0MDBkZDU3Yjc1MmZhZWYyNmYyN2UzZjFiNzRjMGExM2M3MjljMjQ=
// hash: 9e3ae8e0417dc209fe46987fdcb2071a67796a5d3ca28a03b91855aa80e61b7fd34ba07a5e4a8a8c2935f4ce1400dd57b752faef26f27e3f1b74c0a13c729c24

$pubkey_pem = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==\n-----END PUBLIC KEY-----";
$key = PublicKeyLoader::loadPublicKey($pubkey_pem);        
$verify = $key->verify($signingString, $providedSignature);
echo $verify; // always false

I'm guessing the issue here is constructing the signature string from the provided HTTP request.

fgrieu avatar
ng flag
Programming questions are off-topic, and mods/users will close it. Glancing at it, you seem to have narrowed the issue to the last block of code. [snip] I suggest that right before `$key->verify` is invoked you dump `$signingString`, `$providedSignature`, `$pubkey_pem`, and whatever of `$key` can be meaningfully dumped, and ponder that. Adding that to the question would make it even more off-topic, I'm afraid. But a link to a pastebin?
Carlos avatar
zw flag
@fgrieu The signing string is the second code block. The real issue here might be constructing the signature string. I have no way of knowing if that's the correct string or not. I tried following the specification as best as I could: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#page-7
fgrieu avatar
ng flag
There is no such thing as _the_ correct signature string, because ECDSA is not deterministic. However it's defined what _a_ correct signature string is, given the message (`$signingString`) and public key (`$pubkey_pem`). That's why I suggest to dump these two and `$providedSignature`.
Carlos avatar
zw flag
@fgrieu It is my understanding if you take into context the provided HTTP request, then there is one correct signature string for this specific case. All of those variables are within the code I've provided, but I've edited it to make it clearer.
fgrieu avatar
ng flag
**Comments have been [moved to chat](https://chat.stackexchange.com/rooms/142930/discussion-on-question-by-carlos-verifying-ecdsa-sha256-http-signature); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.