티스토리 뷰

 

과거 다른 블로그에서 쓴 글을 퍼왔습니다.

Introduction

내가 관리하는 API 서버는 다양한 타입의 클라이언트들로 오는 요청을 처리하고 있다. 기본적인 oauth 인증을 이용하는 웹 어플리케이션 뿐만 아니라, Windows server application, aws lambda 등 많은 어플리케이션이 HMAC 인증을 사용하고 있다. DRF (django Rest Framework)의 Permission Class 로 구현되지 않고, view 로직에 signature 검증 로직이 있는 상태다. 즉, 아무 인증 없이 django 의 Authentication 을 통과하여 view 로직 초반에서 signature를 검증한다. 나는 이 signature 검증 로직을 view 함수 밖으로 끄집어내어 permission 검증에서 해결하고 싶었다.

 

Signature?

aws를 조금이라도 이용해본 개발자라면, Authorization: AWS4-HMAC-SHA256 와 같은 것을 봤을거다. 실제로 aws HTTP API를 이용할 때 아래와 같은 요청을 보내게 되는데, 마치 아무런 인증처리가 없는 것처럼 보인다.

GET <https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08> HTTP/1.1
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
content-type: application/x-www-form-urlencoded; charset=utf-8
host: iam.amazonaws.com
x-amz-date: 20150830T123600Z

 

우리는 두번째 줄에 있는 Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7 를 살펴봐야한다. 저 SHA-256 해시값이 어디서 튀어나왔는지 확인해볼 필요가 있다.

AWS 공식 문서 (https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html)를 살펴보면 이런 python 예제를 확인할 수 있다. 처음에 나온 예제는 EC2 API 중 DescribeRegions 를 위해 API Request 를 만드는 과정을 설명한다.

def sign(key, msg):
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
    
def getSignatureKey(key, dateStamp, regionName, serviceName):
    kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
    kRegion = sign(kDate, regionName)
    kService = sign(kRegion, serviceName)
    kSigning = sign(kService, 'aws4_request')
    return kSigning
    
"""생략"""

# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
signing_key = getSignatureKey(secret_key, datestamp, region, service)

# Sign the string_to_sign using the signing_key
signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()

 

단지 열심히 secret_key, datestamp, region, service 4개의 string으로 해싱하는 모습이다. "보내는 사람", "보내는 시간", "사용하는 서비스 리전", "서비스 이름"으로 해싱한 값으로 Authentication 처리를 한다. 그리고 300초의 datestamp 유효성 검사까지 포함된다.

정확한 이론은 https://ko.wikipedia.org/wiki/HMAC 를 참고하도록 하자.

 

class IsSignatureValidV2

Confluence 에 케케묵은 이 문서, 처리할 때가 됐지.

django 와 python 이 아무리 성숙한 써드파티 라이브러리 환경이 조성되어있다 하지만, 깃헙 스타 숫자가 낮거나 커밋이 활발하지 않다면 사용하기 꺼려지는게 사실이다. 그리고, 이렇게 밖에 굴러다니는 라이브러리를 쓰는데에는 엄청난 비용이 든다. signature 인증을 사용하는 API들이 꽤 많기 때문에 우리의 AS-IS 를 통째로 뜯어고쳐야 했기 때문이다. 남들이 만들어놓은 써드파티 라이브러리를 사용하는 것이 인증 체계를 직접 구현하는 것보다 훨씬 더 큰 비용이 든다.

골자는 아래와 같다. django restframework로 구현된 ViewSet에서 아래와 같이 간단하게 해결하는 것이다.

class ViewSet(viewsets.ViewSet):
    queryset = Model.objects.filter(is_enabled=True)
    serializer_class = Serializer
    throttle_classes = (AnonRateThrottle,)
    permission_classes = [IsSignatureValidV2]

 

단지 permission_classes 를 한 줄로 지정함으로 해결할 수 있게 만드는 것이다. (사실 저 Permission Class 이름에 V2 가 붙은건... 디자이너들의 "최종", "진짜 최종" 과 같은 거랄까.. 🧐)

 

이번 포스팅에서는 구현의 흐름정도만 봐주시면 좋을 것 같다.

from rest_framework.permissions import BasePermission
class IsSignatureValidV2(BasePermission):
    def __init__(self) -> None:
        self.data = dict()
        self.signature = None
        self.private_key_name = None
        self.timestamp_expired_in = 30
    def has_permission(self, request, view) -> bool:
        self.set_data(request, view)
        self.enforce_signature()
        self.enforce_private_key()
        self.enforce_timestamp()
        self.validate_timestamp()
        self.validate_signature()
        return True

 

request, view를 통하여 signature hashing 에 필요한 데이터를 받아서, signature를 검증하고, timestamp 가 30초 이상 지나 만료되었는지 유효성 검사하는 인증과정을 거친다. 이 모든 메소드를 거쳐 예외가 발생되지 않으면 True를 반환한다. has_permission 은 View와 Serializer 검증이 들어가기 전에 호출되며 True / False 만 반환하여야 한다. False 가 반환되면 그 API는 인증이 실패하게 된다. (401 UNAUTHORIZED)

def enforce_private_key(self):
    if not self.private_key_name:
        return
    if self.private_key_name not in self.data:
        ee = APIException()
        ee.status_code = status.HTTP_400_BAD_REQUEST
        ee.detail = const.SIGNATURE_ERROR_400_PRIVATE_KEY_NAME
        raise ee
def enforce_timestamp(self):
    if self.data.get('timestamp', None) is None:
        ee = APIException()
        ee.status_code = status.HTTP_400_BAD_REQUEST
        ee.detail = const.SIGNATURE_ERROR_400_SIGNATURE_REQUIRED
        raise ee
def enforce_signature(self):
    if self.data.get('signature', None) is None:
        ee = APIException()
        ee.status_code = status.HTTP_400_BAD_REQUEST
        ee.detail = const.SIGNATURE_ERROR_400_SIGNATURE_REQUIRED
        raise ee

 

private key와 timestamp, signature 가 요청에 있는지 검증하는 메소드다. 단순하다. set_data() 를 통해 request, view에서 받아온 데이터에 필요한 것들이 있는지 최소한으로 검사한다.

def validate_signature(self):
    if self.data['signature'] != self.calculate_signature():
        ee = APIException()
        ee.status_code = status.HTTP_400_BAD_REQUEST
        ee.detail = const.SIGNATURE_ERROR_400_INVALID_SIGNATURE
        raise ee

 

단지 self.data로 calculate_signature()를 통해 계산된 signature 값을 request 속의 signature와 같은지 비교한다.

'Python' 카테고리의 다른 글

파이썬의 모든 변수는 객체(Object) : C와의 차이점은?  (0) 2019.07.25
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함