본문 바로가기
개발 이야기/Django

Request with Signature, Nonce by Using API Key & Secret pair & Validate Request

by _ppuing 2020. 12. 30.
반응형

API key와 Secret 을 활용하여 안전한 API 를 구현할 때 Signature와 Nonce의 원리를 이용하면 쉽게 구현할 수 있다. Nonce는 쉽게 생각하면 요청 ID로 생각할 수 있으며, 이전 요청의 Nonce값 보다 다음 요청의 Nonce 값이 커져야 한다. (가장 쉬운 방법은 아래 코드처럼 타임스탬프를 이용하면 된다.)

Nonce가 필요한 이유는 공격자가 클라이언트의 요청을 훔쳐서 그대로 서버에 요청을 보내는 경우, Nonce 값이 커지지 않고 똑같기 때문에 서버가 두 번째 요청을 거절할 수 있다. 그럼 클라이언트의 요청을 훔쳐서 Nonce만 높여서 보내는 경우에는 탈취가 가능하지 않나? 맞다. 그렇기 때문에 Signature가 필요하다.

서버에서 API key와 Secret을 발급할 때 API Key는 public 하게 공개가 되더라도 문제가 없는 값이고, Secret은 클라이언트가 웬만하면 코드에 놓지 말고 안전한 곳에 보관해야 한다. Signature는 이 안전하게 보관된 Secret을 Key로 하여 Data를 암호화 서명하는 것이다. 여기서 해시 함수를 사용하기 때문에 Data에 숫자 1만 바뀌어도 결과 Signature는 아예 다른 값이 되기 때문에 Data 의 Nonce를 수정하더라도 공격자는 제대로 된 요청을 보낼 수 없다. 

이 원리로 클라이언트를 구현하면 아래와 같다.

# client.py
import json
import hmac
import hashlib
import base64
import time

API_KEY = b'API KEY'
API_SECRET = b'API SECRET'
payload = {
    'x': 3,
    'nonce': int(time.time() * 1000)
}
data = json.dumps(payload)

def generate_signature(data):
    return hmac.new(API_SECRET, msg=data, digestmod=hashlib.sha256).hexdigest()

def generate_headers(data):
    headers = {
        'Signature': generate_signature(data),
        'Authorization': API_KEY
    }
    return headers
    
def request():
    headers = generate_headers(data)
    response = requests.post(url='<testurl>', headers=headers, json=data)
    return response
    

이 코드에서 Secret이 코드에 그대로 있지만 제대로 구현하기 위해서는 안전한 스토리지에 보관해야 한다. 요청 헤더의 Signature 는 이 Secret과 요청 보낼 Data를 해시하여 만든 서명 값이고, 서버에서 "누가" 요청했는지 알리기 위해 API key를 Authorization 헤더에 넣어 전송한다. (헤더 이름은 원하는대로 바꿔도 상관 없다.)

# server.py
import json
import hmac
import hashlib
import base64
import time

# app/models.py
class TokenModel(models.Model):
    id = models.AutoField(primary_key=True)
    api_key = models.CharField(max_length=64, unique=True)
    api_secret = models.CharField(max_length=64)
    nonce = models.IntegerField(default=0)
    
# middleware.py
def generate_signature(api_secret, data):
    return hmac.new(api_secret, msg=data, digestmod=hashlib.sha256).hexdigest()

def validate_data(headers, data):
    """
    :params headers: 요청 헤더
    :params data: 데이터
    """
    nonce = int(data.get('nonce')) # client 의 nonce
    api_key = headers.get('Authorization') # client 의 api key
    signature = headers.get('Signature') # client 에서 보낸 signature
    token = Token.objects.get(api_key=api_key) # api key 로 token model 접근
    
    generated_signature = generate_signature(token.api_secret, json.dumps(data))
    if signature != generated_signature:
        raise Exception('Signature is invalid')
    
    if nonce <= token.nonce:
        raise Exception('Nonce is invalid')
    

서버에서는 request가 왔을 때 먼저 요청이 유효한지 검사해야 한다. 우선 Authorization Header로부터 API Key를 추출하고, 서버 DB에 저장되어 있는 토큰 정보(token 변수)를 읽어온다. 만약 API Key 가 Invalid하다면 Exception이 날 것이므로, token 가져오는 부분도 사실상 예외처리가 필요할 것이다. 

토큰을 정상적으로 가져왔다는 말은, 이 요청을 보낸 클라이언트가 누군지 알았다는 걸 의미한다. 그럼 그 토큰의 Secret으로 받은 Signature가 맞는 Signature인지 검증할 수 있다. POST 요청으로 들어온 Data와 Secret으로 위 client의 generate_signature와 똑같은 함수로 generated_signature를 만들고 이 값이 요청으로 들어온 Signature와 같은지 비교하면 된다.

Signature가 정확하다면, nonce가 유효한지 마지막에 검증한다. 

 

만약 Key ,Secret 모두 털렸을 때까지 체크하고 싶으면 Token 모델에 whitelist_ips(CharField)를 추가해서 요청 들어온 IP가 허용된 IP목록에 있는지도 체크할 수 있다.

반응형

댓글