Skip to main content

Message Signature

JSON Web Signature

Overview

In accordance with the evolution of the API, PayNet has implemented a new authentication mechanism utilizing JWS.

JWS stands for "JSON Web Signature". It is a compact, URL-safe means of representing digitally signed JSON (JavaScript Object Notation) data. It provides a way to ensure that a message has not been tampered with, and that it comes from a trusted source.

A JWS has three main components:

  • Header: It contains metadata about the JWS such as the algorithm used for signing and the type of token. The header is a JSON object that is Base64Url encoded.

  • Payload: This contains the actual data that is being transmitted in the JWS. It can be any valid JSON object, and it too is Base64Url encoded.

  • Signature: This is the result of applying the cryptographic signature algorithm to the encoded header and payload, using a secret key. The signature is also Base64Url encoded.

These three components are then concatenated with periods (.) to form the complete JWS. The resulting string can be transmitted over the wire and verified by the recipient to ensure its authenticity and integrity.

info

We are in the midst of updating all our APIs to support JWS. Please refer to the respective API product reference to find out which authentication method is being supported currently.

Generating JWS (API Request)

Here are the steps to generate a JWS token for an API request.

1. Generate the payload, minify and hashing using SHA-256

Provided that you have the following payload to transmit, it is imperative that you apply SHA-256 hashing algorithm.

Sample payload :

{
"data": {
"businessMessageId": "20230412BOEEMYK1000ORB00000001",
"clientMessage": "Client hello"
}
}

Sample payload after minification :

{"data":{"businessMessageId":"20230412BOEEMYK1000ORB00000001","clientMessage":"Client hello"}}

Sample generated hash SHA-256 :

8fc1f5ed05596aa2952e68ac221f31ee8a87641315c7b091f0bd41266d380739

GET special handling

info

For GET methods where no JSON request payload exists, users are expected to construct the generic body for JWS in the format shown below

{"data":{"businessMessageId":"<business_message_id_of_get_request>"}}

2. Generate JWS Header

Please find below a sample JWS header that the client is required to construct in a similar fashion.

{
"alg": "RS512",
"typ": "JWT",
"kid": "<Cert Serial Number>"
}
FieldDefinition
algThe algorithm for generating the signature in this context supports RS512
typThe type of content for the JWS payload in this context is set as default to JWT
kidThe Key ID (kid) in this context is a reference to the specific public key used to verify the JWS signature. Please put certificate serial number used to verify the signature

3. Generate JWS body

Please find below a sample JWS body that the client is required to construct in a similar fashion.

{
"iss": "BOEEMYK1",
"exp": 1681385787,
"jti": "20230412BOEEMYK1000ORB00000001",
"ds": "<Hash SHA-256 Value of Payload>"
}
FieldDefinition
issThe "iss" (issuer) claim identifies the principal that issued the JWT. Default to client BIC code.
expThe "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Default to 15 mins in epoch time
jtiDefault to businessMessageId
dssha-256 value of request payload

Example :

{
"iss": "BOEEMYK1",
"exp": 1681385787,
"jti": "20230412BOEEMYK1000ORB00000001",
"ds": "8fc1f5ed05596aa2952e68ac221f31ee8a87641315c7b091f0bd41266d380739"
}

4. Generate signature based on JWS constructed using private key

WAjEQpSsafFfT9WsH4QRTVrvEeU3wD6Ou67WzCF1d7UN2AIJvAASSb0zsAhdRxmvSbdus6UwZBPiSy0VszdpRon8A43olcTvyD4MO2OB9kSC6CYwL6by0rH3Y8U-2txGaO7lcN_ZgtBpus9p2QMO73ZgT3Bhg0t2w80L1NKhG7WHFWxy3h1q9aMAHH8AQLzXtVsHz_xWAw3Lemcuf6Q7gosaVCHm_xB1cTLretGlvXRniyk908afwOtoXJ3hfS5VIGc7-3EVil04UirN-kpQbXKfx35CN9mgsuAuydDCzj6HgEMn3k-mNLhof9ZZaDSziLimETXPN0Y6hnN5h0-54w

5. Generate JWS full token

