HMAC 활용하기
HMAC을 활용한 인증 정보 무결성 검증
eKYC 인증 결과는 postMessage를 통해 전달돼요.
이때 결과가 중간에 위·변조되지 않았는지 확인하려면, 함께 전달되는 HMAC 값을 검증해야 해요.
이 문서에서는 아래 내용을 차례대로 안내할게요.
- HMAC이란 무엇인지
- postMessage에 HMAC 값이 어떻게 전달되는지
- 고객사 서버에서 HMAC을 어떻게 검증하는지
1. HMAC이란?
HMAC (Hash-based Message Authentication Code)는 메시지의 무결성과 진짜인지 여부(진본성)를 검증하는 암호화 방식이에요.
즉, 인증 결과가 중간에서 조작되거나 변경되지 않았는지를 확인하는 데 사용돼요.
HMAC이 동작하는 방식
-
HMAC 생성
비밀키(sharedSecret)와 메시지를 함께 암호화해서해시 값을 만들어요. -
HMAC 검증
수신 측(고객사 서버)에서 같은 방식으로 HMAC을 다시 계산해요.
전달받은 HMAC 값과 동일하면 위·변조되지 않은 것이고, 다르면 데이터가 변경됐을 가능성이 있어요. -
HMAC이 사용되는 위치
인증 결과(review_result)가 포함된 postMessage의 신뢰성을 확보하는 데 사용돼요.
2. postMessage에 HMAC 값이 어떻게 전달되는지
eKYC 인증 결과는 다음과 같은 JSON 구조로 postMessage로 전달돼요.
{
"hmac": {
"timestamp": "2022-06-14T07:21:29Z",
// HMAC 생성 시점의 timestamp
"value": "2vKyOeSsQn8TfdAKKaMrlb8GnSBtWSOx07zNKVV6uAc="
// HMAC(sharedSecret(비밀키), timestamp, review_result를 이용해서 계산된 HMAC)
},
"review_result": {
"id": 42,
"transaction_id": "",
"request_time": "2022-06-10T01:21:21.943Z",
"name": "postman",
"phone_number": "12345678901234567890",
"birthday": "2022-01-01",
"module": {
"id_card_ocr": true,
"id_card_verification": true,
"face_authentication": false,
"account_verification": false,
"liveness": false,
"custom_field": false,
"edd_field": false
},
"id_card": {
"modified": false,
"verified": true,
"id_card_image": "/9j/4AAQSkZJRgAB...",
"id_crop_image": null,
"original_ocr_data": "{\"idType\":\"1\",...}",
"modified_ocr_data": null,
"id_card_origin": null,
"is_manual_input": false,
"uploaded_type": "camera",
"is_uploaded": false,
"id_real": null,
"id_confidence": null
},
"face_check": null,
"account": null,
"result_type": 1,
"result_email": 1,
"result_sms": 2
},
"result": "success"
}
| 필드명 | 설명 |
|---|---|
hmac.timestamp | 인증 결과가 생성된 시각 (ISO 8601 형식) |
hmac.value | sharedSecret, timestamp, review_result |
review_result | 인증 결과 데이터 (검증 대상 본문) |
HMAC 생성 방식
아래와 같은 순서로 HMAC을 만들어요.
message = timestamp + "." + JSON.stringify(review_result)
hmac = HMAC-SHA256(sharedSecret, message)
• . 구분자를 기준으로 timestamp와 본문을 하나의 문자열로 만든 다음 해시를 생성해요.
• 생성된 해시 값은 base64로 인코딩해서 전달돼요.
3. 고객사 서버에서 HMAC을 어떻게 검증하는지
고객사 서버에서는 전달받은 timestamp와 review_result를 기반으로 HMAC을 동일한 방식으로 계산한 뒤, postMessage로 받은 hmac.value와 비교해서 위·변조 여부를 확인해야 해요.
Node.js 예제
# HMAC 검증 예시(javascript)
# crypto module load
const crypto = require('crypto');
const sharedSecret = '01234567890012345678900123456789001'
const timeStr = '2022-06-14T07:21:29Z'
const expectedHmac = '2vKyOeSsQn8TfdAKKaMrlb8GnSBtWSOx07zNKVV6uAc='
const jsonStr = `{"id":42,"transaction_id":"","request_time":"2022-06-10T01:21:21.943Z",
"name":"postman","phone_number":"12345678901234567890","birthday":"2022-01-01",
"module":{"id_card_ocr":true,"id_card_verification":true,"face_authentication":false,
"account_verification":false,"liveness":false,"custom_field":false,"edd_field":false},
"id_card":{"modified":false,"verified":true,"id_card_image":"/9j/4AAQSkZJRgAB...",
"id_crop_image":null,"original_ocr_data":"{\\"idType\\":\\"1\\",...}","modified_ocr_data":null,
"id_card_origin":null,"is_manual_input":false,"uploaded_type":"camera","is_uploaded":false,
"id_real":null,"id_confidence":null},"face_check":null,"account":null,"result_type":1,
"result_email":1,"result_sms":2}`
let result = crypto.createHmac('sha256', sharedSecret).update(jsonStr + timeStr).digest('base64');
console.log(expectedHmac === result)
Golang 예제
# HMAC 검증 예시(golang)
# crypto module load
func main() {
// 공유 받은 비밀키 (관리자 페이지 또는 세일즈팀 이메일을 통해서 전달)
sharedSecret := "01234567890012345678900123456789001"
// postMessage의 hmac.timestamp 값
timeStr := "2022-06-14T07:21:29Z"
// postMessage의 hmac.value 값
expectedHmac := "2vKyOeSsQn8TfdAKKaMrlb8GnSBtWSOx07zNKVV6uAc="
// postMessage의 review_result 값
jsonStr := `{"id":42,"transaction_id":"","request_time":"2022-06-10T01:21:21.943Z",
"name":"postman","phone_number":"12345678901234567890","birthday":"2022-01-01",
"module":{"id_card_ocr":true,"id_card_verification":true,"face_authentication":false,
"account_verification":false,"liveness":false,"custom_field":false,"edd_field":false},
"id_card":{"modified":false,"verified":true,"id_card_image":"/9j/4AAQSkZJRgAB...",
"id_crop_image":null,"original_ocr_data":"{\"idType\":\"1\",...}","modified_ocr_data":null,
"id_card_origin":null,"is_manual_input":false,"uploaded_type":"camera","is_uploaded":false,
"id_real":null,"id_confidence":null},"face_check":null,"account":null,"result_type":1,
"result_email":1,"result_sms":2}`
result := getHashValue([]byte(sharedSecret), []byte(jsonStr), []byte(timeStr))
// 전달받은 hmac과 직접 계산한 hmac 값의 동일 여부 검증
fmt.Println(expectedHmac == result)
}
func getHashValue(sharedSecret, jsonBytes, timestamp []byte) string {
jsonBytes = append(jsonBytes, timestamp...)
h := hmac.New(sha256.New, sharedSecret)
if _, err := h.Write(jsonBytes); err != nil {
log.Fatalf("Fail to get Hash: %s", err)
return ""
}
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
유의사항
sharedSecret은 반드시 서버에만 보관하고, 클라이언트에는 절대 노출되면 안 돼요.- HMAC 검증은 클라이언트가 아닌 서버에서만 수행해야 해요.
- 필요하다면 timestamp 기준으로 유효 시간(예: 5분 이내) 조건도 함께 검토해 주세요.