Skip to content
Advertisement

Problem verifying signature using public key

I have a webhook that sends a header, which needs to be verified. Below are some details:

Problem :

The Java method always returns false. The provided header and body are correct and should result as TRUE.

As per docs from the provider :

Signature = Base64(RSA512(WEBHOOK_PRIVATE_KEY, SHA512(eventBody)))

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-----

Header :

YNYVgWsx3PdoEGq2nUFGLmE6tE2y0LCc/eWPSY+rAqK+8fcxrPN0SPGbTdAXQ9+v62T5akWaVRWKXc1YBWlZxhVTCa/Ou7FfjVPG6JIQNX3Lks3ZhW0k29bVKf7Qvjp7z8HM9s1D8ZC28HvpX15a4by7DpNKkQ6cLWMDtBvqY02FSO+L4Vq54GZoTrplYkqCYcI4/oWchYzMZMq4omIOuam2DXm5BLlZ7HCR/nhUyp5duJpYnWJCKEwOTh3zLm842r5Fa9humq9WKkkT+AgFxe95bG4F3p8XhsciXiaNgx8diKLF0aBklqJ6yA70vjIP92BHuEnvIl37RiSFiIvkYWvLpMc1LoPxWZvncaLUjlYSVT3zd/gCDPEn1Mu8wUogGt9npkc/eKMdrKefcjEIMrJoO0HMMREZcOpc72F0+RM4QCkMaQMmK4zq9cBF0E2bNaEabNDSWXIfx9fa2VuyGYa5GLmAPUQPYRv50n92IGFewxj9vFAWhca+uthvsqz3FekyHK+c9G1Wh9OScR2TQp9Lbe4LqlX4FGapQitmfDvKRJhjAVm0n5355+k1dRse4fGeXqd2EfledWUJ3egpmW1NlmWBr8d4PsruKYZnphEMn9F5F3Vyu2sCpBSvqmcMANXzyZP7u3lGsUpH4V2lM6nCeBiRcbfwyrFsJ6Q5dso=

RequestBody:

{"type":"TRANSACTION_STATUS_UPDATED","tenantId":"f4df1e73-ec68-53c5-aa92-1a2bc45900ef","timestamp":1671288284087,"data":{"id":"b96f37dd-0fe9-4aa0-853b-f7d39c2ddc52","createdAt":1671286112004,"lastUpdated":1671286112019,"assetId":"BTC_TEST","source":{"id":"","type":"UNKNOWN","name":"External","subType":""},"destination":{"id":"26","type":"VAULT_ACCOUNT","name":"55b49ae0-0f34-4b3c-8cf6-0094254261c2","subType":""},"amount":1.0E-5,"networkFee":1.41E-6,"netAmount":1.0E-5,"sourceAddress":"tb1qluc5wgms8kpu0tydu00590qryfan3969jvmc8e","destinationAddress":"tb1qyhfnsfe2dy8az3040yvx087qdfsw6yxk8pc7yj","destinationAddressDescription":"","destinationTag":"","status":"CONFIRMING","txHash":"15873dc631db22a1bd13c6adecf7fb63f8fbbecce36eb38d12df65573e83dfa9","subStatus":"PENDING_BLOCKCHAIN_CONFIRMATIONS","signedBy":[],"createdBy":"","rejectedBy":"","amountUSD":0.17,"addressType":"","note":"","exchangeTxId":"","requestedAmount":1.0E-5,"feeCurrency":"BTC_TEST","operation":"TRANSFER","customerRefId":null,"numOfConfirmations":2,"amountInfo":{"amount":"0.00001","requestedAmount":"0.00001","netAmount":"0.00001","amountUSD":"0.17"},"feeInfo":{"networkFee":"0.00000141"},"destinations":[],"externalTxId":null,"blockInfo":{"blockHeight":"2411637","blockHash":"00000000d45b7ebf40921cbc70fb6791985a0b256241757a28bf762be07478e8"},"signedMessages":[],"index":1}}

Java method that’s called when webhook event is received :

