2
$\begingroup$

I must verify an HTTP signature to guarantee the origin and integrity of a webhook data: https://www.blockcypher.com/dev/bitcoin/#webhook-signing

This is their x509 PKIX encoded signing key's public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==

I am following this specification to construct the signing string: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3

Here is a POST request from said webhook:

POST /api/blockcypher HTTP/1.1
Host: (...removed...)
Accept-Encoding: gzip
Content-Length: 1551
Content-Type: application/json
Date: Thu, 16 Feb 2023 11:55:43 UTC
Digest: SHA-256=BYzO03UhXY2O4gzJd+8g367LSEtO3cAMJfOjL0F0HiE=
Signature: keyId="https://www.blockcypher.com/dev/bitcoin/#webhook-signing",algorithm="ecdsa-sha256",signature="ZJfhAFQP19xfnWZo9KD9hyzoyR8fZ5uEyh6Ltu3z/vWL7Jr+aezw8n9U9Yq41APWJG+vnhmS8KoGV48X34vMYQ==",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
{
    (...removed...)
}

I've successfully matched the provided Digest header with a constructed digest of my own using the request's (removed) content (I've removed the content to reduce visual bloat).

From what I can gather with the specification provided above, this would be the constructed signature string for this particular request:

(request-target): post /api/blockcypher\n
digest: SHA-256=BYzO03UhXY2O4gzJd+8g367LSEtO3cAMJfOjL0F0HiE=\n
date: Thu, 16 Feb 2023 11:55:43 UTC

The specification mentions the (created) and (expired) headers' values "MUST be a Unix timestamp integer value", but doesn't talk about the Date header. I've tried both Thu, 16 Feb 2023 11:55:43 UTC and 1676548543 without success.

The specification is confusing to me in some aspects. For instance, the signature string where the (created) field is included, does not end with a new line (\n).

My question is, is my signature string well constructed? If not, what am I doing wrong?

To sum up:

PUBLIC KEY (x509 PKIX encoded, I don't know what that means)

MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==

SIGNATURE

ZJfhAFQP19xfnWZo9KD9hyzoyR8fZ5uEyh6Ltu3z/vWL7Jr+aezw8n9U9Yq41APWJG+vnhmS8KoGV48X34vMYQ==

MESSAGE (I think)

(request-target): post /api/blockcypher\ndigest: SHA-256=BYzO03UhXY2O4gzJd+8g367LSEtO3cAMJfOjL0F0HiE=\ndate: Thu, 16 Feb 2023 11:55:43 UTC

The verification fails. I'm guessing it's because the signature string is not properly constructed.

$\endgroup$

2 Answers 2

3
$\begingroup$

Works for me:

PublicKey pubkey = KeyFactory.getInstance("EC").generatePublic( new X509EncodedKeySpec(
    Base64.getDecoder().decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==") ));
Signature s = Signature.getInstance("SHA256withECDSAinP1363format"); s.initVerify(pubkey);
s.update("(request-target): post /api/blockcypher\ndigest: SHA-256=BYzO03UhXY2O4gzJd+8g367LSEtO3cAMJfOjL0F0HiE=\ndate: Thu, 16 Feb 2023 11:55:43 UTC".getBytes());
System.out.println ( s.verify( Base64.getDecoder().decode("ZJfhAFQP19xfnWZo9KD9hyzoyR8fZ5uEyh6Ltu3z/vWL7Jr+aezw8n9U9Yq41APWJG+vnhmS8KoGV48X34vMYQ==") ) );
->
true

Note that signature is in 'plain' P1363, CVC, or PKCS11 format, not ASN.1 DER format. Some verifiers require one, some the other, and some are selectable like the Java (9 up) provider I used. (And some generators create one, or the other, or both.) Are you using something that accepts plain format?

See Shouldn't a signature using ECDSA be exactly 96 bytes, not 102 or 103?
and ECDSA Signature R|S to ASN1 DER Encoding question
and cross https://stackoverflow.com/questions/61775022/openssl-ecdsa-sign-input-as-is-without-digest
and https://stackoverflow.com/questions/52122705/java-ecdsawithsha256-signature-with-inconsistent-length
and more dupes you can find by following 'Linked'

$\endgroup$
2
  • $\begingroup$ Good to know the problem isn't with the signature construction. First I attempted PHP's openssl_verify, but it doesn't work. Then I tried phpseclib3 also without success. $\endgroup$
    – Carlos
    Feb 17, 2023 at 10:24
  • 1
    $\begingroup$ Carlos: PHP openssl_ functions use OpenSSL (which should not be a surprise) and OpenSSL supports only DER format for ECDSA (and also DSA FWIW). I don't use phpseclib, but under your link, at phpseclib.com/docs/ec#creating--verifying-signatures . it says for ECDSA it supports a signature format 'SSH2' -- which is actually the SSH2 format for DSA-aka-DSS not(!) ECDSA -- but looks like it might work. (I can't easily test.) $\endgroup$ Feb 18, 2023 at 3:07
0
$\begingroup$

For everyone like me that was struggled trying to replicate @dave_thompson_085 answer, here is tha same code in node:

const crypto = require("crypto");

const public_key = getEcPublicKeyPem();

const data =
  "(request-target): post /api/blockcypher\ndigest: SHA-256=BYzO03UhXY2O4gzJd+8g367LSEtO3cAMJfOjL0F0HiE=\ndate: Thu, 16 Feb 2023 11:55:43 UTC";

const signature =
  "ZJfhAFQP19xfnWZo9KD9hyzoyR8fZ5uEyh6Ltu3z/vWL7Jr+aezw8n9U9Yq41APWJG+vnhmS8KoGV48X34vMYQ==";

const signatureVerified = ecVerifySignatureFromBase64(
  public_key,
  data,
  signature
);

// Base64 encoded signature
console.log("\nsignature (Base64) verified: ", signatureVerified);

function ecVerifySignatureFromBase64(publicKey, message, signatureBase64) {
  const signature = Buffer.from(signatureBase64, "base64");
  const verifier = crypto.createVerify("sha256");
  verifier.update(message);
  verifier.end();
  return verifier.verify(
    { key: publicKey, dsaEncoding: "ieee-p1363" },
    signature
  );
}

function getEcPublicKeyPem() {
  const key =
    "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEflgGqpIAC9k65JicOPBgXZUExen4rWLq05KwYmZHphTU/fmi3Oe/ckyxo2w3Ayo/SCO/rU2NB90jtCJfz9i1ow==";
  return `-----BEGIN PUBLIC KEY-----\n${key}\n-----END PUBLIC KEY-----\n`;
}

Hope this can help others! all credit for @dave_thompson_085

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.