base64UrlEncode(jws_header) + "." + base64UrlEncode(jws_payload) + "." + base64UrlEncode(jws_signature)

Example :

eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IlJQUF9QVksyMDIxIn0.eyJpc3MiOiJCT0VFTVlLMSIsImV4cCI6MTY4MTM4NTc4NywianRpIjoiMjAyMzA0MTJCT0VFTVlLMTAwME9SQjAwMDAwMDAxIiwiZHMiOiI4ZmMxZjVlZDA1NTk2YWEyOTUyZTY4YWMyMjFmMzFlZThhODc2NDEzMTVjN2IwOTFmMGJkNDEyNjZkMzgwNzM5In0=.WAjEQpSsafFfT9WsH4QRTVrvEeU3wD6Ou67WzCF1d7UN2AIJvAASSb0zsAhdRxmvSbdus6UwZBPiSy0VszdpRon8A43olcTvyD4MO2OB9kSC6CYwL6by0rH3Y8U-2txGaO7lcN_ZgtBpus9p2QMO73ZgT3Bhg0t2w80L1NKhG7WHFWxy3h1q9aMAHH8AQLzXtVsHz_xWAw3Lemcuf6Q7gosaVCHm_xB1cTLretGlvXRniyk908afwOtoXJ3hfS5VIGc7-3EVil04UirN-kpQbXKfx35CN9mgsuAuydDCzj6HgEMn3k-mNLhof9ZZaDSziLimETXPN0Y6hnN5h0-54w

6. Place JWS token into Authorization header during request

curl --location -g --request PUT 'https://api_domain' \
--header 'Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IlJQUF9QVksyMDIxIn0.eyJpc3MiOiJCT0VFTVlLMSIsImV4cCI6MTY4MTM4NTc4NywianRpIjoiMjAyMzA0MTJCT0VFTVlLMTAwME9SQjAwMDAwMDAxIiwiZHMiOiI4ZmMxZjVlZDA1NTk2YWEyOTUyZTY4YWMyMjFmMzFlZThhODc2NDEzMTVjN2IwOTFmMGJkNDEyNjZkMzgwNzM5In0=.WAjEQpSsafFfT9WsH4QRTVrvEeU3wD6Ou67WzCF1d7UN2AIJvAASSb0zsAhdRxmvSbdus6UwZBPiSy0VszdpRon8A43olcTvyD4MO2OB9kSC6CYwL6by0rH3Y8U-2txGaO7lcN_ZgtBpus9p2QMO73ZgT3Bhg0t2w80L1NKhG7WHFWxy3h1q9aMAHH8AQLzXtVsHz_xWAw3Lemcuf6Q7gosaVCHm_xB1cTLretGlvXRniyk908afwOtoXJ3hfS5VIGc7-3EVil04UirN-kpQbXKfx35CN9mgsuAuydDCzj6HgEMn3k-mNLhof9ZZaDSziLimETXPN0Y6hnN5h0-54w' \
--data-raw '{
<Sample Body>
}'

Verifying JWS (API Response)

Here are the steps to verify a JWS token for an API response.

1. Fetch JWS token from Authorization header

curl --location -g --request PUT 'https://api_domain' \
--header 'Authorization: Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IlJQUF9QVksyMDIxIn0.eyJpc3MiOiJCT0VFTVlLMSIsImV4cCI6MTY4MTM4NTc4NywianRpIjoiMjAyMzA0MTJCT0VFTVlLMTAwME9SQjAwMDAwMDAxIiwiZHMiOiI4ZmMxZjVlZDA1NTk2YWEyOTUyZTY4YWMyMjFmMzFlZThhODc2NDEzMTVjN2IwOTFmMGJkNDEyNjZkMzgwNzM5In0=.WAjEQpSsafFfT9WsH4QRTVrvEeU3wD6Ou67WzCF1d7UN2AIJvAASSb0zsAhdRxmvSbdus6UwZBPiSy0VszdpRon8A43olcTvyD4MO2OB9kSC6CYwL6by0rH3Y8U-2txGaO7lcN_ZgtBpus9p2QMO73ZgT3Bhg0t2w80L1NKhG7WHFWxy3h1q9aMAHH8AQLzXtVsHz_xWAw3Lemcuf6Q7gosaVCHm_xB1cTLretGlvXRniyk908afwOtoXJ3hfS5VIGc7-3EVil04UirN-kpQbXKfx35CN9mgsuAuydDCzj6HgEMn3k-mNLhof9ZZaDSziLimETXPN0Y6hnN5h0-54w' \
--data-raw '{
<Sample Body>
}'