public boolean matches(WebhookEvent body, String header){

        try {
            File publicKeyFile = new File("publicKey.pub");

            byte[] bytes = PemUtils.parsePEMFile(publicKeyFile);
            KeyFactory kf = KeyFactory.getInstance("RSA");
            X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
            PublicKey publicKey = kf.generatePublic(spec);

            ObjectMapper objectMapper = new ObjectMapper();
            String message = objectMapper.writeValueAsString(body);

            Signature verifier = Signature.getInstance("SHA512withRSA");
            verifier.initVerify(publicKey);
            verifier.update(message.getBytes());

            boolean isVerified = verifier.verify(Base64.getDecoder().decode(header));
            System.out.println("Verified: " + isVerified);

            return isVerified;
        } catch (Exception e){
            log.error("Error while verifying signature : " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

PemFile parser :

    static byte[] parsePEMFile(File pemFile) throws IOException {
        if (!pemFile.isFile() || !pemFile.exists()) {
            throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
        }
        PemReader reader = new PemReader(new FileReader(pemFile));
        PemObject pemObject = reader.readPemObject();
        byte[] content = pemObject.getContent();
        reader.close();
        return content;
    }

Request POJO

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WebhookEvent {

    @JsonProperty(value = "type")
    private FireblocksEventType eventType;

    @JsonProperty(value = "tenantId")
    private String tenantId;

    @JsonProperty(value = "timestamp")
    private long timestamp;

    @JsonProperty(value = "data")
    private TransactionDetailObject eventData;
}

TransactionDetailObject :

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class TransactionDetailObject {

    private String id;
    private String assetId;
    private TransferPeerPathResponse source;
    private TransferPeerPathResponse destination;
    private BigDecimal requestedAmount;
    private AmountInfo amountInfo;
    private FeeInfo feeinfo;
    private BigDecimal amount;
    private BigDecimal netAmount;
    private BigDecimal amountUSD;
    private BigDecimal serviceFee;
    private Boolean treatAsGrossAmount;
    private BigDecimal networkFee;
    private Long createdAt;
    private Long lastUpdated;
    private TransactionStatus status;
    private String txHash;
    private Long index;
    private TransactionSubStatus subStatus;
    private String sourceAddress;
    private String destinationAddress;
    private String destinationAddressDescription;
    private String destinationTag;
    private List<String> signedBy;
    private String createdBy;
    private String rejectedBy;
    private String addressType;
    private String note;
    private String exchangeTxId;
    private String feeCurrency;
    private TransactionOperation operation;
    private AmlScreeningResult amlScreeningResult;
    private String customerRefId;
    private Long numOfConfirmations;
    private List<NetworkRecord>networkRecords;
    private String replacedTxHash;
    private String externalTxId;
    private List<DestinationResponse>destinations;
    private BlockInfo blockInfo;
    private RewardsInfo rewardsInfo;
    private AuthorizationInfo authorizationInfo;
    private List<SignedMessage>signedMessages;
    private Object extraParameters;
}

Advertisement

Answer

As mentioned in the comments, verification of the posted signature with the posted key is successful if the following message is used:

String message = "{"type":"TRANSACTION_STATUS_UPDATED","tenantId":"f4df1e73-ec68-53c5-aa92-1a2bc45900ef","timestamp":1671288284087,"data":{"id":"b96f37dd-0fe9-4aa0-853b-f7d39c2ddc52","createdAt":1671286112004,"lastUpdated":1671286112019,"assetId":"BTC_TEST","source":{"id":"","type":"UNKNOWN","name":"External","subType":""},"destination":{"id":"26","type":"VAULT_ACCOUNT","name":"55b49ae0-0f34-4b3c-8cf6-0094254261c2","subType":""},"amount":0.00001,"networkFee":0.00000141,"netAmount":0.00001,"sourceAddress":"tb1qluc5wgms8kpu0tydu00590qryfan3969jvmc8e","destinationAddress":"tb1qyhfnsfe2dy8az3040yvx087qdfsw6yxk8pc7yj","destinationAddressDescription":"","destinationTag":"","status":"CONFIRMING","txHash":"15873dc631db22a1bd13c6adecf7fb63f8fbbecce36eb38d12df65573e83dfa9","subStatus":"PENDING_BLOCKCHAIN_CONFIRMATIONS","signedBy":[],"createdBy":"","rejectedBy":"","amountUSD":0.17,"addressType":"","note":"","exchangeTxId":"","requestedAmount":0.00001,"feeCurrency":"BTC_TEST","operation":"TRANSFER","customerRefId":null,"numOfConfirmations":2,"amountInfo":{"amount":"0.00001","requestedAmount":"0.00001","netAmount":"0.00001","amountUSD":"0.17"},"feeInfo":{"networkFee":"0.00000141"},"destinations":[],"externalTxId":null,"blockInfo":{"blockHeight":"2411637","blockHash":"00000000d45b7ebf40921cbc70fb6791985a0b256241757a28bf762be07478e8"},"signedMessages":[],"index":1}}";

In order for verification to succeed, the exact same message must be used as for signing (i.e. the same formatting and order).
However, on my machine, when filling the WebhookEvent object with RequestBody, the posted code generates a different message when using the posted POJOs:

String message = "{"type":"TRANSACTION_STATUS_UPDATED","tenantId":"f4df1e73-ec68-53c5-aa92-1a2bc45900ef","timestamp":1671288284087,"data":{"id":"b96f37dd-0fe9-4aa0-853b-f7d39c2ddc52","assetId":"BTC_TEST","source":{"id":"","type":"UNKNOWN","name":"External","subType":""},"destination":{"id":"26","type":"VAULT_ACCOUNT","name":"55b49ae0-0f34-4b3c-8cf6-0094254261c2","subType":""},"requestedAmount":0.000010,"amountInfo":{"amount":"0.00001","requestedAmount":"0.00001","netAmount":"0.00001","amountUSD":"0.17"},"feeinfo":null,"amount":0.000010,"netAmount":0.000010,"amountUSD":0.17,"serviceFee":null,"treatAsGrossAmount":null,"networkFee":0.00000141,"createdAt":1671286112004,"lastUpdated":1671286112019,"status":"CONFIRMING","txHash":"15873dc631db22a1bd13c6adecf7fb63f8fbbecce36eb38d12df65573e83dfa9","index":1,"subStatus":"PENDING_BLOCKCHAIN_CONFIRMATIONS","sourceAddress":"tb1qluc5wgms8kpu0tydu00590qryfan3969jvmc8e","destinationAddress":"tb1qyhfnsfe2dy8az3040yvx087qdfsw6yxk8pc7yj","destinationAddressDescription":"","destinationTag":"","signedBy":[],"createdBy":"","rejectedBy":"","addressType":"","note":"","exchangeTxId":"","feeCurrency":"BTC_TEST","operation":"TRANSFER","amlScreeningResult":null,"customerRefId":null,"numOfConfirmations":2,"networkRecords":null,"replacedTxHash":null,"externalTxId":null,"destinations":[],"blockInfo":{"blockHeight":"2411637","blockHash":"00000000d45b7ebf40921cbc70fb6791985a0b256241757a28bf762be07478e8"},"rewardsInfo":null,"authorizationInfo":null,"signedMessages":[],"extraParameters":null}}";

RequestBody differs from the correct message only in that scientific notation is used instead of the plain format for BigDecimal.
If RequestBody reliably matches the correct message apart from the BigDecimal format, a correction of the BigDecimal format might be enough for successful verification, e.g. with:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.math.BigDecimal;
...
private static String fixDecimalNumberFormat(String json) throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    JsonNode jsonNode = objectMapper.readTree(json);
    ObjectNode objectNode = (ObjectNode)jsonNode.get("data");
    objectNode.put("createdAt", new BigDecimal(objectNode.get("createdAt").toString()));
    objectNode.put("amount", new BigDecimal(objectNode.get("amount").toString()));
    objectNode.put("networkFee", new BigDecimal(objectNode.get("networkFee").toString()));
    objectNode.put("netAmount", new BigDecimal(objectNode.get("netAmount").toString()));
    objectNode.put("amountUSD", new BigDecimal(objectNode.get("amountUSD").toString()));
    objectNode.put("requestedAmount", new BigDecimal(objectNode.get("requestedAmount").toString()));
    return jsonNode.toString();
}

Otherwise, the rules for formatting and order must be agreed with the signing side.


Test:

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
...
public static void main(String[] args) throws Exception {
    
    // Import key and signature 
    byte[] derKey = Base64.getDecoder().decode("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZGjjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOHclM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVdCGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZdE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5BxqdHgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNPSwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh14k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadcIMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnktSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==");
    KeyFactory kf = KeyFactory.getInstance("RSA");
    X509EncodedKeySpec spec = new X509EncodedKeySpec(derKey);
    PublicKey publicKey = kf.generatePublic(spec);
    String header = "YNYVgWsx3PdoEGq2nUFGLmE6tE2y0LCc/eWPSY+rAqK+8fcxrPN0SPGbTdAXQ9+v62T5akWaVRWKXc1YBWlZxhVTCa/Ou7FfjVPG6JIQNX3Lks3ZhW0k29bVKf7Qvjp7z8HM9s1D8ZC28HvpX15a4by7DpNKkQ6cLWMDtBvqY02FSO+L4Vq54GZoTrplYkqCYcI4/oWchYzMZMq4omIOuam2DXm5BLlZ7HCR/nhUyp5duJpYnWJCKEwOTh3zLm842r5Fa9humq9WKkkT+AgFxe95bG4F3p8XhsciXiaNgx8diKLF0aBklqJ6yA70vjIP92BHuEnvIl37RiSFiIvkYWvLpMc1LoPxWZvncaLUjlYSVT3zd/gCDPEn1Mu8wUogGt9npkc/eKMdrKefcjEIMrJoO0HMMREZcOpc72F0+RM4QCkMaQMmK4zq9cBF0E2bNaEabNDSWXIfx9fa2VuyGYa5GLmAPUQPYRv50n92IGFewxj9vFAWhca+uthvsqz3FekyHK+c9G1Wh9OScR2TQp9Lbe4LqlX4FGapQitmfDvKRJhjAVm0n5355+k1dRse4fGeXqd2EfledWUJ3egpmW1NlmWBr8d4PsruKYZnphEMn9F5F3Vyu2sCpBSvqmcMANXzyZP7u3lGsUpH4V2lM6nCeBiRcbfwyrFsJ6Q5dso=";

    // Fix BigDecimal format, message from RequestBody
    String message = "{"type":"TRANSACTION_STATUS_UPDATED","tenantId":"f4df1e73-ec68-53c5-aa92-1a2bc45900ef","timestamp":1671288284087,"data":{"id":"b96f37dd-0fe9-4aa0-853b-f7d39c2ddc52","createdAt":1671286112004,"lastUpdated":1671286112019,"assetId":"BTC_TEST","source":{"id":"","type":"UNKNOWN","name":"External","subType":""},"destination":{"id":"26","type":"VAULT_ACCOUNT","name":"55b49ae0-0f34-4b3c-8cf6-0094254261c2","subType":""},"amount":1.0E-5,"networkFee":1.41E-6,"netAmount":1.0E-5,"sourceAddress":"tb1qluc5wgms8kpu0tydu00590qryfan3969jvmc8e","destinationAddress":"tb1qyhfnsfe2dy8az3040yvx087qdfsw6yxk8pc7yj","destinationAddressDescription":"","destinationTag":"","status":"CONFIRMING","txHash":"15873dc631db22a1bd13c6adecf7fb63f8fbbecce36eb38d12df65573e83dfa9","subStatus":"PENDING_BLOCKCHAIN_CONFIRMATIONS","signedBy":[],"createdBy":"","rejectedBy":"","amountUSD":0.17,"addressType":"","note":"","exchangeTxId":"","requestedAmount":1.0E-5,"feeCurrency":"BTC_TEST","operation":"TRANSFER","customerRefId":null,"numOfConfirmations":2,"amountInfo":{"amount":"0.00001","requestedAmount":"0.00001","netAmount":"0.00001","amountUSD":"0.17"},"feeInfo":{"networkFee":"0.00000141"},"destinations":[],"externalTxId":null,"blockInfo":{"blockHeight":"2411637","blockHash":"00000000d45b7ebf40921cbc70fb6791985a0b256241757a28bf762be07478e8"},"signedMessages":[],"index":1}}";
    message = fixDecimalNumberFormat(message);
    
    // Verify signature
    Signature verifier = Signature.getInstance("SHA512withRSA");
    verifier.initVerify(publicKey);
    verifier.update(message.getBytes());
    boolean isVerified = verifier.verify(Base64.getDecoder().decode(header));

    System.out.println(message);
    System.out.println(isVerified);
}

performs a successful verification of the fixed data from RequestBody:

{"type":"TRANSACTION_STATUS_UPDATED","tenantId":"f4df1e73-ec68-53c5-aa92-1a2bc45900ef","timestamp":1671288284087,"data":{"id":"b96f37dd-0fe9-4aa0-853b-f7d39c2ddc52","createdAt":1671286112004,"lastUpdated":1671286112019,"assetId":"BTC_TEST","source":{"id":"","type":"UNKNOWN","name":"External","subType":""},"destination":{"id":"26","type":"VAULT_ACCOUNT","name":"55b49ae0-0f34-4b3c-8cf6-0094254261c2","subType":""},"amount":0.00001,"networkFee":0.00000141,"netAmount":0.00001,"sourceAddress":"tb1qluc5wgms8kpu0tydu00590qryfan3969jvmc8e","destinationAddress":"tb1qyhfnsfe2dy8az3040yvx087qdfsw6yxk8pc7yj","destinationAddressDescription":"","destinationTag":"","status":"CONFIRMING","txHash":"15873dc631db22a1bd13c6adecf7fb63f8fbbecce36eb38d12df65573e83dfa9","subStatus":"PENDING_BLOCKCHAIN_CONFIRMATIONS","signedBy":[],"createdBy":"","rejectedBy":"","amountUSD":0.17,"addressType":"","note":"","exchangeTxId":"","requestedAmount":0.00001,"feeCurrency":"BTC_TEST","operation":"TRANSFER","customerRefId":null,"numOfConfirmations":2,"amountInfo":{"amount":"0.00001","requestedAmount":"0.00001","netAmount":"0.00001","amountUSD":"0.17"},"feeInfo":{"networkFee":"0.00000141"},"destinations":[],"externalTxId":null,"blockInfo":{"blockHeight":"2411637","blockHash":"00000000d45b7ebf40921cbc70fb6791985a0b256241757a28bf762be07478e8"},"signedMessages":[],"index":1}}
true
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement