HMAC 생성 및 검증
API 호출 간 보안을 위한 검증 방식으로 HMAC을 사용합니다.
언어별로 아래 코드를 참조하여 HMAC 검증 부분을 구현합니다. DEV 환경에서는 "test_secret_key"를 사용하여 테스트 해주시면 됩니다. LIVE 환경의 SECRET_KEY는 연동 시작 시 전달해드립니다.
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
}
}
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;
}
}
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" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
String genSignature = generate(stringToSign, secretKey);
return reqSignature.equals(genSignature);
}
public static String generateSignature(String method, String uri, String hmacDatetime, String queryString, String payload, String secretKey)
throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
return generate(stringToSign, secretKey);
}
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;
}
private static String sortedQueryString(String queryString) {
String[] params = queryString.split("&");
Map<String, String> map = new HashMap<String, String>();
for (String param : params) {
String name = param.split("=")[0];
String value = param.split("=")[1];
map.put(name, value);
}
List<Map.Entry<String, String>> entries = new LinkedList<>(map.entrySet());
Collections.sort(entries, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));
String sortedQueryString = "";
for (Map.Entry<String, String> entry : entries) {
if (sortedQueryString != "") sortedQueryString += "&";
sortedQueryString += entry.getKey() + "=" + entry.getValue();
}
return sortedQueryString;
}
}
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" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
String genSignature = generate(stringToSign, secretKey);
return reqSignature.equals(genSignature);
}
public static String generateSignature(String method, String uri, String hmacDatetime, String queryString, String payload, String secretKey)
throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
{
String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
return generate(stringToSign, secretKey);
}
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;
}
private static String sortedQueryString(String queryString) {
String[] params = queryString.split("&");
Map<String, String> map = new HashMap<String, String>();
for (String param : params) {
String name = param.split("=")[0];
String value = param.split("=")[1];
map.put(name, value);
}
List<Map.Entry<String, String>> entries = new LinkedList<>(map.entrySet());
Collections.sort(entries, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));
String sortedQueryString = "";
for (Map.Entry<String, String> entry : entries) {
if (sortedQueryString != "") sortedQueryString += "&";
sortedQueryString += entry.getKey() + "=" + entry.getValue();
}
return sortedQueryString;
}
}
import hmac
import hashlib
import json
import base64
import urllib.parse
import datetime
from pytz import timezone
class hmac_util:
def __init__(self, secret):
self.secret = secret
def hmac_datetime(self):
now = datetime.datetime.now(timezone('Asia/Seoul'))
return now.strftime("%Y-%m-%dT%H:%M:%S%z")
def signature(self, method, uri, hmac_datetime, query_string, payload):
string_to_sign = self.__string_to_sign(method, uri, hmac_datetime, query_string, payload)
print("|- string_to_sign:\n{}".format(string_to_sign))
return self.__sign(string_to_sign)
def __sorted_query_string(self, query_string):
query_dict = urllib.parse.parse_qsl(query_string)
query_dict = sorted(query_dict)
return urllib.parse.urlencode(query_dict)
def __payload_hash(self, payload):
payload_string = json.dumps(payload, separators=(',', ':'),ensure_ascii=False)
m = hashlib.sha256()
m.update(payload_string.encode('utf-8'))
hash_string = m.hexdigest()
print("|- payload_hash: {}".format(hash_string))
return hash_string
def __string_to_sign(self, method, uri, hmac_datetime, query_string, payload):
result = ""
result += method
result += "\n"
result += uri
result += "\n"
result += hmac_datetime
result += "\n"
result += self.__sorted_query_string(query_string)
result += "\n"
result += self.__payload_hash(payload)
return result
def __sign(self, string_to_sign):
key = self.secret.encode('utf-8')
msg = string_to_sign.encode('utf-8')
hashed = hmac.new(key=key, msg=msg, digestmod=hashlib.sha256)
signature_computed = base64.b64encode(hashed.hexdigest().encode('utf-8')).decode('utf-8')
return signature_computed
require 'cgi'
require 'digest'
require 'openssl'
require 'base64'
class HmacUtil
attr_accessor :secret_key, :algorithm
module SupportAlgorithm
SHA1 = 'sha1'
SHA256 = 'SHA256'
end
def initialize(secret_key, algorithm)
@secret_key = secret_key
@algorithm = algorithm
end
def hmac_datetime
Time.now.strftime('%FT%T%:z')
end
def signature(method, uri, hmac_datetime, encoded_query_string, payload)
sign(string_to_sign(method, uri, hmac_datetime, encoded_query_string, payload))
end
private
def sorted_query_string(encoded_query_string)
parse_uri_query_string = CGI.parse(encoded_query_string)
URI.encode_www_form(parse_uri_query_string.sort.to_h).gsub('+', '%20')
end
def payload_hash(payload)
Digest::SHA256.hexdigest payload
end
def string_to_sign(method, uri, hmac_datetime, query_string, payload)
method + "\n" + uri + "\n" + hmac_datetime + "\n" + sorted_query_string(query_string) + "\n" + payload_hash(payload)
end
def sign(string_to_sign)
raw_hmac = OpenSSL::HMAC.hexdigest(@algorithm, @secret_key, string_to_sign)
Base64.strict_encode64(raw_hmac)
end
end
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.text.SimpleDateFormat
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object HmacUtil {
private const val ALGORITHM = "HmacSHA256"
private const val EXPIRES_IN_SEC = 120
fun isValid(
method: String,
uri: String,
hmacDatetime: String,
queryString: String,
payload: String,
reqSignature: String,
secretKey: String
): Boolean {
// hmacDateTime 기준 2분 경과 시 만료 처리
val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime)
val diff = Date().time - date.time
val diffSec = (diff / 1000).toInt()
if (diffSec > EXPIRES_IN_SEC) {
return false
}
val stringToSign = """
$method
$uri
$hmacDatetime
$queryString
${sha256hash(payload)}
""".trimIndent()
val genSignature = generate(stringToSign, secretKey)
return reqSignature == genSignature
}
private fun generate(plainText: String, key: String): String {
val signingKey = SecretKeySpec(key.toByteArray(), ALGORITHM)
val mac = Mac.getInstance(ALGORITHM)
mac.init(signingKey)
return base64encode(toHexString(mac.doFinal(plainText.toByteArray())))
}
private fun sha256hash(str: String): String? {
return try {
val sh = MessageDigest.getInstance("SHA-256")
sh.update(str.toByteArray())
val byteData = sh.digest()
val sb = StringBuffer()
for (i in byteData.indices) {
sb.append(((byteData[i].toInt() and 0xff) + 0x100).toString(16).substring(1))
}
sb.toString()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
}
}
private fun toHexString(bytes: ByteArray): String {
val formatter = Formatter()
for (b in bytes) {
formatter.format("%02x", b)
}
return formatter.toString()
}
private fun base64encode(str: String): String {
val targetBytes = str.toByteArray()
return Base64.getEncoder().encodeToString(targetBytes)
}
}
API 호출 시 검증에 필요한 값은 요청 헤더로 전달됩니다. 헤더에서 추출한 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==
StringToSign값의 개행 처리에 주의해서 Signature값을 생성해주세요.
샘플 코드
실제 signature
검증과 관련하여 아래의 샘플을 통해 확인하실 수 있습니다.
Last updated
Was this helpful?