# EDC Overview
# Digital Signature Validation
- Apply Token B2B Signature
- Compose the string to sign:
<X-CLIENT-KEY> + "|" + <X-TIMESTAMP> - Generate RSA-2048 public and private keys, and use the private key to sign, and fill the public key into the merchant background.
- Take the signature from HTTP header "X-SIGNATURE".
- Compare the value between X-SIGNATURE and the generated signature, if those value are the same, then consume the message.
- Transaction Signature
- Compose the string to sign:
<HTTP METHOD> + ":" + <RELATIVE PATH URL> + ":" + <B2B ACCESS TOKEN> + ":" + LowerCase(HexEncode(SHA-256(Minify(<HTTP BODY>)))) + ":" + <X-TIMESTAMP>
Note: <HTTP BODY> must be sorted in ASCII order and converted to a json string. - Generate RSA-2048 public and private keys, and use the private key to sign, and fill the public key into the merchant background.
- Take the signature from HTTP header "X-SIGNATURE".
- Compare the value between X-SIGNATURE and the generated signature, if those value are the same, then consume the message.
# Appendix: RSA Usage Example (Java Language)
- Generate RSA key pair:
import java.security.*;
import java.util.Base64;
public class RsaGenerateKeyPair {
public static void main(String[] args) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PublicKey publicKey = keyPair.getPublic();
byte[] publicKeyBytes = publicKey.getEncoded();
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes);
System.out.println(publicKeyBase64);
PrivateKey privateKey = keyPair.getPrivate();
byte[] privateKeyBytes = privateKey.getEncoded();
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
System.out.println(privateKeyBase64);
}
}
RSA Public Key:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAna4Dyz8nGJiAlc9jTGyRa+TtlZXYABTc+Xfb3T4NdDbnUO8vtNLHugwmqARp8kzEzsMRbmvKro4EpaXqANn7SAGo+YI6sVUDmX7ESk3P6j51PtTvWR6dikJN6qwtmV64ojEbxDnIBL3VKuctefL8uPcI7MZBUPBXg9l8CZmnn2cKqWjZ8MuEQr4G45IqmJ0tRsRmW9ofNnvI1MLPt7c/Z/D1E6HKVwjPcMZKMuF0HpIDqdQaPX83dlSzv9FF9jFR8HWfWW8Oz3jz+GtSLSdh2ERcyO56WHpWl1POV4o9jF+4R/oBgcH+0zA1Z2aFfQf/n9miMhacrioStBaHkh1f/QIDAQAB
RSA Private Key:
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdrgPLPycYmICVz2NMbJFr5O2VldgAFNz5d9vdPg10NudQ7y+00se6DCaoBGnyTMTOwxFua8qujgSlpeoA2ftIAaj5gjqxVQOZfsRKTc/qPnU+1O9ZHp2KQk3qrC2ZXriiMRvEOcgEvdUq5y158vy49wjsxkFQ8FeD2XwJmaefZwqpaNnwy4RCvgbjkiqYnS1GxGZb2h82e8jUws+3tz9n8PUTocpXCM9wxkoy4XQekgOp1Bo9fzd2VLO/0UX2MVHwdZ9Zbw7PePP4a1ItJ2HYRFzI7npYelaXU85Xij2MX7hH+gGBwf7TMDVnZoV9B/+f2aIyFpyuKhK0FoeSHV/9AgMBAAECggEAYyqq5iucqgJXdGCO4eSx/LpolZg81ahJZXf1RgqdqYZSKnuTdFTQGflEYo0MGMAhUqwqDVkrimZ1E7zqE4kEWT/6BpnZ0edWsTWhu91+MqL/V/nRYio4CFk06a9JqliBJDhgbyOr4ReGtknYNwcT3Dw5V7hEIeRWFe007lC9tCi7mlpzBNwEIf4itmnncuA70GlxcoMkoGzfYg79eUCfXorbfJcaamR2wXLSU6KoJ422UR3L0rgzmgXzVQw9rrlQ3h6viDykKfaPi/43MN2qb6Zu5isbJIzyz0kHrcE6KJMgJhBDkLIo0f0qE/rEl1Xp/qDwr4+3WBfCHeuTFsud/QKBgQDXmA3f0/ONPMgEGdJlwG20W+7jXHabnRPuUJyDQKbtP+vuaKrpzN+jC1rlxBfAJj2iAVXXXM/RFWWapBd16TqGI4P3RW8eocaxhyl8rWSvCOy/OueNI+fM8gX/IjsJc7VMmCEWHuLvXoM2ixXPWP3v0DEPPPDrCd5dnjR6+5oGgwKBgQC7O03ps4KzMUzEtJcrFFKV0C/m1X905OqQ3cKQnGqRzLp/7d9DQsv+oKzjlpz1xktdJmig7ABiL0+FqJHdcrNiVabI5c6oS2SZkToQFlKv2GYT2KikJ0L43xLfiDvB3tues//9OXuU0WzXZqq7CNAvcmAdPjlFi9RxHsRGABo3fwKBgEi2EJ/XpQGSaUbwyoPktVsp0lS9/4aWIH20lES0DlhfwZuDk3kMzrP3hW2OiBAXFZxI5QGgXLqAg+b2xq7OvR02ZzCDK2niV9fR5Q0Wkaly0h3gqO1yGaCGU71rdwvGCXROroH+Yr0mXAyONgnbUrGJvrIL9JjgmC1syPhdWOIvAoGBAJHJbbNpWX3aB2KrE4IxwtRwVLwyxZnpnVPLuPINOVXpydZPDCc9XcYYqkZUQkeFba1MeO/Ek8/f8tWqGloKM+9/reyENFQK0Hxa/pEEMMJHh8QwUa/v+k/6sqFnXNBqjSuYEN3F4ppQL6XRhWM5S5GGR5y9lK64YGTshfvTnJZVAoGBAJ3TmJcRJWfi7CA985VAnE+IQoQfKKz9NTT7hGBwWTVd7iUc0QCpgHNIixZnfVcjKxz7Hhq6Vy+cEbDBtwbSuDfuVf1spiiqOuYVIjFqq5AsuvpX1CJmm7V+LRtJO/NXmXQP5YfojzET9NqTZvGEVXuzPA0qp8JC7HKrCYykscqE
- Signature And Validation Example:
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
@Slf4j
public class SnapSignUtil {
public static boolean verifySign(String httpMethod, String url, String accessToken, String body, String timestamp, String sign, String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
log.info("snap verifySign => httpMethod:{}, url:{}, accessToken:{}, body:{}, timestamp:{}, sign:{}", httpMethod, url, accessToken, body, timestamp, sign);
String bodySha = handleBody(minifyJsonString(body));
String payload;
if (StringUtils.isNotBlank(accessToken)) {
payload = StrUtil.format("{}:{}:{}:{}:{}", httpMethod, url, accessToken, bodySha, timestamp);
} else {
payload = StrUtil.format("{}:{}:{}:{}", httpMethod, url, bodySha, timestamp);
}
log.info("snap verifySign => payload:{}", payload);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
signature.update(payload.getBytes());
return signature.verify(Base64.getDecoder().decode(sign));
}
public static String generateSign(HttpMethod httpMethod, String url, String accessToken, String body, String timestamp, String privateKey) {
log.info("snap generateSign => httpMethod:{}, url:{}, accessToken:{}, body:{}, timestamp:{}", httpMethod, url, accessToken, body, timestamp);
String bodySha = handleBody(minifyJsonString(body));
String payload;
if (StringUtils.isNotBlank(accessToken)) {
payload = StrUtil.format("{}:{}:{}:{}:{}", httpMethod.toString(), url, accessToken, bodySha, timestamp);
} else {
payload = StrUtil.format("{}:{}:{}:{}", httpMethod.toString(), url, bodySha, timestamp);
}
log.info("snap sign payload => payload:{}", payload);
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))));
signature.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signWithoutHex = signature.sign();
String sign = Base64.getEncoder().encodeToString(signWithoutHex);
return sign;
} catch (Exception e) {
log.error("[Generate Sign] Generate error.", e);
}
return null;
}
public static String generateAccessTokenSign(String partnerId, String timestamp, String privateKey) {
String payload = StrUtil.format("{}|{}", partnerId, timestamp);
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))));
signature.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signWithoutHex = signature.sign();
return Base64.getEncoder().encodeToString(signWithoutHex);
} catch (Exception e) {
log.error("[Generate Access Token Sign] Generate error.", e);
}
return null;
}
public static Boolean verifyAccessTokenSign(String partnerId, String timestamp, String sign, String publicKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
String payload = StrUtil.format("{}|{}", partnerId, timestamp);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
signature.update(payload.getBytes());
return signature.verify(Base64.getDecoder().decode(sign));
}
public static String buildTimeStamp() {
return ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC+7"))
.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss+07:00"));
}
private static String handleBody(String body) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] bytes = messageDigest.digest(body.getBytes());
String result = new String(Hex.encodeHex(bytes));
return result.toLowerCase();
} catch (NoSuchAlgorithmException e) {
log.error("[Generate Sign] Handle body error.", e);
}
return null;
}
/**
* minify json string
* @param json
* @return
*/
public static String minifyJsonString(String json) {
try {
// Remove whitespaces and newlines
StringBuilder result = new StringBuilder();
boolean inString = false;
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '"') {
inString = !inString;
}
if (!Character.isWhitespace(c) || inString) {
result.append(c);
}
}
return result.toString();
} catch (Exception e) {
throw new RuntimeException("Parse JSON failed. Value: " + json, e);
}
}
public static final String PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdrgPLPycYmICVz2NMbJFr5O2VldgAFNz5d9vdPg10NudQ7y+00se6DCaoBGnyTMTOwxFua8qujgSlpeoA2ftIAaj5gjqxVQOZfsRKTc/qPnU+1O9ZHp2KQk3qrC2ZXriiMRvEOcgEvdUq5y158vy49wjsxkFQ8FeD2XwJmaefZwqpaNnwy4RCvgbjkiqYnS1GxGZb2h82e8jUws+3tz9n8PUTocpXCM9wxkoy4XQekgOp1Bo9fzd2VLO/0UX2MVHwdZ9Zbw7PePP4a1ItJ2HYRFzI7npYelaXU85Xij2MX7hH+gGBwf7TMDVnZoV9B/+f2aIyFpyuKhK0FoeSHV/9AgMBAAECggEAYyqq5iucqgJXdGCO4eSx/LpolZg81ahJZXf1RgqdqYZSKnuTdFTQGflEYo0MGMAhUqwqDVkrimZ1E7zqE4kEWT/6BpnZ0edWsTWhu91+MqL/V/nRYio4CFk06a9JqliBJDhgbyOr4ReGtknYNwcT3Dw5V7hEIeRWFe007lC9tCi7mlpzBNwEIf4itmnncuA70GlxcoMkoGzfYg79eUCfXorbfJcaamR2wXLSU6KoJ422UR3L0rgzmgXzVQw9rrlQ3h6viDykKfaPi/43MN2qb6Zu5isbJIzyz0kHrcE6KJMgJhBDkLIo0f0qE/rEl1Xp/qDwr4+3WBfCHeuTFsud/QKBgQDXmA3f0/ONPMgEGdJlwG20W+7jXHabnRPuUJyDQKbtP+vuaKrpzN+jC1rlxBfAJj2iAVXXXM/RFWWapBd16TqGI4P3RW8eocaxhyl8rWSvCOy/OueNI+fM8gX/IjsJc7VMmCEWHuLvXoM2ixXPWP3v0DEPPPDrCd5dnjR6+5oGgwKBgQC7O03ps4KzMUzEtJcrFFKV0C/m1X905OqQ3cKQnGqRzLp/7d9DQsv+oKzjlpz1xktdJmig7ABiL0+FqJHdcrNiVabI5c6oS2SZkToQFlKv2GYT2KikJ0L43xLfiDvB3tues//9OXuU0WzXZqq7CNAvcmAdPjlFi9RxHsRGABo3fwKBgEi2EJ/XpQGSaUbwyoPktVsp0lS9/4aWIH20lES0DlhfwZuDk3kMzrP3hW2OiBAXFZxI5QGgXLqAg+b2xq7OvR02ZzCDK2niV9fR5Q0Wkaly0h3gqO1yGaCGU71rdwvGCXROroH+Yr0mXAyONgnbUrGJvrIL9JjgmC1syPhdWOIvAoGBAJHJbbNpWX3aB2KrE4IxwtRwVLwyxZnpnVPLuPINOVXpydZPDCc9XcYYqkZUQkeFba1MeO/Ek8/f8tWqGloKM+9/reyENFQK0Hxa/pEEMMJHh8QwUa/v+k/6sqFnXNBqjSuYEN3F4ppQL6XRhWM5S5GGR5y9lK64YGTshfvTnJZVAoGBAJ3TmJcRJWfi7CA985VAnE+IQoQfKKz9NTT7hGBwWTVd7iUc0QCpgHNIixZnfVcjKxz7Hhq6Vy+cEbDBtwbSuDfuVf1spiiqOuYVIjFqq5AsuvpX1CJmm7V+LRtJO/NXmXQP5YfojzET9NqTZvGEVXuzPA0qp8JC7HKrCYykscqE";
public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAna4Dyz8nGJiAlc9jTGyRa+TtlZXYABTc+Xfb3T4NdDbnUO8vtNLHugwmqARp8kzEzsMRbmvKro4EpaXqANn7SAGo+YI6sVUDmX7ESk3P6j51PtTvWR6dikJN6qwtmV64ojEbxDnIBL3VKuctefL8uPcI7MZBUPBXg9l8CZmnn2cKqWjZ8MuEQr4G45IqmJ0tRsRmW9ofNnvI1MLPt7c/Z/D1E6HKVwjPcMZKMuF0HpIDqdQaPX83dlSzv9FF9jFR8HWfWW8Oz3jz+GtSLSdh2ERcyO56WHpWl1POV4o9jF+4R/oBgcH+0zA1Z2aFfQf/n9miMhacrioStBaHkh1f/QIDAQAB";
public static void main(String[] args) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
String appId = "869dd85c7d174f3a8e5d463796c85fe9";
String timestamp = buildTimeStamp();
String s = generateAccessTokenSign(appId, timestamp, PRIVATE_KEY);
System.out.println(s);
System.out.println(verifyAccessTokenSign(appId, timestamp, s, PUBLIC_KEY));
String httpMethod = "POST";
String url = "/v2.1/qr/qr-mpm-generate";
String accessToken = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6Ijg2OWRkODVjN2QxNzRmM2E4ZTVkNDYzNzk2Yzg1ZmU5IiwiY2FjaGVLZXkiOiJUT0tFTl9BUElfODY5ZGQ4NWM3ZDE3NGYzYThlNWQ0NjM3OTZjODVmZTlfZTdmNmVlYzEtYzg5ZC00ZGIxLWFhZTktZTM1MjNiYWQwNjM1IiwiY3JlYXRlVGltZSI6MTc0MjU0NDg3NjE5OH0.vsM-hRP2odu6ykGifcYKBgNBwIzYQibPmpuenyT8c4U";
String body = "{\"additionalInfo\":{\"channelCode\":\"QRIS_DYNAMIC_QR\",\"customerMobile\":\"08123456789\",\"customerName\":\"test123\",\"feeSplitType\":2,\"items\":[{\"branchNo\":\"123456\",\"codAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"courierNo\":\"123456\",\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"regionNo\":\"12356\",\"subOrderId\":\"123456\"},{\"branchNo\":\"123456\",\"codAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"courierNo\":\"123456\",\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"40000.00\"},\"regionNo\":\"12356\",\"subOrderId\":\"123456\"}],\"notifyUrl\":\"www.test.com\"},\"codAmount\":{\"currency\":\"IDR\",\"value\":\"60000.00\"},\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"70000.00\"},\"merchantId\":\"869dd85c7d174f3a8e5d463796c85fe9\",\"partnerReferenceNo\":\"test7099074702266642\",\"validityPeriod\":\"300\"}";
String sign = generateSign(HttpMethod.POST, url, accessToken, body, timestamp, PRIVATE_KEY);
System.out.println(sign);
System.out.println(verifySign(httpMethod, url, accessToken, body, timestamp, sign, PUBLIC_KEY));
}
}