간단한 SSO #4 - 위조방지 HMAC

간단한 SSO #4 - 위조방지 HMAC

사이트를 이동할 때, 예를 들어 SSO을 위해 사용자 정보(예: User ID)를 전달한다고 가정하자. 이전 글을 통해서 User ID를 암호화 하는 과정을 알아봤다. 사용된 암호화 알고리즘과 사용된 키를 모른다면 다른 User ID로 가장하는 것은 불가능한 일이다.

아니다 불가능하지 않다! 엄청난 연산을 수반하지만 알고리즘을 알아내는 것은 상대적으로 쉽고 궁극에는 원래 데이터를 위조할 수도 있다 (아주 아주 어렵지만 말이다). 이번 글에서 알아볼 내용은 암호화와는 별개로 데이터 전송에 있어 중간 과정에서 위변조가 있었는지 알아내는 방법을 소개한다.

해싱 기법을 적용하여 메시지의 위변조를 방지하는 기법을 HMAC (Hash-based Message Authentication) 이라고 한다.

해싱은 원문(plain text)을 일정 길이의 바이트로 변환하는데 그 결과가 유일하여 긴 문장의 빠른 검색을 위한 키 값으로 많이 쓰인다. 그리고, 해시된 결과를 사용해서 거꾸로 원문을 복구할 수 없다는 것이 해시를 사용하는 고유한 가치라고 할 수 있다. 이러한 해시의 특성을 사용하여 데이터의 위변조 여부를 알아낼 수 있다.

아래 그림을 통해, Sender가 전달한 UserID를 Receiver 입장에서 어떻게 신뢰할 수 있는지 알아보자.

HMAC을 적용한 쿼리스트링 보안

<그림 1> HMAC을 적용한 쿼리스트링 보안

  1. 사전에 Sender와 Receiver는 별도 채널로 해시에 사용할 키 shared key를 공유한다. 그리고, 양쪽에서 공히 사용할 해시 알고리즘을 정한다.

  2. Sender는 공유키를 사용해서 User ID를 해시한다.

  3. Sender는 원본 User ID와 그 해시결과(HMAC)를 쿼리스트링 값으로 Receiver에게 전달한다.

  4. Receiver는 받은 User ID를 공유키를 사용하여 같은 알고리즘으로 해시한 결과(Receiver’s HMAC)를 만든다.

  5. Receiver가 만든 HMAC과 쿼리스트링으로 받은 HMAC이 같다면 User ID는 변경되지 않았다고 신뢰할 수 있다.

User ID + 공유키 = HMAC - 이런 공식에서 User ID를 변경하다면 그 결과인 HMAC도 변경된다. 따라서, 위조한 User ID가 Receiver에서 인정 받으려면 똑 같은 해싱 과정을 거쳐 HMAC도 제공해야 되는데, 공유키와 해시 알고리즘을 알지 못하면 어려운 일인 것이다.

해시는 키를 사용하는 방법과 사용하지 않는 두 가지 방법이 있다. 해시 알고리즘은 공개되어 있고 그 종류도 많지 않아서, 원문을 알고있는 사용자가 해시결과를 보고 어떤 해시 알고리즘이 적용되었는지 알아내는 것은 어렵지 않다. 이렇게 해시 결과를 보고 알고리즘을 유추할 수 없게 하기 위해서 원문 자체에 살짝 양념을 (salt 라고 표현하고 공유키라고 읽는다) 넣는 것이다.

이렇게 키를 사용하여 해시하는 방법을 키 해시(keyed hash)라고 하는데, 키는 원문과 함께 섞여서 한번 해시되고, 그 결과에 한번 더 섞여서 해시된다.

HMAC = Hash(Hash(message + key) + key)

이제 암호화와 HMAC 기법을 사용하여 예제 시나리오를 완성해 보자.

Cryptography + HMAC

Data Privacy와 Data Integrity를 보장할 수 있는 가장 좋은 방법은 이전 글에서 설명한 암호화 기법과 이 글에서 설명한 위조방지 기법을 함께 사용하는 것이다.

이 기법은 웹 사이트간에 간단한 Single-sign On 을 구현할 때 유용하게 사용된다. 예제 시나리오는 다음과 같다.

  • 모든 사용자는 내부 직원용 HR 포탈사이트에 로그인한다.
  • 사용자가 HR 포탈에서 View my salary 링크를 클릭하면 다른 사이트로 이동한다. 이때, 사용자 ID를 전달하여 추가 로그인을 방지한다.
  • 단, HR 포탈과 다른 사이트간에 사용자 ID는 별도 채널(out-of-band)로 동기화된다.

먼저, HR 포탈에서 링크를 클릭하면 어떤 작업을 하는지 코드로 알아보자.

<h2> HR Portal</ h2>
<div style=" margin-top:50px ;">
    <ul>
        <li> @Html.ActionLink( "View my details", "Detail" , "User" , new { id = 3 }, null)</ li>
        <li> @Html.ActionLink( "View my salary", "GoToPartnerSite" , "User" , new { id = 3 }, null) </ li>
    </ul>
</div>

<소스 1> 다른 사이트로의 링크를 포함하는 HR 포탈의 랜딩 페이지

랜딩 페이지에서는 사용자 ID를 3으로 지정하여 GoToPartnerSite 액션을 호출한다. 이 액션 메서드에서는 퀴리스트링으로 전달할 값을 암호화 하고 HMAC을 생성한다.

