Webhooks - Java

I translated the Python Webhook code on the site into Java and my code is below. Any thoughts on why this isn’t pulling values from Fireblocks? Is my public key correct? I am using the https://api.fireblocks.io/v1 instance:

import com.bakkt.custody.provider.model.Event;
import com.bakkt.custody.provider.model.ExternalWalletAssetRemoved;
import com.bakkt.custody.provider.model.TransactionCreated;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Controller("/webhook")
public class WebhookController {

  private static final Logger LOG = LoggerFactory.getLogger(WebhookController.class);
  private PublicKey fireblocksPublicKey;
  private final ObjectMapper objectMapper;

  public WebhookController(ObjectMapper objectMapper) throws Exception {
    this.objectMapper = objectMapper;

    String fireblocksPublicKeyStr =
        """
    -----BEGIN PUBLIC KEY-----
    MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
    jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
    YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
    4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
    clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
    CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
    dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
    HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
    SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
    4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
    IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
    tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
    -----END PUBLIC KEY-----
    """
            .replace("-----BEGIN PUBLIC KEY-----\n", "")
            .replace("-----END PUBLIC KEY-----\n", "")
            .replace("\n", "");

    byte[] encoded = Base64.getDecoder().decode(fireblocksPublicKeyStr);
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    this.fireblocksPublicKey = kf.generatePublic(keySpec);
  }

  @Post
  @Consumes(MediaType.APPLICATION_JSON)
  public HttpResponse<String> handleWebhook(
      HttpRequest<String> request, @Header("Fireblocks-Signature") String fireblocksSignature)
      throws JsonProcessingException {
    String body = request.getBody().orElse("");
    LOG.info("Incoming request body: {}", body);

    try {
      boolean isSignatureValid =
          verifySignature(body.getBytes(StandardCharsets.UTF_8), fireblocksSignature);
      if (!isSignatureValid) {
        return HttpResponse.unauthorized();
      }

      LOG.info("Received request with body: {} and signature: {}", body, fireblocksSignature);

      Event event = objectMapper.readValue(body, Event.class);

      if (event instanceof ExternalWalletAssetRemoved) {
        ExternalWalletAssetRemoved externalWalletAssetRemovedEvent =
            (ExternalWalletAssetRemoved) event;
        LOG.info(
            "Received External Wallet Asset Removed event: {}", externalWalletAssetRemovedEvent);
      } else if (event instanceof TransactionCreated) {
        TransactionCreated transactionCreatedEvent = (TransactionCreated) event;
        LOG.info("Received Transaction Created event: {}", transactionCreatedEvent);
      } else {
        LOG.warn("Received unknown event: {}", event);
      }

      return HttpResponse.ok("Event received");
    } catch (Exception e) {
      LOG.error("Error handling webhook", e);
      return HttpResponse.serverError("Error handling webhook");
    }
  }

  private boolean verifySignature(byte[] data, String signature) throws Exception {
    Signature sig = Signature.getInstance("SHA512withRSA");
    sig.initVerify(fireblocksPublicKey);
    sig.update(data);
    LOG.info("Verifying signature...");
    boolean isVerified = sig.verify(Base64.getUrlDecoder().decode(signature));
    LOG.info("Signature verification result: {}", isVerified);
    return isVerified;
  }
}

Hi @moza,

My name is Alon and I am part of the Solutions Engineering team at Fireblocks.

The public key you’re using seems fine and matches the expected public key for the non-sandbox webhooks - here

Do you not get a request at all? meaning the LOG.info command in your handleWebhook method is not called?

Try to do the following:

  1. You must configure your own webhook URL (that is your own domain, not the https://api.fireblocks.io/v1 URL), to do this follow this guide
  2. Sandbox environments need a different public key (as mentioned above) - make sure to use that one in the event you’re using a Sandbox environment
  3. Webhooks are automatically sent as a response to events in your workspace, if nothing happens on your workspace then you won’t get webhooks. Try to make a transfer or transfer some assets into the workspace, you should get the webhook as a response
  4. Depending on your environment, it might be that some firewall or other network component is dropping the request from being received, check all network components from your webhook server (which needs to have access to the internet directly) to the internet and make sure none of them are dropping the traffic

Please let me know if the issue persists.

Thanks,
Alon

1 Like

Hi,
Hopping onto this thread. I have a similar issue implementing signature validation in .NET. Production environment
I am using public key=

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----

This is what the verification looks like

var isValid = rsa.VerifyData(messageBytes, signatureBytes, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
where
var messageBytes = Encoding.UTF8.GetBytes(body);

I also tried to verify with external tools
[https://8gwifi.org/RSAFunctionality?rsasignverifyfunctions=rsasignverifyfunctions&keysize=4096]

This tool also fails to validate the signature.

Can you provide a sample message and its signature that I can use to debug?

hey @aparanta sorry for the delayed response. It’s quite outdated post and I did not see the notification on your question here.

Were you able to figure this out or it’s still an issue that you’re experiencing?

Hi @SlavaSereb, this is resolved now. I found the correct message and signature to use :slight_smile: