HMAC 생성 및 검증

API 호출간 보안을 위한 검증 방식으로 HMAC을 사용합니다. 언어별로 아래 코드를 참조하여 연동하며 SECRET_KEY 는 연동 시 전달드립니다. DEV 환경에서는 "test_secret_key"를 사용하여 테스트 해주시면 됩니다.

Node JS
PHP
Java 6
Java 8
Node JS
const moment = require('moment');
const qs = require('qs');
const crypto = require('crypto');
function HmacUtil () {
this.algorithm = "sha256"
this.secretKey = "test_secret_key"
this.expiresIn = 2 * 60 * 1000 // 2minutes
this.hmacDatetime = function() {
return moment().format("YYYY-MM-DDTHH:mm:ssZ")
}
this.alphabeticalSort = function(a, b) {
return a.localeCompare(b);
}
this.sortedQueryString = function(encodedQueryString) {
let obj = qs.parse(encodedQueryString);
return qs.stringify(obj, { sort: this.alphabeticalSort });
}
this.payloadHash = function(payload) {
return crypto.createHash(this.algorithm).update(payload,'utf8').digest('hex');
}
this.stringToSign = function(method, uri, hmacDatetime, queryString, payload) {
return method + "\n" + uri + "\n" + hmacDatetime + "\n" + this.sortedQueryString(queryString) + "\n" + this.payloadHash(payload)
}
this.sign = function(stringToSign) {
rawHmac = crypto.createHmac(this.algorithm, this.secretKey).update(stringToSign).digest('hex');
return Buffer.from(rawHmac).toString('base64');
}
this.signature = function(method, uri, hmacDatetime, queryString, payload) {
return this.sign(this.stringToSign(method, uri, hmacDatetime, queryString, payload));
}
this.isValid = function(method, uri, hmacDatetime, queryString, payload, signature) {
let sameSignature = this.signature(method, uri, hmacDatetime, queryString, payload) === signature
let notExpired = (new Date() - new Date(hmacDatetime)) < this.expiresIn // 2.minutes
return sameSignature && notExpired
}
}
PHP
class HmacUtil {
private $secret_key = 'test_secret_key';
private $algorithm = 'sha256';
public function __construct($params = array()) {}
public function signature($method, $uri, $hmac_datetime, $encoded_query_string, $payload) {
$sign_str = $this->string_to_sign($method, $uri, $hmac_datetime, $encoded_query_string, $payload);
$signature = $this->sign($sign_str);
return $signature;
}
public function hmac_datetime() {
$datetime = date('c');
return $datetime;
}
public function valid($method, $uri, $hmac_datetime, $encoded_query_string, $payload, $signature) {
$new_signature = $this->signature($method, $uri, $hmac_datetime, $encoded_query_string, $payload);
if($new_signature != $signature) return FALSE;
$two_minute_ago = date("c", strtotime("-2 minutes", strtotime(date("Y-m-d H:i:s"))));
if($two_minute_ago > $hmac_datetime) return FALSE;
return TRUE;
}
private function sorted_query_string($encoded_query_string) {
parse_str($encoded_query_string, $parse_uri_query_string);
sort($parse_uri_query_string);
$encoded_uri = str_replace('+', '%20', http_build_query($parse_uri_query_string));
return $encoded_uri;
}
private function sign($string_to_sign) {
$raw_hmac = hash_hmac($this->algorithm, $string_to_sign, $this->secret_key);
$signed = base64_encode($raw_hmac);
return $signed;
}
private function payload_hash($payload) {
$payload = hash('SHA256', $payload);
return $payload;
}
private function string_to_sign($method, $uri, $hmac_datetime, $encoded_query_string, $payload) {
$sign = $method."\n".$uri."\n".$hmac_datetime."\n".$this->sorted_query_string($encoded_query_string)."\n".$this->payload_hash($payload);
return $sign;
}
}
Java 6
import java.util.Date;
import java.util.Formatter;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
public final class HmacUtil {
private static final String ALGORITHM = "HmacSHA256";
private static final int EXPIRES_IN_SEC = 120;
// 검증 처리용 함수
// secretKey : dev 는 'test_secret_key' 고정이며 production 은 별도 발급됩니다.
public static boolean isValid(String method, String uri, String hmacDatetime, String queryString, String payload, String reqSignature, String secretKey)
throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
// hmacDateTime 기준 2분 경과 시 만료 처리
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime);
long diff = new Date().getTime() - date.getTime();
int diffSec = (int) (diff / (1000));
if (diffSec > EXPIRES_IN_SEC) {
return false;
}
String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + queryString + "\n" + sha256hash(payload);
String genSignature = generate(stringToSign, secretKey);
return reqSignature.equals(genSignature);
}
private static String generate(String plainText, String key)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return base64encode(toHexString(mac.doFinal(plainText.getBytes())));
}
private static String sha256hash(String str) {
String hashString = "";
try {
MessageDigest sh = MessageDigest.getInstance("SHA-256");
sh.update(str.getBytes());
byte byteData[] = sh.digest();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < byteData.length; i++) {
sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));
}
hashString = sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
hashString = null;
}
return hashString;
}
private static String toHexString(byte[] bytes) {
Formatter formatter = new Formatter();
for (byte b : bytes) {
formatter.format("%02x", b);
}
return formatter.toString();
}
private static String base64encode(String str) {
byte[] targetBytes = str.getBytes();
String encoded = DatatypeConverter.printBase64Binary(targetBytes);
return encoded;
}
}
Java 8
import java.util.Date;
import java.util.Formatter;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class HmacUtil {
private static final String ALGORITHM = "HmacSHA256";
private static final int EXPIRES_IN_SEC = 120;
// 검증 처리용 함수
// secretKey : dev 는 'test_secret_key' 고정이며 production 은 별도 발급됩니다.
public static boolean isValid(String method, String uri, String hmacDatetime, String queryString, String payload, String reqSignature, String secretKey)
throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
// hmacDateTime 기준 2분 경과 시 만료 처리
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime);
long diff = new Date().getTime() - date.getTime();
int diffSec = (int) (diff / (1000));
if (diffSec > EXPIRES_IN_SEC) {
return false;
}
String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + queryString + "\n" + sha256hash(payload);
String genSignature = generate(stringToSign, secretKey);
return reqSignature.equals(genSignature);
}
private static String generate(String plainText, String key)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return base64encode(toHexString(mac.doFinal(plainText.getBytes())));
}
private static String sha256hash(String str) {
String hashString = "";
try {
MessageDigest sh = MessageDigest.getInstance("SHA-256");
sh.update(str.getBytes());
byte byteData[] = sh.digest();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < byteData.length; i++) {
sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));
}
hashString = sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
hashString = null;
}
return hashString;
}
private static String toHexString(byte[] bytes) {
Formatter formatter = new Formatter();
for (byte b : bytes) {
formatter.format("%02x", b);
}
return formatter.toString();
}
private static String base64encode(String str) {
byte[] targetBytes = str.getBytes();
String encoded = Base64.getEncoder().encodeToString(targetBytes);
return encoded;
}
}

API 호출 시 signature 정보는 아래 헤더에 포함됩니다. 헤더에 추출한 signature 와 API 호출 정보로 생성한 signature 가 동일한 경우 유효한 요청입니다.

헤더

설명

X-Hmac-Datetime

HMAC 생성 시간 (signature 생성시에도 포함되는 값)

X-Hmac-Signature

HMAC Signature

signature 생성 예시

아래는 signature 생성에 사용 되는 데이터 예시입니다.

데이터

method

(대문자)

POST

uri

/api/offerwall/reward

hmacDateTime

2020-06-08T16:56:34+09:00

queryString

(빈값)

payload

(raw body)

{"campaign_id":"1","uid":"test_uid","advertising_id":"d2ca81dc-e3bc-4449-aa8b-f7b0f62b76b8","platform":1,"reward":100,"reward_type":0,"ad_name":"테스트 광고명!","repeat_participate_type":0,"click_key":"MTU5MTYwMzA2OTA4ODo-PDp0ZXN0X3VpZDo-PDp1U0hIaE5wOTZQeGpGaDFOUjlRR2NiU0U"}

위 데이터로 signature 를 생성한 결과입니다. (secret_key 는 test_secret_key 사용) 직접 구현한 모듈로 테스트 시 동일한 signature 가 생성 되는지 확인해주세요.

처리결과

SHA256(payload)

04dd512aa6c17b5e1f38cc3c2d9f652ea22878d51e5ea483161852f20e85bde9

StringToSign

POST

/api/offerwall/reward

2020-06-08T16:56:34+09:00

04dd512aa6c17b5e1f38cc3c2d9f652ea22878d51e5ea483161852f20e85bde9

HMAC Signature

MDY4MzYwNzc2MWYxZmViMTcxNDczZmYyNzVjY2ZlODMzYTU2OWVmMmI0MzE0N2RkZDBmZGY1MTJlMmEzMjE0Nw==

샘플 코드

실제 signature 검증과 관련하여 아래의 샘플을 통해 확인하실 수 있습니다.