2. Decode JWS header, and fetch alg and kid

{
"alg": "RS512",
"typ": "JWT",
"kid": "<Cert Serial Number>"
}

Extract the JWS header from the JWS token, which is the first part of the token separated by a period (".").The decoded JWS header will be a JSON object that contains information about the JWS token, including the algorithm used to generate the signature and the key identifier (kid) that represents the certificate serial number to identify which certificate needs to be used to verify the response.

3. Decode JWS payload, and verify the signature

Validate the JWS signature against the JWS payload by using the public key with the encryption algorithm, as specified in alg element.

4. Verify the JWS payload against the actual API payload

Sample payload :

{
"data": {
"businessMessageId": "20230412BOEEMYK1000ORB00000001",
"clientMessage": "Client hello"
}
}

Sample payload after minification :

{"data":{"businessMessageId":"20230412BOEEMYK1000ORB00000001","clientMessage":"Client hello"}}

Sample generated hash SHA-256 :

8fc1f5ed05596aa2952e68ac221f31ee8a87641315c7b091f0bd41266d380739

The client needs to compare the generated SHA-256 hash with those inside the JWS payload 'ds' tag.

Sample code

Dependencies and Libraries

info

Please ensure that your project includes the required libraries mentioned below before running the provided sample codes.

  ...
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
...

pip install pyOpenSSL python-jose


/**
* Required PHP 4 >= 4.0.4, PHP 5, PHP 7, PHP 8
*/


go get github.com/ake-persson/mapslice-json

Business Message Id generation

// Import class
import my.paynet.sdk.duitnow.utils.DuitNowUtils;
import my.paynet.sdk.duitnow.constant.ChannelCode;
import my.paynet.sdk.duitnow.constant.OriginatorCode;
import my.paynet.sdk.duitnow.constant.TransactionCode;

public class Transaction {

public void generateBusinessMessageId() {
TransactionCode transactionCode = TransactionCode.TRANSACTION_STATUS_INQUIRY;
OriginatorCode originator = OriginatorCode.RETAIL_PAYMENTS_PLATFORM;
ChannelCode channel = ChannelCode.RETAIL_INTERNET_BANKING;
// call sdk method to generate
String businessMessageId = DuitNowUtils.generateBusinessMessageId(bic, transactionCode, originator, channel);
...
}
}

JWS generation

// Import class
import my.paynet.sdk.duitnow.utils.DuitNowUtils;
import my.paynet.sdk.duitnow.constant.ChannelCode;
import my.paynet.sdk.duitnow.constant.OriginatorCode;
import my.paynet.sdk.duitnow.constant.TransactionCode;

public class Transaction {

public void generateJWS() {
TransactionCode transactionCode = TransactionCode.TRANSACTION_STATUS_INQUIRY;
OriginatorCode originator = OriginatorCode.RETAIL_PAYMENTS_PLATFORM;
ChannelCode channel = ChannelCode.RETAIL_INTERNET_BANKING;
// call sdk method to generate
String businessMessageId = DuitNowUtils.generateBusinessMessageId(bic, transactionCode, originator, channel);
...
String certSerialNumber = "12345"; // Replace with your certificate's serial number
Object payload = null; // Replace with your actual payload object
PaynetSigner signer = new DuitNowV2Signer();
String jws = signer.generateJws(privateKey, businessMessageId, certSerialNumber, payload);

...
}
}
from jose import jws
from jose.constants import ALGORITHMS
from OpenSSL import crypto
import hashlib
import json
import time

private_key_path = b'<path to private key file>'
private_key_content = open(private_key_path, 'rt').read()


