1

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.

Carlos
  • 33
  • 4
  • 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? – fgrieu Feb 16 '23 at 11:19
  • 1
    @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 – Carlos Feb 16 '23 at 11:21
  • 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. – fgrieu Feb 16 '23 at 12:12
  • @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. – Carlos Feb 16 '23 at 12:27
  • Comments have been moved to chat; please do not continue the discussion here. Before posting a comment below this one, please review the purposes of comments. Comments that do not request clarification or suggest improvements usually belong as an answer, on [meta], or in [chat]. Comments continuing discussion may be removed. – fgrieu Feb 16 '23 at 15:38

0 Answers0