REST API HMAC 인증의 이해



관련된 좋은 포스팅이 있어 가져 왔습니다.
출처 원본이 보기 좋게 되어 있으니 링크 타고 들어가서 보시는 걸 추천 드립니다.

출처 : https://www.lesstif.com/ws/rest-api-hmac-87949394.html


REST API 제작시 꼭 알고 있어야 할 HMAC 기반 인증

유명한 서비스의 REST API 를 사용하려면 secret access key id(보안 액세스 키 ID) 와 secret access key(보안 엑세스 키) 를 생성해서 등록해야 사용이 가능한 경우가 많습니다.

예로 AWS 는 발급받은 "AWS 보안 액세스 키 ID" 를 Authorization 헤더에 추가해서 보내야 하며 본문은 "보안 엑세스 키" 를 사용해서 HMAC 방식의 인증을 거쳐야 API 사용이 가능합니다.
Authorization: AWS AWSAccessKeyId:Signature
위와 같은 방식은 내부적으로 해시 기반 메시지 인증(HMAC; Hash-based Message Authentication Code))이라는 보안 기술을 기반하고 있는데 HMAC 이 어떻게 동작하는지 잘 설명한 자료가 많지 않아서 정리해 봅니다.

인증의 필요성과 HTTP Basic Auth

사용자의 인증은 현대적인 웹 서비스에서는 매우 중요한 부분입니다. 제대로 인증을 하지 않으면 권한없는 이가 임의의 자료에 접근하거나 데이타를 훼손/파기할 수 있으며 이로 인해 서비스와 평판에 심각한 손상을 입을 수 있습니다. 전통적인 방식은 HTTP 표준에 있는 Basic Auth 방식을 사용해서 사용자의 id와 암호를 전달하는 방식입니다.
예로 아래와 같이 curl 로 인증 요청을 보내면 내부적으로
Authorization: Basic dXNlcmlkOm15cGFzc3dk
처럼 Authorization 헤더에 base64 로 encoding한 id 와 암호를 전달합니다.

