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