headers = { 'kid': serial_number }
ds = json.dumps({'data': { 'businessMessageId': '<business message id>'}}, separators=(',', ':'))
payload = {'iss': '<bank or merchant id>', 'exp': int(time.time() + 900), 'jti': '<business message id>', 'ds': hashlib.sha256(ds.encode('utf-8')).hexdigest()}

# Signing payload
token = jws.sign(payload=payload, key=private_key_content, headers=headers, algorithm=ALGORITHMS.RS256)
print(token)

<?php

enum Algorithm: string {
case RS256 = 'RS256';
case RS512 = 'RS512';

public function signatureAlgorithm() {
return match($this) {
Algorithm::RS256 => 'sha256WithRSAEncryption',
Algorithm::RS512 => 'sha512WithRSAEncryption'
};
}
}

class Base64 {
public static function encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

public static function decode($data)
{
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
}

function loadPrivateKey($path) {
return openssl_get_privatekey(file_get_contents($path));
}

function generateJws($privateKey, $serialNumber, $algo, $issuer, $id, $data) {
$header = json_encode([
'alg' => $algo->value,
'typ' => 'JWT',
'kid' => $serialNumber
]);
echo time() + 900 . "\n";
$ds = json_encode($data);
$claims = json_encode([
'iss' => $issuer,
'exp' => time() + 900, // now + 15 minutes
'jti' => $id,
'ds' => hash('sha256', $ds)
]);
$base64Header = Base64::encode($header);
$base64Claims = Base64::encode($claims);
$data = $base64Header . "." . $base64Claims;
openssl_sign($data, $signature, $privateKey, $algo->signatureAlgorithm());
$base64Signature = Base64::encode($signature);
return $data . "." . $base64Signature;
}

$privateKeyPath = '<path to private key file>';
$serialNumber = '<certificate serial number>';
$privateKey = loadPrivateKey($privateKeyPath);

$algo = Algorithm::RS256;
$bic = '<bank or merchant id>';
$businessMessageId = '<business message id>';
$payloadData = [
'data' => [
'businessMessageId' => $businessMessageId
]
];

$token = generateJws($privateKey, $serialNumber, Algorithm::RS256, $bic, $businessMessageId, $payloadData);
echo $token . "\n";

package main

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
base64 "encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/ake-persson/mapslice-json"
)

type Algorithm string

const (
RS256 Algorithm = "RS256"
RS512 Algorithm = "RS512"
)

func (algo Algorithm) hash() crypto.Hash {
switch algo {
case RS512:
return crypto.SHA512
default:
return crypto.SHA256
}
}

func check(e error) {
if e != nil {
panic(e)
}
}

func generatePrivateKey(bits int) (*rsa.PrivateKey, error) {
key, err := rsa.GenerateKey(rand.Reader, bits)
return key, err
}

func loadPrivateKey(path string, key *rsa.PrivateKey) (*rsa.PrivateKey, error) {
dat, err := os.ReadFile(path)
if err != nil {
return key, err
}

block, _ := pem.Decode(dat)
if block.Type == "PRIVATE KEY" {
result, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return key, err
}
privateKey := result.(*rsa.PrivateKey)
return privateKey, nil
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}

func generateJws(key *rsa.PrivateKey, algo Algorithm, serialNumber string, issuer string, id string, data any) (string, error) {
headerDs, _ := json.Marshal(mapslice.MapSlice{
mapslice.MapItem{Key: "alg", Value: algo},
mapslice.MapItem{Key: "typ", Value: "JWT"},
mapslice.MapItem{Key: "kid", Value: serialNumber},
})
header := base64.RawURLEncoding.EncodeToString(headerDs)

dataDs, err := json.Marshal(data)
if err != nil {
return "", err
}
h := sha256.New()
h.Write(dataDs)
ds := h.Sum(nil)

claimsDs, err := json.Marshal(mapslice.MapSlice{
mapslice.MapItem{Key: "iss", Value: issuer},
mapslice.MapItem{Key: "exp", Value: time.Now().Local().Add(time.Minute + time.Duration(15)).Unix()},
mapslice.MapItem{Key: "jti", Value: id},
mapslice.MapItem{Key: "ds", Value: fmt.Sprintf("%x", ds)},
})
if err != nil {
return "", err
}
claims := base64.RawURLEncoding.EncodeToString(claimsDs)

payload := fmt.Sprintf("%s.%s", header, claims)

h = algo.hash().New()
h.Write([]byte(payload))
hashedPayload := h.Sum(nil)

signatureBytes, err := rsa.SignPKCS1v15(rand.Reader, key, algo.hash(), hashedPayload)
if err != nil {
return "", err
}
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)

