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


예로 AWS 는 발급받은 "AWS 보안 액세스 키 ID" 를 Authorization 헤더에 추가해서 보내야 하며 본문은 "보안 엑세스 키" 를 사용해서 HMAC 방식의 인증을 거쳐야 API 사용이 가능합니다.

Authorization: AWS AWSAccessKeyId:Signature
CODE


위와 같은 방식은 내부적으로 해시 기반 메시지 인증(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: */*
CODE


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 );
CODE


HMAC 동작 방식


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


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
JAVA


secret key 길이 검사

  1. 입력받은 key 길이가 블록 사이즈보다 클 경우 hash 함수에 key 를 넣어서 키를 생성합니다.

    key = hash(key);
    CODE
  2. key 길이가 블록 사이즈보다 작을 경우 적은 길이만큼 '\0' 을 패딩해서 오른쪽을 채웁니다. 즉 key 길이가 40 이고 block size 가 64 일 경우 key 의 41번째 바이트부터 '0' 을 24개를 padding 합니다.

padded key 구현

  1. i_key_pad 값을 구하기 위해서 key 와 0x36 을 XOR 연산합니다.

  2. o_key_pad 값을 구하기 위해서 key 와 0x5c 를 XOR 연산합니다.
  3. 이제 구한 i_key_pad 값에 전송할 message 를 concatenation 한 후에 hash 를 돌리고 나온 값을 o_key_pad 에  concatenation 한후에 다시 hash 를 한 값이 HMAC 이 됩니다.

    hash(o_key_pad ∥ hash(i_key_pad ∥ message))
    CODE
  4. 이제 송신자에게 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
CODE


Java


PHP

PHP hmac sha256

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


같이 보기

Ref