HOOK-28: fixed signature format (#16)

* HOOK-28: fixed signature format

* fixed keysize and readme

* fixed minor bug

* fixed base64 encoder and decoder to url safe version

* up pom version
This commit is contained in:
Inal Arsanukaev 2017-05-17 13:22:32 +03:00 committed by GitHub
parent c5f4a5beb0
commit 7dca3749fc
5 changed files with 11 additions and 61 deletions

View File

@ -4,75 +4,25 @@
[![Build Status](http://ci.rbkmoney.com/buildStatus/icon?job=rbkmoney_private/hooker/master)](http://ci.rbkmoney.com/job/rbkmoney_private/job/hooker/job/master/)
1. Сервис предоставляет интерфейс для CAPI для создания, удаления вебхуков, установки/редактирования опций вебхука (таких как url, public/private keys - они генерируются сервисом, типы событий).
2. Сервис поллит bustermaze на появление событий, по которым должны отправляться сообщения на вебхуки
2. Сервис поллит bustermaze на появление событий, по которым должны отправляться сообщения мерчантам
Интерфейс для capi доступен по пути /hook
Для более подробного ознакомления со структурой объектов можно воспользоваться ссылкой на сам объект [webhooker.thrift][1]
### Типы событий, по которым отправляются сообщения на вебхуки
Сообщения отправляются на следующие события:
1. Создание инвойса
2. Изменение статуса инвойса
3. Создание платежа
4. Изменение статуса платежа
### Формат сообщения, отправляемого мерчанту
#### Пример отправки запроса мерчанту
###### Отправка данных
Запрос в формате JSON отправляется методом POST на url, указанный при создании вебхука.
Подпись идет в headers запроса.
Тело запроса содержит поля (все поля, кроме payment_id, обязательные):
Название | Описание | Пример
------------ | ------------- | -------------
**event_type** | тип эвента (инвойс или платеж) | invoice (payment)
**invoice_id** | номер инвойса | 2Dbs4d4Dw
**payment_id** | номер платежа | fsee5562
**shop_id** | идентификатор магазина | 55
**amount** | сумма, в минорных единицах | 120000
**currency** | валюта (ISO 4217)| RUB
**created_at** | дата и время создания инвойса в формате UTC (RFC 3339) | 2011-07-01T09:00:00Z
**metadata** | данные, переданные мерчантом при создании инвойса | {"order_id":"my_order_id"}
**status** | статус инвойса(платежа) | для инвойса (unpaid, paid, cancelled, fulfilled); для платежа (pending, processed, captured, cancelled, failed)
Таймаут на соединение и на запрос составляет 10с.
[Cписок кодов состояния HTTP][3]
#### Пример формирования подписи
Тело запроса (JSON) подписывается и подпись добавляется как заголовок http-запроса. Имя заголовка - X-Signature.
Тело запроса для события "Инвойс создан" будет выглядеть так:
```
{"event_type":"invoice","invoice_id":"45b69f6ab0","payment_id":null,"shop_id":1,"amount":6207,"currency":"RUB","created_at":"2017-04-10T21:53:09.271Z","metadata":{"type":"contentType","data":"dGVzdA==","fields":["TYPE","DATA"],"fieldMetaData":{"TYPE":{"fieldName":"type","requirementType":1,"valueMetaData":{"type":11,"typedefName":null,"binary":false,"container":false,"struct":false,"typedef":false}},"DATA":{"fieldName":"data","requirementType":1,"valueMetaData":{"type":11,"typedefName":null,"binary":true,"container":false,"struct":false,"typedef":false}}},"setType":true,"setData":true},"status":"unpaid"}
```
После чего строка подписывается приватным ключом RSA с алгоритмом хэширования SHA-256 и размером ключа 1024.
Подробная информация по протоколу отправки сообщений мерчантам описана в [swagger-спецификации][2]
#### Пример запроса к мерчанту
```
curl -v -X POST
-H "Content-Type: application/json; charset=utf-8"
-H "cor2A+pQ9dOMwUq8U5xUeLwh9UQD3DtzHHxcC5pplChgwmRKH5LG9Wz/rnZ2GLwUzLelowisVre1aHUKGJSl/NSF0PWJSWfCbMVtKpMwA9ZRb4pcKVX/RBiczCUIX+gFVj/6G7qWXBCxbcBV/u/vn61rWFBYi1Knvmaov4kGvZo="
-d '{"event_type":"invoice","invoice_id":"45b69f6ab0","payment_id":null,"shop_id":1,"amount":6207,"currency":"RUB","created_at":"2017-04-10T21:53:09.271Z","metadata":{"type":"contentType","data":"dGVzdA==","fields":["TYPE","DATA"],"fieldMetaData":{"TYPE":{"fieldName":"type","requirementType":1,"valueMetaData":{"type":11,"typedefName":null,"binary":false,"container":false,"struct":false,"typedef":false}},"DATA":{"fieldName":"data","requirementType":1,"valueMetaData":{"type":11,"typedefName":null,"binary":true,"container":false,"struct":false,"typedef":false}}},"setType":true,"setData":true},"status":"unpaid"}
-H "Content-Signature: alg=RS256; digest=QydtAwZ8jmmrX1wP87vAJIXVBiRZe3Zo5xRYnzMQIsDxxsVn26XGMBeD4op8_9LmXIAirYWd53gfX638ZR83D2pTSCXkNpzHyqiyzhPlH2asaC9qiiOOWaQ-kHk9xFlOcMa1Qtt0BaVst-tbGDsFhgjl6tvGuN4JHRq5khzl_iIJ_ZQniEuzGOYReWn8wCLmaspX0MPyACMTo8HDwyihOHB1_RQBliBFYyvw523xvSQ6WxWYjYhFsjglIg1wdKUMyaiScw0kmKm53OdxDAnjl4MPQmtryuANjbklN8_EatOrQAqGwRUp1ayR_3WMlayhpxaEHlG1sAHQaaO3ulI35g=="
-d '{"eventID":27,"occuredAt":"2017-05-16T13:49:34.935099Z","topic":"InvoicesTopic","eventType":"PaymentCaptured","invoice":{"id":"qXMiygTqb2","shopID":1,"createdAt":"2017-05-16T13:49:32.753723Z","status":"unpaid","reason":null,"dueDate":"2017-05-16T13:59:32Z","amount":100000,"currency":"RUB","metadata":{"type":"application/json","data":"eyJpbnZvaWNlX2R1bW15X2NvbnRleHQiOiJ0ZXN0X3ZhbHVlIn0="},"product":"test_product","description":"test_invoice_description"},"payment":{"id":"1","createdAt":"2017-05-16T13:49:33.182195Z","status":"captured","error":null,"amount":100000,"currency":"RUB","paymentToolToken":"5Gz2nhE1eleFGBAcGe9SrA","paymentSession":"2nTYVgk6h85O7vIVV9j4pA","contactInfo":{"email":"bla@bla.ru","phoneNumber":null},"ip":"10.100.2.1","fingerprint":"test fingerprint"}}
' https://{host}:{port}/{path}
```
Мерчант, используя публичный ключ и имея в распоряжении тело запроса, подпись, алгоритм подписи и хэширования, может произвести проверку подписи
#### Политика переотправки сообщений
Если хук не отвечает или отвечает с ошибкой, пробуем 4 раза с интервалами 30сек, 5мин, 15мин, 1час опять послать
неотправленное сообщение в этот хук. При этом очередь сообщений для хука копится.
Если и четвертая попытка отправить сообщение оканчивается неудачей, хук помечается как выключеный и очередь сообщений для него сбрасывается.
Больше попыток отправить туда сообщения не предпринимается.
В текущей реализации хукера есть возможность устанавливать разные политики для разных хуков, но имплементирована только одна политика, которую легко изменить.
[1]: https://github.com/rbkmoney/damsel/blob/master/proto/webhooker.thrift
[2]: https://github.com/rbkmoney/swag-webhook-events/blob/master/spec/swagger.yaml

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>hooker</artifactId>
<version>1.1.0-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hooker</name>

View File

@ -30,7 +30,7 @@ public class PostSender {
RequestBody body = RequestBody.create(JSON, paramsAsString);
final Request request = new Request.Builder()
.url(url)
.addHeader(SIGNATURE_HEADER, signature)
.addHeader(SIGNATURE_HEADER, "alg=RS256; digest="+signature)
.post(body)
.build();

View File

@ -13,7 +13,7 @@ import java.util.Base64;
public class AsymSigner implements Signer {
public static final String KEY_ALGORITHM = "RSA";
public static final String HASH_ALGORITHM = "SHA256withRSA";
public static final int KEYSIZE = 1024;
public static final int KEYSIZE = 2048;
private KeyFactory keyFactory;
private Signature sig;
@ -44,7 +44,7 @@ public class AsymSigner implements Signer {
sig.update(data.getBytes());
signatureBytes = sig.sign();
}
return Base64.getEncoder().encodeToString(signatureBytes);
return Base64.getUrlEncoder().encodeToString(signatureBytes);
} catch (InvalidKeySpecException | InvalidKeyException | SignatureException e) {
throw new UnknownCryptoException(e);
}

View File

@ -20,9 +20,9 @@ public class AsymSignerTest {
public void test() throws Exception {
AsymSigner asymSigner = new AsymSigner();
KeyPair keyPair = asymSigner.generateKeys();
String data = "{\"invoice_id\":\"2Dbs4d4Dw\",\"amount\":120000,\"currency\":\"RUB\",\"created_at\":\"2011-07-01T09:00:00Z\",\"context\":{\"type\":null,\"data\":\"eyJvcmRlcl9pZCI6Im15X29yZGVyX2lkIn0=\",\"setType\":false,\"setData\":true},\"status\":\"PAID\"}";
String data = "{\"eventID\":27,\"occuredAt\":\"2017-05-16T13:49:34.935099Z\",\"topic\":\"InvoicesTopic\",\"eventType\":\"PaymentCaptured\",\"invoice\":{\"id\":\"qXMiygTqb2\",\"shopID\":1,\"createdAt\":\"2017-05-16T13:49:32.753723Z\",\"status\":\"unpaid\",\"reason\":null,\"dueDate\":\"2017-05-16T13:59:32Z\",\"amount\":100000,\"currency\":\"RUB\",\"metadata\":{\"type\":\"application/json\",\"data\":\"eyJpbnZvaWNlX2R1bW15X2NvbnRleHQiOiJ0ZXN0X3ZhbHVlIn0=\"},\"product\":\"test_product\",\"description\":\"test_invoice_description\"},\"payment\":{\"id\":\"1\",\"createdAt\":\"2017-05-16T13:49:33.182195Z\",\"status\":\"captured\",\"error\":null,\"amount\":100000,\"currency\":\"RUB\",\"paymentToolToken\":\"5Gz2nhE1eleFGBAcGe9SrA\",\"paymentSession\":\"2nTYVgk6h85O7vIVV9j4pA\",\"contactInfo\":{\"email\":\"bla@bla.ru\",\"phoneNumber\":null},\"ip\":\"10.100.2.1\",\"fingerprint\":\"test fingerprint\"}}";
String sign = asymSigner.sign(data, keyPair.getPrivKey());
byte[] sigBytes = Base64.getDecoder().decode(sign);
byte[] sigBytes = Base64.getUrlDecoder().decode(sign);
byte[] publicBytes = Base64.getDecoder().decode(keyPair.getPublKey());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);