token := fmt.Sprintf("%s.%s", payload, signature)

return token, nil
}

func main() {
privateKeyPath := "<path to private key file>"
serialNumber := "<certificate serial number>"

defaultPrivateKey, _ := generatePrivateKey(4096)
privateKey, err := loadPrivateKey(privateKeyPath, defaultPrivateKey)
check(err)

algo := RS256
bic := "<bank or merchant id>"
businessMessageId := "<business message id>"
data := mapslice.MapSlice{
mapslice.MapItem{Key: "data", Value: mapslice.MapSlice{
mapslice.MapItem{Key: "businessMessageId", Value: businessMessageId},
}},
}

token, err := generateJws(privateKey, algo, serialNumber, bic, businessMessageId, data)
check(err)
fmt.Println(token)
}

JWS verification

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
* @author Umair
*
*/

public class JWSGenerator {

private static final String KEY_ALGORITHM = "RSA";
private static final String CLASSPATH_COLON = "classpath:";
private static final String JWT = "JWT";

private PrivateKey privateKey;
private PublicKey publicKey;
private String certSerialNumber;

private static final String ALG = "alg";
private static final String TYP = "typ";
private static final String KID = "kid";
private static final String ISS = "iss";
private static final String EXP = "exp";
private static final String JTI = "jti";
private static final String DS = "ds";

public static void main(String[] args) {

// replace this with customer specific value
String certificatePath = "<path to certificate file>";
String jsonRequestPayload = "<sample JSON>";
String jwsToken = "<jws token from response>";

X509Certificate certificate = extractX509Certificate(certificatePath);
PublicKey publicKey = certificate.getPublicKey();

System.out.println(validate(jwsToken, jsonRequestPayload));
}

// load certificate for JWS verification
private static X509Certificate extractX509Certificate(String path) throws IOException, CertificateException {
if (path == null || path.isBlank()) {
return null;
}
InputStream inputStream = null;
try {
if (path.startsWith(CLASSPATH_COLON)) {
String fileName = path.replace(CLASSPATH_COLON, "");
inputStream = getClass().getClassLoader().getResourceAsStream(fileName);
} else {
inputStream = new FileInputStream(path);
}
CertificateFactory factory = CertificateFactory.getInstance(X_509);
return (X509Certificate) factory.generateCertificate(inputStream);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}

// minify JSON payload and hashed using SHA-256
private static String minifyDs(String payload) {
try {
ObjectMapper objectMapper = new ObjectMapper();
String minify = objectMapper.readValue(payload, JsonNode.class).toString();
System.out.printf("Minify Ds value...%s\n", minify);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(minify.getBytes(StandardCharsets.UTF_8));
return new String(Hex.encode(hash));
} catch (JsonProcessingException | NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}


// Verify JWS token
public static boolean validate(String token, String payload) {
try {
if (token == null || token.isEmpty()) {
return false;
}

if (token.startsWith("Bearer")) {
token = token.replace("Bearer ", "");
}
String minifyPayload = minifyDs(payload);

Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody();

return minifyPayload.equals(claims.get(DS));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

}



from jose import jws
from jose.constants import ALGORITHMS
from OpenSSL import crypto
import hashlib
import json
import time

certificate_path = b'<path to certificate file>'

certificate_content = open(certificate_path, 'rt').read()
certificate = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_content)
public_key = certificate.get_pubkey()
serial_number = format(certificate.get_serial_number(), 'x')


token = b'<jws token>'
ds = json.dumps({'data': { 'businessMessageId': '<business message id>'}}, separators=(',', ':'))
payload = {'iss': '<bank or merchant id>', 'exp': int(time.time() + 900), 'jti': '<business message id>', 'ds': hashlib.sha256(ds.encode('utf-8')).hexdigest()}

# Verifying signature
verify = jws.verify(token=token, key=certificate_content, algorithms=ALGORITHMS.RS256)
if(verify.decode('utf-8') == json.dumps(payload, separators=(',', ':'))):
print('valid')

<?php

enum Algorithm: string {
case RS256 = 'RS256';
case RS512 = 'RS512';

public function signatureAlgorithm() {
return match($this) {
Algorithm::RS256 => 'sha256WithRSAEncryption',
Algorithm::RS512 => 'sha512WithRSAEncryption'
};
}
}

class Base64 {
public static function encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

public static function decode($data)
{
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
}

function loadCertificate($path) {
$certificateContent = file_get_contents($path);
$publicKey = openssl_pkey_get_public($certificateContent);

$certificateData = openssl_x509_parse($certificateContent, true);
$serialNumber = strtolower($certificateData['serialNumberHex']);

return [$publicKey, $serialNumber];
}

function verifyJws($publicKey, $algo, $token, $data) {
$tokens = explode('.', $token);
$base64Header = $tokens[0];
$base64Claims = $tokens[1];

$payload = $base64Header . "." . $base64Claims;
$signature = Base64::decode($tokens[2]);
$claims = Base64::decode($base64Claims);

$verify = openssl_verify($payload, $signature, $publicKey, $algo->signatureAlgorithm());

$tokenClaims = json_decode($claims);
$now = time();
echo $now . "\n";
echo $tokenClaims->exp . "\n";
if ($now >= $tokenClaims->exp) {
return false;
}
$ds = json_encode($data);
$hashed = hash('sha256', $ds);
return $verify == 1 && $tokenClaims->ds == $hashed;
}

$cerificatePath = '<path to certificate file>'
list($publicKey, $serialNumber) = loadCertificate($certificatePath);

$algo = Algorithm::RS256;
$businessMessageId = '<business message id>'
$token = '<jws token>'
$payloadData = [
'data' => [
'businessMessageId' => $businessMessageId
]
];

$valid = verifyJws($publicKey, $algo, $token, $payloadData);
if ($valid) {
echo 'valid' . "\n";
} else {
echo 'invalid' . "\n";
}
package main

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
base64 "encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/ake-persson/mapslice-json"
)

type Algorithm string

const (
RS256 Algorithm = "RS256"
RS512 Algorithm = "RS512"
)

func (algo Algorithm) hash() crypto.Hash {
switch algo {
case RS512:
return crypto.SHA512
default:
return crypto.SHA256
}
}

func check(e error) {
if e != nil {
panic(e)
}
}

func generatePrivateKey(bits int) (*rsa.PrivateKey, error) {
key, err := rsa.GenerateKey(rand.Reader, bits)
return key, err
}

func loadCertificate(path string, key *rsa.PrivateKey) (*rsa.PublicKey, string, error) {
key, err := generatePrivateKey(4096)
dat, err := os.ReadFile(path)
if err != nil {
return &key.PublicKey, "", err
}
block, _ := pem.Decode(dat)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return &key.PublicKey, "", err
}
publicKey := cert.PublicKey.(*rsa.PublicKey)
serialNumber := fmt.Sprintf("%x", cert.SerialNumber)

return publicKey, serialNumber, nil
}

func verifyJws(publicKey *rsa.PublicKey, algo Algorithm, token string, data any) (bool, error) {
tokens := strings.Split(token, ".")
if len(tokens) != 3 {
return false, errors.New("jws: invalid token received, token must have 3 parts")
}
payload := fmt.Sprintf("%s.%s", tokens[0], tokens[1])
signatureBytes, err := base64.RawURLEncoding.DecodeString(tokens[2])
if err != nil {
return false, err
}
h := algo.hash().New()
h.Write([]byte(payload))

err = rsa.VerifyPKCS1v15(publicKey, algo.hash(), h.Sum(nil), signatureBytes)
if err != nil {
return false, err
}
claimsBytes, _ := base64.RawURLEncoding.DecodeString(tokens[1])
var tokenClaims mapslice.MapSlice
json.Unmarshal(claimsBytes, &tokenClaims)
var ds string
var expiry int64
for _, item := range tokenClaims {
if item.Key == "ds" {
ds = item.Value.(string)
}
if item.Key == "exp" {
expiry = int64(item.Value.(float64))
}
}
now := time.Now().Local().Unix()
if now > expiry {
return false, errors.New("jws: token validity is expired")
}
dataDs, _ := json.Marshal(data)
h = sha256.New()
h.Write(dataDs)
inputDsByte := h.Sum(nil)
inputDs := fmt.Sprintf("%x", inputDsByte)

return ds == inputDs, nil
}

func main() {
certificatePath := "<path to certificate key>"

defaultPrivateKey, _ := generatePrivateKey(4096)
publicKey, serialNumber, err := loadCertificate(certificatePath, defaultPrivateKey)
check(err)

algo := RS256
bic := "<bank or merchant id>"
businessMessageId := "<business message id>"
data := mapslice.MapSlice{
mapslice.MapItem{Key: "data", Value: mapslice.MapSlice{
mapslice.MapItem{Key: "businessMessageId", Value: businessMessageId},
}},
}

token := "<jws token>"

valid, err := verifyJws(publicKey, algo, token, data)
check(err)
fmt.Printf("JWS token validity: %t\n", valid)
}

How to Generate Key Pair

Using OpenSSL

Step 1: Generate private key and CSR (Certificate Signing Request)

openssl req \
-newkey rsa:2048 -nodes -keyout example.key \
-out example.csr

Step 2: Generate self-signed certificate from generated CSR

info

For production usage, this certificate must be created by valid Certificate Authority (CA). Self-signed certificate only valid for sandbox usage.

openssl x509 \
-signkey example.key \
-in example.csr \
-req -days 365 -out example.cer

Sample Codes

Message Signing

// Import class
import my.paynet.sdk.duitnow.utils.DuitNowUtils;
import my.paynet.sdk.duitnow.constant.ChannelCode;
import my.paynet.sdk.duitnow.constant.OriginatorCode;
import my.paynet.sdk.duitnow.constant.TransactionCode;

public class Transaction {

public void signMessage() {
...
TransactionCode transactionCode = TransactionCode.TRANSACTION_STATUS_INQUIRY;
OriginatorCode originator = OriginatorCode.RETAIL_PAYMENTS_PLATFORM;
ChannelCode channel = ChannelCode.RETAIL_INTERNET_BANKING;

// Replace your private key here
PrivateKey privateKey = getPrivateKey(privateKeyString);

String businessMessageId = DuitNowUtils.generateBusinessMessageId(bic, transactionCode, originator, channel);
String endToEndId = "20231220PICAMYK1010ORB29894000";
// Replace message to sign
String message = DuitNowUtils.getMessageToSign(transactionCode, businessMessageId, endToEndId);

PaynetSigner signer = new DuitNowV2Signer();
// SDK generate signature
String signature = signer.generateSignature(privateKey, message);
...
}
}
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from base64 import b64decode, b64encode

private_key = 'privateKey'
rpp_request_data = "requestData"

def sign():
key_bytes = bytes(private_key)
key_bytes = b64decode(key_bytes)
key = RSA.importKey(key_bytes)
hash_value = SHA256.new(bytes(rpp_request_data))
signer = PKCS1_v1_5.new(key)
signature = signer.sign(hash_value)
return b64encode(signature)

res = sign()
print (res)
//read/parse private key
$pvtKeyRes = openssl_pkey_get_private(file_get_contents('apigate.key'));
openssl_pkey_export($pvtKeyRes, $pvtKey);
echo $pvtKey . PHP_EOL;

//read/parse public key
$pubKeyRes = openssl_pkey_get_public(file_get_contents('apigate.pub'));
$dtlKey = openssl_pkey_get_details($pubKeyRes);
echo 'bits=[' . $dtlKey['bits'] . ']' . PHP_EOL;
$pubKey = $dtlKey['key'];
echo $pubKey . PHP_EOL;

$message = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
echo 'message := ' . $message . PHP_EOL;

//sign
$sigAlgo = 'sha256WithRSAEncryption';
$mdMethods = openssl_get_md_methods(true);
$found = false;

foreach ($mdMethods as $mdMethod) {
if ($sigAlgo === $mdMethod) {
$found = true;
break;
}
}

if ($found === false) {
throw new Exception("method=[$sigAlgo] not found");
}


// verify
$signatureBytes = base64_decode($signature);
$verify = openssl_verify($message, $signatureBytes, $pubKeyRes, $sigAlgo);
openssl_pkey_free($pubKeyRes);

if ($verify === -1) {
echo 'error while verifying' . PHP_EOL;
} elseif ($verify === 0) {
echo 'Wrong signature' . PHP_EOL;
} elseif ($verify === 1) {
echo 'Correct signature' . PHP_EOL;
} else {
throw new Exception('fail to verify message');
}
using System;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;

// namespace declaration
namespace SignApp{

// Class declaration
class Sign {

public static string SignData(byte[] data, string pkcs12File, string pkcs12Password)
{
X509Certificate2 signerCert = new X509Certificate2(pkcs12File, pkcs12Password, X509KeyStorageFlags.Exportable);
RSACryptoServiceProvider rsaCSP = new RSACryptoServiceProvider();
rsaCSP.FromXmlString(signerCert.PrivateKey.ToXmlString(true));
var SignedData = rsaCSP.SignData(data, CryptoConfig.MapNameToOID("SHA256"));
return Convert.ToBase64String(SignedData);
}

}
}

Message Verification

// Import class
import my.paynet.sdk.duitnow.utils.DuitNowUtils;
import my.paynet.sdk.duitnow.constant.TransactionCode;

public class Transaction {

public void messageVerification() {
// Replace your private key here
PrivateKey privateKey = getPrivateKey(privateKeyString);
String endToEndId = "20231220PICAMYK1010ORB29894000";
PaynetSigner signer = new DuitNowV2Signer();

// Replace your certificate here
X509Certificate certificate = ...
String signature = signer.generateSignBodyAndSignature(certificate, TransactionCode.TRANSACTION_STATUS_INQUIRY, businessMessageId, endToEndId);
// Message from response
String message = ...

boolean validate = signer.verify(getPublicCert(publicCertString),message,signature);
...
}
}
//read/parse private key
$pvtKeyRes = openssl_pkey_get_private(file_get_contents('apigate.key'));
openssl_pkey_export($pvtKeyRes, $pvtKey);
echo $pvtKey . PHP_EOL;

//read/parse public key
$pubKeyRes = openssl_pkey_get_public(file_get_contents('apigate.pub'));
$dtlKey = openssl_pkey_get_details($pubKeyRes);
echo 'bits=[' . $dtlKey['bits'] . ']' . PHP_EOL;
$pubKey = $dtlKey['key'];
echo $pubKey . PHP_EOL;

$message = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
echo 'message := ' . $message . PHP_EOL;

//sign
$sigAlgo = 'sha256WithRSAEncryption';
$mdMethods = openssl_get_md_methods(true);
$found = false;

foreach ($mdMethods as $mdMethod) {
if ($sigAlgo === $mdMethod) {
$found = true;
break;
}
}

if ($found === false) {
throw new Exception("method=[$sigAlgo] not found");
}


// verify
$signatureBytes = base64_decode($signature);
$verify = openssl_verify($message, $signatureBytes, $pubKeyRes, $sigAlgo);
openssl_pkey_free($pubKeyRes);

if ($verify === -1) {
echo 'error while verifying' . PHP_EOL;
} elseif ($verify === 0) {
echo 'Wrong signature' . PHP_EOL;
} elseif ($verify === 1) {
echo 'Correct signature' . PHP_EOL;
} else {
throw new Exception('fail to verify message');
}
using System;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;

// namespace declaration
namespace SignApp{

// Class declaration
class Sign {


public static bool VerifySignature(byte[] data, string signature, string publicCert)
{
X509Certificate2 partnerCert = new X509Certificate2(publicCert);
RSACryptoServiceProvider rsaCSP = (RSACryptoServiceProvider)partnerCert.PublicKey.Key;
return rsaCSP.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), Convert.FromBase64String(signature));
}
}
}