SNAP Asymmetric Signature
Signature Generation and Validation
Asymetric Signature is used to ensure data integrity and authenticity of the request or response for each API. The signature needs to be provided using a private key. Netzme's system supports the use of PKCS#8 for the private key. The generated signature will be verified using a public key. The algorithm used by NETZME is SHA256withRSA and the format type is Base64.
Asymmetric Key Generation
Prepare the data to do signature, the data are:
NoDataRemarksExample1
HTTP METHOD
Method on each API, for instance
GET
,POST
,PUT
,PATCH
, andDELETE
POST
2
RELATIVE PATH URL
URL on each API
/v1.0/balance-inquiry.htm
3
HTTP BODY
Minify request body and hash the request body with SHA-256, refer to step no ii and iii for the detail
e9295c3253c05560273ff305d9eea6abf7
7fff65229bf90b1781383c09c29d98
4
X-TIMESTAMP
Transaction date time, in format YYYY-MM-DDTHH:mm:ss+07:00. Time must be in GMT+7 (Jakarta time)
2022-11-30T09:45:35+07:00
Minify the request body
Refer to the tools section number 1 for the example process. The following are the example value before and after minify the request body:
(<HTTP BODY>) before minify{ "partnerReferenceNo":"2020102900000000000001", "balanceTypes":["BALANCE"], "additionalInfo":{ "accessToken" : "fa8sjjEj813Y9JGoqwOeOPWbnt4CUpvIJbU1mMU4a11MNDZ7Sg5u9a" } }
(<HTTP BODY>) after minify{"partnerReferenceNo":"2020102900000000000001","balanceTypes":["BALANCE"],"additionalInfo":{"accessToken":"fa8sjjEj813Y9JGoqwOeOPWbnt4CUpvIJbU1mMU4a11MNDZ7Sg5u9a"}}
Lowercase(HexEncode(SHA-256(RequestBody)))
Refer to the tools section number 3 for the example process. The following is the example value:
e9295c3253c05560273ff305d9eea6abf77fff65229bf90b1781383c09c29d98
Compose the string to sign
<HTTP METHOD> + ”:” + <RELATIVE PATH URL> + “:“ + LowerCase(HexEncode(SHA-256(Minify(<HTTP BODY>)))) + “:“ + <X-TIMESTAMP>
The following is the example:
POST:/v1.0/balance-inquiry.htm:e9295c3253c05560273ff305d9eea6abf77fff65229bf90b1781383c09c29d98:2022-11-30T09:45:35+07:00
The signature string is generated from string to sign above with applying SHA-256 with RSA-2048 encryption using PKCS#8 or PKCS#1 private key, and then encode the result to base64.
The following is the example value:
iSkd8HPpdeeQSnq5lSRM46l8w/C4ZhNq7ordOv2dfDC0A0rGWxqz+9j864gcuVhu0tgTHJUuV9k5wsluig/sJ4W5Yy1EZPzbpeeUwFxSK0WgnW5LLq/h5RQAgVEyJL5MI1KrByzBQIv+5IYgKaLFmTLeo4xy7ToLJKND/6Ja+HRuo+SpnzNA2NNJEcc+PI87pAo0yXItZhjUXhyz9rkv0P8Ra8tDar2asHVGxA5BiGthy/eyPbe9VYavfMrOKAZpISw9VVoZ1axHgqvLCVEPodIx45nWUqF96PUyIB2H51VZCTPaxeefpdKzgPR0Ji24zIeFhaowk7i2znPnNDINvA==
Put the signature string into HTTP header “ X-SIGNATURE“.
The following is the example value:
X-SIGNATURE: iSkd8HPpdeeQSnq5lSRM46l8w/C4ZhNq7ordOv2dfDC0A0rGWxqz+9j864gcuVhu0tgTHJUuV9k5wsluig/sJ4W5Yy1EZPzbpeeUwFxSK0WgnW5LLq/h5RQAgVEyJL5MI1KrByzBQIv+5IYgKaLFmTLeo4xy7ToLJKND/6Ja+HRuo+SpnzNA2NNJEcc+PI87pAo0yXItZhjUXhyz9rkv0P8Ra8tDar2asHVGxA5BiGthy/eyPbe9VYavfMrOKAZpISw9VVoZ1axHgqvLCVEPodIx45nWUqF96PUyIB2H51VZCTPaxeefpdKzgPR0Ji24zIeFhaowk7i2znPnNDINvA==
Digital Signature Validation
The following steps are used to explain how to validate the digital signature used by the receiver of APIs:
Take the signature from HTTP header “ X-SIGNATURE“ from the sender of APIs.
The following is the example:
X-SIGNATURE: iSkd8HPpdeeQSnq5lSRM46l8w/C4ZhNq7ordOv2dfDC0A0rGWxqz+9j864gcuVhu0tgTHJUuV9k5wsluig/sJ4W5Yy1EZPzbpeeUwFxSK0WgnW5LLq/h5RQAgVEyJL5MI1KrByzBQIv+5IYgKaLFmTLeo4xy7ToLJKND/6Ja+HRuo+SpnzNA2NNJEcc+PI87pAo0yXItZhjUXhyz9rkv0P8Ra8tDar2asHVGxA5BiGthy/eyPbe9VYavfMrOKAZpISw9VVoZ1axHgqvLCVEPodIx45nWUqF96PUyIB2H51VZCTPaxeefpdKzgPR0Ji24zIeFhaowk7i2znPnNDINvA==
Compose the string to verify
<HTTP METHOD> + ”:” + <RELATIVE PATH URL> + “:“ + LowerCase(HexEncode(SHA-256(Minify(<HTTP BODY>)))) + “:“ + <X-TIMESTAMP>
The following is the example:
POST:/v1.0/balance-inquiry.htm:e9295c3253c05560273ff305d9eea6abf77fff65229bf90b1781383c09c29d98:2022-11-30T09:45:35+07:00
Verify the correctness of the signature based on SHA-256 with RSA-2048 encryption signing against the string to sign with provided public key of sender of APIs. And don't forget to decode public key and signature before used it.
If the verification is correct, then consume the request.
I will show you how to validate signature in several way : A. Go
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
)
// verifySignature verifies an asymmetric signature
func verifySignature(signature, payload, httpMethod, url, timestamp, publicKey string) bool {
// Create string to sign
stringToSign := createSignToKey(payload, url, httpMethod, timestamp)
fmt.Printf("String to sign: %s\n", stringToSign)
// Verify signature
isVerified, err := verify(signature, publicKey, stringToSign)
if err != nil {
fmt.Printf("Error verifying signature: %v\n", err)
return false
}
return isVerified
}
// createSignToKey creates the content that was originally signed
// Implements the logic from the provided Kotlin function
func createSignToKey(payload, url, httpMethod, timestamp string) string {
minifiedPayload := minifyJsonString(payload)
payloadHash := digestHexSHA256(minifiedPayload)
payloadHashLower := strings.ToLower(payloadHash)
return httpMethod + ":" + url + ":" + payloadHashLower + ":" + timestamp
}
// minifyJsonString removes unnecessary whitespace from JSON string
func minifyJsonString(jsonStr string) string {
var jsonObj interface{}
err := json.Unmarshal([]byte(jsonStr), &jsonObj)
if err != nil {
// If there's an error parsing, return original string
return jsonStr
}
// Re-marshal to get minified version
minified, err := json.Marshal(jsonObj)
if err != nil {
return jsonStr
}
return string(minified)
}
// digestHexSHA256 computes SHA-256 hash and returns hex string
func digestHexSHA256(input string) string {
hasher := sha256.New()
hasher.Write([]byte(input))
hashBytes := hasher.Sum(nil)
return hex.EncodeToString(hashBytes)
}
// verify checks if the signature is valid
func verify(signature, publicKey, content string) (bool, error) {
if signature == "" {
return false, nil
}
contentBytes := []byte(content)
// Decode base64 signature
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false, fmt.Errorf("failed to decode signature: %w", err)
}
// Decode base64 public key
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return false, fmt.Errorf("failed to decode public key: %w", err)
}
// Verify using RSA
return verifyRSA(contentBytes, signatureBytes, publicKeyBytes)
}
// verifyRSA performs RSA signature verification
func verifyRSA(data, signature, publicKeyBytes []byte) (bool, error) {
// Transform public key bytes to rsa.PublicKey
publicKey, err := parsePublicKey(publicKeyBytes)
if err != nil {
return false, err
}
// Hash the data using SHA-256
hashed := sha256.Sum256(data)
// Verify the signature
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signature)
if err != nil {
return false, err
}
return true, nil
}
// parsePublicKey converts raw public key bytes to an RSA public key
func parsePublicKey(data []byte) (*rsa.PublicKey, error) {
pubKey, err := x509.ParsePKIXPublicKey(data)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
rsaPublicKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an RSA public key")
}
return rsaPublicKey, nil
}
func main() {
// Example usage
signature := "YOUR_BASE64_SIGNATURE"
payload := "{\"transactionId\":\"123\",\"amount\":\"100.00\"}"
httpMethod := "POST"
url := "/api/v2/payments/notification"
timestamp := "1714465371"
publicKey := "YOUR_BASE64_PUBLIC_KEY"
// Verify signature
isValid := verifySignature(signature, payload, httpMethod, url, timestamp, publicKey)
if isValid {
fmt.Println("Signature is valid!")
} else {
fmt.Println("Signature verification failed!")
}
}
B. PHP
<?php
/**
* Verify asymmetric signature
*
* @param string $signature Base64 encoded signature
* @param string $payload JSON payload
* @param string $httpMethod HTTP method (e.g., "POST")
* @param string $url Endpoint URL
* @param string $timestamp Request timestamp
* @param string $publicKey Base64 encoded public key
* @return bool True if signature is valid, false otherwise
*/
function verifySignature($signature, $payload, $httpMethod, $url, $timestamp, $publicKey) {
try {
// Create string to sign
$stringToSign = createSignToKey($payload, $url, $httpMethod, $timestamp);
echo "String to sign: " . $stringToSign . PHP_EOL;
// Verify signature
$isVerified = verify($signature, $publicKey, $stringToSign);
if (!$isVerified) {
echo "Failed to verify signature: " . $signature . PHP_EOL;
return false;
}
return true;
} catch (Exception $e) {
echo "Error verifying signature: " . $e->getMessage() . PHP_EOL;
return false;
}
}
/**
* Create the string to be signed
*
* @param string $payload JSON payload
* @param string $url Endpoint URL
* @param string $httpMethod HTTP method
* @param string $timestamp Request timestamp
* @return string String to be signed
*/
function createSignToKey($payload, $url, $httpMethod, $timestamp) {
$minifiedPayload = minifyJsonString($payload);
$payloadHash = digestHexSHA256($minifiedPayload);
$payloadHashLower = strtolower($payloadHash);
return $httpMethod . ":" . $url . ":" . $payloadHashLower . ":" . $timestamp;
}
/**
* Minify JSON string
*
* @param string $jsonStr JSON string
* @return string Minified JSON string
*/
function minifyJsonString($jsonStr) {
$jsonObj = json_decode($jsonStr);
if (json_last_error() !== JSON_ERROR_NONE) {
// If there's an error parsing, return original string
return $jsonStr;
}
// Re-encode to get minified version
return json_encode($jsonObj, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
/**
* Compute SHA-256 hash and return as hex string
*
* @param string $input Input string
* @return string Hex encoded SHA-256 hash
*/
function digestHexSHA256($input) {
return hash('sha256', $input);
}
/**
* Verify signature with public key
*
* @param string $signature Base64 encoded signature
* @param string $publicKey Base64 encoded public key
* @param string $content Content that was signed
* @return bool True if signature is valid
*/
function verify($signature, $publicKey, $content) {
if (empty($signature)) {
return false;
}
// Decode base64 signature and public key
$signatureBytes = base64_decode($signature);
$publicKeyBytes = base64_decode($publicKey);
if ($signatureBytes === false || $publicKeyBytes === false) {
throw new Exception("Failed to decode base64 signature or public key");
}
// Create public key resource
$publicKeyResource = openssl_pkey_get_public($publicKeyBytes);
if ($publicKeyResource === false) {
throw new Exception("Failed to parse public key: " . openssl_error_string());
}
// Verify signature
$result = openssl_verify($content, $signatureBytes, $publicKeyResource, OPENSSL_ALGO_SHA256);
// Free the key resource
openssl_free_key($publicKeyResource);
if ($result === 1) {
return true;
} else if ($result === 0) {
return false;
} else {
throw new Exception("Error during signature verification: " . openssl_error_string());
}
}
// Example usage
function main() {
$signature = "YOUR_BASE64_SIGNATURE";
$payload = '{"transactionId":"123","amount":"100.00"}';
$httpMethod = "POST";
$url = "/api/v2/payments/notification";
$timestamp = "1714465371";
$publicKey = "YOUR_BASE64_PUBLIC_KEY";
$isValid = verifySignature($signature, $payload, $httpMethod, $url, $timestamp, $publicKey);
if ($isValid) {
echo "Signature is valid!" . PHP_EOL;
} else {
echo "Signature verification failed!" . PHP_EOL;
}
}
// Run the example
main();
?>
C. Java
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SignatureVerifier {
// verifySignature verifies an asymmetric signature
public static boolean verifySignature(String signature, String payload, String httpMethod,
String url, String timestamp, String publicKey) {
// Create string to sign
String stringToSign = createSignToKey(payload, url, httpMethod, timestamp);
System.out.printf("String to sign: %s\n", stringToSign);
// Verify signature
try {
return verify(signature, publicKey, stringToSign);
} catch (Exception e) {
System.out.printf("Error verifying signature: %s\n", e.getMessage());
return false;
}
}
// createSignToKey creates the content that was originally signed
private static String createSignToKey(String payload, String url, String httpMethod, String timestamp) {
String minifiedPayload = minifyJsonString(payload);
String payloadHash = digestHexSHA256(minifiedPayload);
String payloadHashLower = payloadHash.toLowerCase();
return httpMethod + ":" + url + ":" + payloadHashLower + ":" + timestamp;
}
// minifyJsonString removes unnecessary whitespace from JSON string
private static String minifyJsonString(String jsonStr) {
try {
ObjectMapper mapper = new ObjectMapper();
Object jsonObj = mapper.readValue(jsonStr, Object.class);
return mapper.writeValueAsString(jsonObj);
} catch (Exception e) {
// If there's an error parsing, return original string
return jsonStr;
}
}
// digestHexSHA256 computes SHA-256 hash and returns hex string
private static String digestHexSHA256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hashBytes);
} catch (Exception e) {
throw new RuntimeException("Failed to compute SHA-256 hash", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
// verify checks if the signature is valid
private static boolean verify(String signature, String publicKey, String content) throws Exception {
if (signature == null || signature.isEmpty()) {
return false;
}
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
// Decode base64 signature
byte[] signatureBytes = Base64.getDecoder().decode(signature);
// Decode base64 public key
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey);
// Verify using RSA
return verifyRSA(contentBytes, signatureBytes, publicKeyBytes);
}
// verifyRSA performs RSA signature verification
private static boolean verifyRSA(byte[] data, byte[] signature, byte[] publicKeyBytes) throws Exception {
// Transform public key bytes to PublicKey
PublicKey publicKey = parsePublicKey(publicKeyBytes);
// Create Signature instance for RSA with SHA-256
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data);
// Verify the signature
return sig.verify(signature);
}
// parsePublicKey converts raw public key bytes to a PublicKey
private static PublicKey parsePublicKey(byte[] data) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
public static void main(String[] args) {
// Example usage
String signature = "YOUR_BASE64_SIGNATURE";
String payload = "{\"transactionId\":\"123\",\"amount\":\"100.00\"}";
String httpMethod = "POST";
String url = "/api/v2/payments/notification";
String timestamp = "1714465371";
String publicKey = "YOUR_BASE64_PUBLIC_KEY";
// Verify signature
boolean isValid = verifySignature(signature, payload, httpMethod, url, timestamp, publicKey);
if (isValid) {
System.out.println("Signature is valid!");
} else {
System.out.println("Signature verification failed!");
}
}
}
D. Kotlin
import com.fasterxml.jackson.databind.ObjectMapper
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PublicKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
class SignatureVerifier {
companion object {
/**
* Verifies an asymmetric signature
*/
fun verifySignature(
signature: String,
payload: String,
httpMethod: String,
url: String,
timestamp: String,
publicKey: String
): Boolean {
// Create string to sign
val stringToSign = createSignToKey(payload, url, httpMethod, timestamp)
println("String to sign: $stringToSign")
// Verify signature
return try {
verify(signature, publicKey, stringToSign)
} catch (e: Exception) {
println("Error verifying signature: ${e.message}")
false
}
}
/**
* Creates the content that was originally signed
*/
private fun createSignToKey(payload: String, url: String, httpMethod: String, timestamp: String): String {
val minifiedPayload = minifyJsonString(payload)
val payloadHash = digestHexSHA256(minifiedPayload)
val payloadHashLower = payloadHash.lowercase()
return "$httpMethod:$url:$payloadHashLower:$timestamp"
}
/**
* Removes unnecessary whitespace from JSON string
*/
private fun minifyJsonString(jsonStr: String): String {
return try {
val mapper = ObjectMapper()
val jsonObj = mapper.readValue(jsonStr, Any::class.java)
mapper.writeValueAsString(jsonObj)
} catch (e: Exception) {
// If there's an error parsing, return original string
jsonStr
}
}
/**
* Computes SHA-256 hash and returns hex string
*/
private fun digestHexSHA256(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(input.toByteArray(StandardCharsets.UTF_8))
return bytesToHex(hashBytes)
}
private fun bytesToHex(bytes: ByteArray): String {
return bytes.joinToString("") { byte -> "%02x".format(byte.toInt() and 0xFF) }
}
/**
* Checks if the signature is valid
*/
private fun verify(signature: String, publicKey: String, content: String): Boolean {
if (signature.isEmpty()) {
return false
}
val contentBytes = content.toByteArray(StandardCharsets.UTF_8)
// Decode base64 signature
val signatureBytes = Base64.getDecoder().decode(signature)
// Decode base64 public key
val publicKeyBytes = Base64.getDecoder().decode(publicKey)
// Verify using RSA
return verifyRSA(contentBytes, signatureBytes, publicKeyBytes)
}
/**
* Performs RSA signature verification
*/
private fun verifyRSA(data: ByteArray, signature: ByteArray, publicKeyBytes: ByteArray): Boolean {
// Transform public key bytes to PublicKey
val publicKey = parsePublicKey(publicKeyBytes)
// Create Signature instance for RSA with SHA-256
val sig = Signature.getInstance("SHA256withRSA")
sig.initVerify(publicKey)
sig.update(data)
// Verify the signature
return sig.verify(signature)
}
/**
* Converts raw public key bytes to a PublicKey
*/
private fun parsePublicKey(data: ByteArray): PublicKey {
val spec = X509EncodedKeySpec(data)
val kf = KeyFactory.getInstance("RSA")
return kf.generatePublic(spec)
}
}
}
fun main() {
// Example usage
val signature = "YOUR_BASE64_SIGNATURE"
val payload = """{"transactionId":"123","amount":"100.00"}"""
val httpMethod = "POST"
val url = "/api/v2/payments/notification"
val timestamp = "1714465371"
val publicKey = "YOUR_BASE64_PUBLIC_KEY"
// Verify signature
val isValid = SignatureVerifier.verifySignature(signature, payload, httpMethod, url, timestamp, publicKey)
if (isValid) {
println("Signature is valid!")
} else {
println("Signature verification failed!")
}
}
Last updated