public ActionResult GoToPartnerSite(string id)
{
    AesCryptography cipher = new AesCryptography ();
    string secretKey = "9hXe9j9K2jXto5vIA66QAiFKBgOS9LKJFdDWI+IKp3mTn7ybSNxwV3ZQZ2UwX/l73nx5K77cu+6HRSUT7bE/VQ==" ;
    string dateTime = DateTime.UtcNow.ToString( "yyyy-MM-ddTHH:mm:ssZ");
    // HMAC(ID + DATETIME)
    byte[] hmac = HashedMac.ComputeHash(secretKey, string.Concat(id, dateTime));
    // target page; "http://localhost:50257/Authentication.aspx"
    UriBuilder target = new UriBuilder ("http" , "localhost" , 50257, "Authenticate.aspx" );

    target.Query = string.Format( "i={0}&t={1}&h={2}",
        HttpServerUtility.UrlTokenEncode(cipher.EncryptStringToBytes(id)),
        HttpServerUtility.UrlTokenEncode(cipher.EncryptStringToBytes(dateTime)),
        HttpServerUtility.UrlTokenEncode(hmac));
    return Redirect(target.Uri.AbsoluteUri);
}

<소스 2> 암호화 + HMAC

쿼리스트링의 포맷은 i={0}&t={1}&h={2} 으로 세 개의 값을 전달하는데 그 의미는 아래와 같다.

  • i = User ID 이고 그 값은 암호화 된다
  • t = 시스템의 현재 DateTime 값이고 암호화 된다
  • h = HMAC(User ID + DateTime) 이고 암호화하지 않는다
    그리고, 쿼리 스트링의 모든 값은 UrlTokenEncode 된다

예제 구현의 편의상 중요 값들이 하드코딩되어 있지만 실무에서는 configuration 처리가 필요할 것이다. 이제 다른 사이트로 이동해서서 웹 폼으로 구현한 Authenticate.aspx 의 코드를 살펴보자.

protected void Page_Load( object sender, EventArgs e)
{
    AesCryptography cipher = new AesCryptography ();
    string secretKey = "9hXe9j9K2jXto5vIA66QAiFKBgOS9LKJFdDWI+IKp3mTn7ybSNxwV3ZQZ2UwX/l73nx5K77cu+6HRSUT7bE/VQ==" ;
    try
    {
        byte[] id = HttpServerUtility.UrlTokenDecode(Request.QueryString["i" ]);
        byte[] dateTime = HttpServerUtility.UrlTokenDecode(Request.QueryString["t" ]);
        byte[] hmac = HttpServerUtility.UrlTokenDecode(Request.QueryString["h" ]);
        // decrypt
        string decryptedId = cipher.DecryptStringFromBytes(id);
        string decryptedDateTime = cipher.DecryptStringFromBytes(dateTime);
        DateTime requestTime = Convert.ToDateTime(decryptedDateTime);
        // check request time
        double allowedTimeWindow = 2;   // 2 munites
        if ( DateTime.Now.AddMinutes(allowedTimeWindow * -1) > requestTime ||
            DateTime.Now.AddMinutes(allowedTimeWindow) < requestTime)
        {
            Response.Write( "Access is not allowed due to the URL expiration.");
            return;
        }
        // make a hash
        byte[] hmacToCompare = HashedMac.ComputeHash(secretKey, string.Concat(decryptedId, decryptedDateTime));
        // check if the params have been tampered
        bool isTampered = !hmac.SequenceEqual(hmacToCompare);
        if (isTampered)
        {
            Response.Write( "Access is not permitted due to the tampered URL.");
        }
        else
        {
            Response.Write( "Welcome User ID [" + decryptedId + "]" );
        }
    } catch ( Exception ex){
        Response.Write( "Error: " + ex.Message);
    }
}

UrlTokenDecode 메서드로 쿼리스트링의 파라미터를 디코드한 후, User ID와 DateTime은 복호화한다. 지연시간(여기서는 +2분, -2분)을 참작하여 만료된 URL인지 확인한다. 이렇게 시간이란 변수를 사용하면 HMAC의 randomness도 높이고 URL 만료라는 개념도 구현할 수 있어 보안의 강도을 높일 수 있다. 마지막으로 원문과 공유키를 사용하여 재작성한 HMAC과 전달받은 HMAC이 일치하면 User ID가 안전하게 전달되었다고 할 수 있다.

위조방지 처리된 쿼리스트링

<그림 1> 위조방지 처리된 쿼리스트링

사용된 쿼리스트링을 자세히 보면 아래와 같다.

  • i = aciRK750oU2HY9msZOmchQ2
  • t = bvTUi6UcZAw3spVApw9UnlBtykBySOkgi6_4MLm9G9w1
  • h = wkwbl2mK9wDeR8W6Nr1FzcA2P5E1MEM7-aQbYU089jA1

마무리

진지하게 Single Sing-On 솔루션을 찾아보면 생각보다 많은 벤더와 솔루션 그리고, 프로토콜이 존재한다. 각각의 면면을 들여다 보면 각기 다른 장단점이 있어 상황에 맞는 솔루션을 도입하는 것이 중요하다. 하나의 솔루션이 모든 상황에 대응하기 어렵고 아무리 간단한 솔루션도 깊은 이해와 다소 복잡한 구현과정을 거쳐야 한다.

아쉽게도 여기서 소개한 방법은 추가 로그인을 피할 수 있는 단방향 보안 링크라고 할 수 있다. oAuth, OpenID 등 Security Token Service를 구현하고 Claims-based Identity를 관리할 수 있어야 SSO의 개념에 더욱 근접할 것이다.


연재 글

comments powered by Disqus