Verifying the signature of a notification
Generate API Certificate
https://merchant.alikassa.com/cabinet/form/setting-api-certs
Generate an "API certificate for notifications", save the archive, and extract the public.pem
file.
Signature Verification
Wrap all GET parameters into a JSON (in this exact order) and verify the signature:
$verif = openssl_verify(json_encode([
'type' => $_GET['type'],
'id' => (int) $_GET['id'],
'order_id' => $_GET['order_id'],
'payment_status' => $_GET['payment_status'],
'amount' => $_GET['amount'],
'payment_amount' => $_GET['payment_amount'],
'commission_amount' => $_GET['commission_amount'],
'is_partial_payment' => $_GET['is_partial_payment'],
'account' => $_GET['account'],
'service' => $_GET['service'],
'desc' => $_GET['desc'],
]),
base64_decode($_GET['sign']),
file_get_contents('./certs/notification/public.pem'));
if (!$verif) {
throw new \Exception('Invalid signature');
}
import http from 'http';
import { createVerify } from 'crypto';
import { readFileSync } from 'fs';
http
.createServer((req, res) => {
const url = new URL(req.url || '', `http://${req.headers.host}`);
const params = {
type: url.searchParams.get('type'),
id: Number(url.searchParams.get('id')),
order_id: url.searchParams.get('order_id'),
payment_status: url.searchParams.get('payment_status'),
amount: url.searchParams.get('amount'),
payment_amount: url.searchParams.get('payment_amount'),
commission_amount: url.searchParams.get('commission_amount'),
is_partial_payment: url.searchParams.get('is_partial_payment'),
account: url.searchParams.get('account'),
service: url.searchParams.get('service'),
desc: url.searchParams.get('desc'),
};
const data = JSON.stringify(params);
const signature = Buffer.from(url.searchParams.get('sign') || '', 'base64');
const publicKey = readFileSync('./certs/notification/public.pem');
const verifier = createVerify('SHA256');
verifier.update(data);
verifier.end();
const verified = verifier.verify(publicKey, signature);
if (!verified) {
throw new Error('Invalid signature');
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
})
.listen(8080);
import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from flask import Flask, request
from cryptography.exceptions import InvalidSignature
app = Flask(__name__)
@app.route('/notification', methods=['GET'])
def verify_notification():
params = {
'type': request.args.get('type'),
'id': int(request.args.get('id', 0)),
'order_id': request.args.get('order_id'),
'payment_status': request.args.get('payment_status'),
'amount': request.args.get('amount'),
'payment_amount': request.args.get('payment_amount'),
'commission_amount': request.args.get('commission_amount'),
'is_partial_payment': request.args.get('is_partial_payment'),
'account': request.args.get('account'),
'service': request.args.get('service'),
'desc': request.args.get('desc'),
}
data = json.dumps(params).encode('utf-8')
signature = base64.b64decode(request.args.get('sign', ''))
with open('./certs/notification/public.pem', 'rb') as f:
public_key = load_pem_public_key(f.read())
try:
public_key.verify(
signature,
data,
padding.PKCS1v15(),
hashes.SHA256()
)
return 'OK', 200
except InvalidSignature:
raise Exception('Invalid signature')
if __name__ == '__main__':
app.run(port=8080)
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class SignatureVerifier {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/notification", new NotificationHandler());
server.setExecutor(null);
server.start();
}
static class NotificationHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
String query = exchange.getRequestURI().getQuery();
Map<String, String> params = parseQuery(query);
Map<String, Object> data = new HashMap<>();
data.put("type", params.get("type"));
data.put("id", Integer.parseInt(params.get("id")));
data.put("order_id", params.get("order_id"));
data.put("payment_status", params.get("payment_status"));
data.put("amount", params.get("amount"));
data.put("payment_amount", params.get("payment_amount"));
data.put("commission_amount", params.get("commission_amount"));
data.put("is_partial_payment", params.get("is_partial_payment"));
data.put("account", params.get("account"));
data.put("service", params.get("service"));
data.put("desc", params.get("desc"));
String jsonData = new Gson().toJson(data);
byte[] signature = Base64.getDecoder().decode(params.get("sign"));
// Load public key
byte[] keyBytes = Files.readAllBytes(Paths.get("./certs/notification/public.pem"));
String publicKeyPEM = new String(keyBytes)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(spec);
// Verify signature
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(jsonData.getBytes());
if (!sig.verify(signature)) {
throw new RuntimeException("Invalid signature");
}
String response = "OK";
exchange.sendResponseHeaders(200, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
} catch (Exception e) {
String response = "Error: " + e.getMessage();
exchange.sendResponseHeaders(400, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
private Map<String, String> parseQuery(String query) throws IOException {
Map<String, String> result = new HashMap<>();
if (query != null) {
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2) {
result.put(
URLDecoder.decode(pair[0], "UTF-8"),
URLDecoder.decode(pair[1], "UTF-8")
);
}
}
}
return result;
}
}
}
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
)
type NotificationParams struct {
Type string `json:"type"`
ID int `json:"id"`
OrderID string `json:"order_id"`
PaymentStatus string `json:"payment_status"`
Amount string `json:"amount"`
PaymentAmount string `json:"payment_amount"`
CommissionAmount string `json:"commission_amount"`
IsPartialPayment string `json:"is_partial_payment"`
Account string `json:"account"`
Service string `json:"service"`
Desc string `json:"desc"`
}
func verifySignature(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
id, _ := strconv.Atoi(query.Get("id"))
params := NotificationParams{
Type: query.Get("type"),
ID: id,
OrderID: query.Get("order_id"),
PaymentStatus: query.Get("payment_status"),
Amount: query.Get("amount"),
PaymentAmount: query.Get("payment_amount"),
CommissionAmount: query.Get("commission_amount"),
IsPartialPayment: query.Get("is_partial_payment"),
Account: query.Get("account"),
Service: query.Get("service"),
Desc: query.Get("desc"),
}
jsonData, err := json.Marshal(params)
if err != nil {
http.Error(w, "Error marshaling JSON", http.StatusBadRequest)
return
}
signature, err := base64.StdEncoding.DecodeString(query.Get("sign"))
if err != nil {
http.Error(w, "Error decoding signature", http.StatusBadRequest)
return
}
// Load public key
publicKeyData, err := ioutil.ReadFile("./certs/notification/public.pem")
if err != nil {
http.Error(w, "Error reading public key", http.StatusInternalServerError)
return
}
block, _ := pem.Decode(publicKeyData)
if block == nil {
http.Error(w, "Error decoding PEM block", http.StatusInternalServerError)
return
}
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
http.Error(w, "Error parsing public key", http.StatusInternalServerError)
return
}
rsaPublicKey, ok := publicKey.(*rsa.PublicKey)
if !ok {
http.Error(w, "Not an RSA public key", http.StatusInternalServerError)
return
}
// Verify signature
hash := sha256.Sum256(jsonData)
err = rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hash[:], signature)
if err != nil {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
func main() {
http.HandleFunc("/notification", verifySignature)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Parameters Description
Name | Type | Description |
---|---|---|
type | string | payment or payout |
id | int | AliKassa ID |
order_id | string | Your ID |
payment_status | string | Payment status • wait — in the process of payment• paid — successfully paid (final status)• cancel — canceled (final status)• fail — error (final status) |
amount | string | Amount |
payment_amount | string | Payment amount |
commission_amount | string | Commission amount |
is_partial_payment | bool | Is the payment partial? |
account | string | Account |
service | string | Service (Account, Methods) |
desc | string | Description |
sign | string | Request signature |