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.