$ curl -v -u userid:mypasswd example.com > GET / HTTP/1.1 > Host: example.com > Authorization: Basic dXNlcmlkOm15cGFzc3dk > User-Agent: curl/7.55.1 > Accept: */*

base64 encoding 은 암호화가 아니므로 Basic Auth 방식은 패킷을 가로채면 누구나 id와 암호를 알아낼 수 있는 문제가 있으므로 꼭 HTTPS 를 붙여서 사용해야 합니다. 그렇다고 해도 사용자의 id의 암호를 직접 보내는 것은 보안상의 여러 문제가 있고 전송한 메시지 인증이 안 되는 문제가 있으므로 HMAC 이라는 방식이 제안되었습니다.

HMAC 이란?

MD5나 SHA1, SHA256 같은 암호화 해시 함수는 입력에 대해서 유일한 출력을 내는 함수로 공개키 기반 구조(PKI; Public Key Infrastructure)나 git 의 커밋 내역, bit coin 의 transaction 등 여러 분야에 응용되고 있습니다.

HMAC 은 암호화 해시 함수를 사용하여 client 가 보낸 메시지를 인증하는 방식으로 가볍고 구현이 용이하고 속도가 빨라서 다양하게 활용되고 있으며 특히 REST API 의 인증에 필수 요소로 자리 잡고 있습니다.

HMAC 은 사용자의 id와 암호같이 민감한 정보를 직접 받을 필요 없이 사전에 공유한 secret key 와 전송할 message 를 입력받아서 Hash 기반의 MAC(Message Authentication Code) 를 생성해서 전송하며 서버는 secret key 와 message를 기반으로 MAC 를 검증해서 secret key 를 소유한 client 가 보낸 메시지가 맞는지 인증할 수 있습니다.
hmac(key, message );



HMAC 동작 방식

많이 사용되는 HMAC-SHA256 을 기준으로 HMAC 을 사용하는 인증은 아래의 Wikipedia 에 있는 pseudocode 처럼 다음 절차로 진행됩니다.

출처: https://en.wikipedia.org/wiki/HMAC#Implementation
function hmac is
    input:
        key:        Bytes     // Array of bytes
        message:    Bytes     // Array of bytes to be hashed
        hash:       Function  // The hash function to use (e.g. SHA-1)
        blockSize:  Integer   // The block size of the underlying hash function (e.g. 64 bytes for SHA-1)
        outputSize: Integer   // The output size of the underlying hash function (e.g. 20 bytes for SHA-1)
 
    // Keys longer than blockSize are shortened by hashing them
    if (length(key) > blockSize) then
        key  hash(key) // Key becomes outputSize bytes long

    // Keys shorter than blockSize are padded to blockSize by padding with zeros on the right
    if (length(key) < blockSize) then
        key  Pad(key, blockSize) // Pad key with zeros to make it blockSize bytes long

    o_key_pad  key xor [0x5c * blockSize]   // Outer padded key
    i_key_pad  key xor [0x36 * blockSize]   // Inner padded key

    return hash(o_key_pad  hash(i_key_pad  message)) // Where  is concatenation

위에서 AWS 의 API 를 사용하기 위해 생성한 secret access key 가 바로 HMAC 에서의 secret key 이며 secret access key id 는 secret access key 를 찾기 위한 index 용도로 활용됩니다.

secret key 길이 검사

입력받은 key 길이가 블록 사이즈보다 클 경우 hash 함수에 key 를 넣어서 키를 생성합니다.
key = hash(key);
key 길이가 블록 사이즈보다 작을 경우 적은 길이만큼 '\0' 을 패딩해서 오른쪽을 채웁니다. 즉 key 길이가 40 이고 block size 가 64 일 경우 key 의 41번째 바이트부터 '0' 을 24개를 padding 합니다.
padded key 구현
i_key_pad 값을 구하기 위해서 key 와 0x36 을 XOR 연산합니다.
o_key_pad 값을 구하기 위해서 key 와 0x5c 를 XOR 연산합니다.
이제 구한 i_key_pad 값에 전송할 message 를 concatenation 한 후에 hash 를 돌리고 나온 값을 o_key_pad 에 concatenation 한후에 다시 hash 를 한 값이 HMAC 이 됩니다.
hash(o_key_pad ∥ hash(i_key_pad ∥ message))
이제 송신자에게 HMAC 값과 message 를 서버에 전송하면 서버가 보유한 secret key 를 사용해서 전송받은 message 에서 HMAC 을 계산하고 client 가 전송한 HMAC 과 비교하면 됩니다.

더 안전한 HMAC 을 위해서

HMAC 은 임의의 message 와 secret key 를 사용한 이중 해시 구조이므로 다른 입력값에 대해 같은 hash 값을 내는 해시 충돌에 대해 hash 알고리즘보다 훨씬 더 안전합니다. 하지만 더 안전하게 HMAC 을 사용하려면 아래와 같은 내용을 추가하는 게 좋습니다.

전송시 안전한 채널(HTTPS) 사용

HMAC 은 secret key 가 없다면 message의 위변조가 불가능하지만 원문 message 를 같이 보내야 하므로 보안을 위해 HTTPS 같이 안전한 전송 채널을 사용하여 전송해야 합니다.

Secret key 관리

client 가 전송한 요청은 중간에 해커가 가로챌수 있지만 secret key 가 없다면 위변조해도 서버의 검증 과정에서 에러가 나게 됩니다. 반대로 secret key 가 유출된다면 해커가 임의로 위변조할 수 있으므로 secret key 를 안전하게 관리하고 유출 우려가 있을 경우 재발급해서 사용해야 합니다.

Replay attach 방지

client 가 전송한 요청은 중간에 해커가 가로채서 replay attack 에 활용할 수 있습니다.
그래서 전송 message 에 timestamp 나 serial, nonce 등 변하는 값을 포함하는 게 필요합니다.

왜 전자서명을 사용하지 않는가?

HMAC 대신 PKI 기반의 전자 서명을 사용하면 더 안전하고 견고해지지만 client 에게 인증서를 발급하고 등록 및 분실/만료시 재발급/갱신해야 하는 부담이 있습니다. 특히 PKI 는 HMAC 방식에 비해서 많은 연산을 필요로 하므로 서버 부하가 많이 생기고 속도가 느린 단점이 있으므로 REST API 등에는 HMAC 을 사용하고 전자계약등 사용자의 부인 방지(Non-Repudiation)가 필요한 업무에만 PKI 를 사용하는 것이 좋습니다.

언어별 사용 예제

각 언어별 HMAC-SHA256 을 사용하는 예제로 HMAC-SHA256 의 출력값은 Wikipedia 에 있는 다음 예제값과 같아야 합니다.
f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class HMacTest {

public static final String ALGORITHM = "HmacSHA256";

public static String calculateHMac(String key, String data) throws Exception {
Mac sha256_HMAC = Mac.getInstance(ALGORITHM);

SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), ALGORITHM);
sha256_HMAC.init(secret_key);

return byteArrayToHex(sha256_HMAC.doFinal(data.getBytes("UTF-8")));
}

public static String byteArrayToHex(byte[] a) {
StringBuilder sb = new StringBuilder(a.length * 2);
for(byte b: a)
sb.append(String.format("%02x", b));
return sb.toString();
}

public static void main(String [] args) throws Exception {
// see https://en.wikipedia.org/wiki/HMAC#Examples
// expected output:
// HMAC_SHA256("key", "The quick brown fox jumps over the lazy dog") = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
System.out.println(calculateHMac("key", "The quick brown fox jumps over the lazy dog"));
}
}

PHP

PHP hmac sha256
hash_hmac('sha256', "The quick brown fox jumps over the lazy dog", 'key');

댓글

이 블로그의 인기 게시물

[Protocol] WIEGAND 통신

Orange for Oracle에서 한글 깨짐 해결책

[UI] GNB·LNB·SNB·FNB 용어 설명