[nginx] RFC2617기준의 Digest를 알아보자. feat. nginx-http-auth-digest

2019. 7. 21. 14:56Programming/Server

반응형

RFC2617의 Digest 인증방식은 obsolates되고 RFC7616이 나오기는 했지만, 요즘에는 Digest인증을 사용하기보다는 폼 베이스 인증방식을 쓰기도 하고, 브라우저에서 RFC7616의 Digest 인증방식을 지원하지 않아서 nginx-http-auth-digest모듈에 RFC7616방식을 구현한 브랜치가 없지 않나 싶다. 아무튼 현재로써는 구현이 필요한 상황이다. 시간나면 구현해봐야겠다.

RFC2617 문서는 HTTP Authentication: Basic and Digest Access Authentication에서, RFC7616 문서는 HTTP Digest Access Authentication에서 확인이 가능하다. RFC2617기준의 Digest 인증방식에 대해 간단하게 설명하자면 다음과 같다.

1) 인증정보 없이 접속을 시도하는 클라이언트에게 401 에러와 함께, 인증에 필요한 값들을 헤더에 기록하여 전달한다. 어떤 암호화 알고리즘을 사용할지는 서버에서 판단하여 헤더값에 함께 포함시키며, 이 값이 없을 경우 MD5를 사용하는 것으로 간주한다.
2) 클라이언트는 헤더에 기록된 값들을 보고, 이 값들을 이용하여 인증에 필요한 값들을 암호화하여 서버에 전달한다.
3) 서버는 암호화된 값들을 전달받아서, 자신이 클라이언트에 넘겨준 값과 인증에 필요한 값들을 조합하여 암호화한다. 클라이언트로부터 전달받은 값과 서버에서 생성한 값이 일치할 경우 인증에 성공한 것으로 판단하며, 그렇지 않을 경우에는 인증에 실패한 것으로 판단한다.

RFC7617에서는 두 개의 방법으로 암호화된 값들을 전달한다. 예를들어 MD5와 SHA256인증을 지원하는 경우, 헤더값에는 다음과 같은 방식으로 하나의 401 에러와 두 개의 WWW-Authenticate 필드가 기록되어야 한다.

 

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
    realm="http-auth@example.org",
    qop="auth, auth-int",
    algorithm=SHA-256,
    nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
    opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
WWW-Authenticate: Digest
    realm="http-auth@example.org",
    qop="auth, auth-int",
    algorithm=MD5,
    nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
    opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"

오늘 기록할 내용은 nginx-http-auth-digest모듈에서 RFC2617에 기재된 Digest 인증방식을 구현한 코드의 분석이다. 최근 폼 베이스 인증방식에 추가로 Dgiest 인증방식을 적용하고 있는데, nginx-http-auth-digest 모듈의 구현사항을 분석하는게 도움이 되지 않을까 싶어서 진행하는 내용이다. 일반적으로는 굳이 찾아볼만한 내용이 아닌지도 모르겠다. ' ㅅ')a

 

nginx에서 digest모듈을 구현하기 위한 nginx-http-auth-digest모듈은 여러개가 존재한다. 작년에 확인했을때는 atomx의 nginx-http-auth-digest가 가장 마지막에 업데이트 됐었는데, 현재는 samizdatco의 nginx-http-auth-digest에 머지가 된 것으로 보인다. 따라서 이 기록은 samizdatco의 nginx-http-auth-digest을 기준으로 작성하도록 한다.

 

이 모듈에서 전체적으로 digest 인증방식을 처리하는 내용은 ngx_http_auth_digest_handler내에 있다. ngx_http_auth_digest_init를 보면 초기화할 때 core module로부터 main configuration을 가져와서, ngx_http_auth_digest_handler를 등록하는 걸 볼 수 있다. ngx_http_confi_get_module_main_conf에 대한 내용은 Configuration API, NGINX 페이지를, ngx_array_push에 대한 내용은 Memory Management API, NGINX 페이지를 참조하도록 하자. 이 내용이 잘 이해가 가지 않는다면 건너뛰도록 하자. 실제 nginx의 모듈을 작성하는 게 아니라면, 꼭 알고 넘어가야 할 부분은 아니다.

Digest 인증방식을 처리하는 ngx_http_auth_digest_handler의 코드는 다음과 같다. 주석이 잘 달려있으므로 코드를 따라가는데 큰 문제는 없어보인다.

static ngx_int_t ngx_http_auth_digest_handler(ngx_http_request_t *r) {
  off_t offset;
  ssize_t n;
  ngx_fd_t fd;
  ngx_int_t rc;
  ngx_err_t err;
  ngx_str_t user_file, passwd_line, realm;
  ngx_file_t file;
  ngx_uint_t i, begin, tail, idle;
  ngx_http_auth_digest_loc_conf_t *alcf;
  ngx_http_auth_digest_cred_t *auth_fields;
  u_char buf[NGX_HTTP_AUTH_DIGEST_BUF_SIZE];
  u_char line[NGX_HTTP_AUTH_DIGEST_BUF_SIZE];
  u_char *p;

  if (r->internal) {
    return NGX_DECLINED;
  }

  // if digest auth is disabled for this location, bail out immediately
  // location에서 http request를 불러온다.
  alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_digest_module);

  // http request에 realm값이 없으면 NGX_DECLINED
  if (alcf->realm.value.len == 0) {
    return NGX_DECLINED;
  }

  //Complex values 검사 후, 통과하지 못할 경우 NGX_ERROR
  if (ngx_http_complex_value(r, &alcf->realm, &realm) != NGX_OK) {
    return NGX_ERROR;
  }

  //Complex values검사 후의 realm값 길이가 0이거나, http request의 user_file값의 길이가 0이면 NGX_DECLINED
  if (realm.len == 0 || alcf->user_file.value.len == 0) {
    return NGX_DECLINED;
  }

  //realm이 off로 설정되어있으면 NGX_DECLINE
  if (ngx_strcmp(realm.data, "off") == 0) {
    return NGX_DECLINED;
  }

  //접속정보를 확인하여 evasion조건에 해당하는 클라이언트의 경우, 인증 요청을 무시한다. 예를 들어서 비밀번호를 n회 틀릴 경우가 이에 해당한다. 자세한 내용은 ngx_http_auth_digest_evading의 구현사항을 참조하도록 하자.
  if (ngx_http_auth_digest_evading(r, alcf)) {
    return NGX_HTTP_UNAUTHORIZED;
  }

  // unpack the Authorization header (if any) and verify that it contains all
  // required fields. otherwise send a challenge
  auth_fields = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_digest_cred_t));
  rc = ngx_http_auth_digest_check_credentials(r, auth_fields);
  if (rc == NGX_DECLINED) {
    return ngx_http_auth_digest_send_challenge(r, &realm, 0);
  } else if (rc == NGX_ERROR) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  // check for the existence of a passwd file and attempt to open it
  if (ngx_http_complex_value(r, &alcf->user_file, &user_file) != NGX_OK) {
    return NGX_ERROR;
  }
  fd = ngx_open_file(user_file.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
  if (fd == NGX_INVALID_FILE) {
    ngx_uint_t level;
    err = ngx_errno;

    if (err == NGX_ENOENT) {
      level = NGX_LOG_ERR;
      rc = NGX_HTTP_FORBIDDEN;

    } else {
      level = NGX_LOG_CRIT;
      rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    ngx_log_error(level, r->connection->log, err,
                  ngx_open_file_n " \"%s\" failed", user_file.data);
    return rc;
  }
  ngx_memzero(&file, sizeof(ngx_file_t));
  file.fd = fd;
  file.name = user_file;
  file.log = r->connection->log;

  // step through the passwd file and find the individual lines, then pass them
  // off
  // to be compared against the values in the authorization header
  passwd_line.data = line;
  offset = begin = tail = 0;
  idle = 1;
  ngx_memzero(buf, NGX_HTTP_AUTH_DIGEST_BUF_SIZE);
  ngx_memzero(passwd_line.data, NGX_HTTP_AUTH_DIGEST_BUF_SIZE);
  while (1) {
    n = ngx_read_file(&file, buf + tail, NGX_HTTP_AUTH_DIGEST_BUF_SIZE - tail,
                      offset);
    if (n == NGX_ERROR) {
      ngx_http_auth_digest_close(&file);
      return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    begin = 0;
    for (i = 0; i < n + tail; i++) {
      if (buf[i] == '\n' || buf[i] == '\r') {
        if (!idle &&
            i - begin >
                36) { // 36 is the min length with a single-char name and realm
          p = ngx_cpymem(passwd_line.data, &buf[begin], i - begin);
          p[0] = '\0';
          passwd_line.len = i - begin;
          // 유저의 존재 유무를 확인한다.
          // 유저를 찾지 못한 경우에는 NGX_HTTP_AUTH_DIGEST_USERNOTFOUND를 반환한다.
          // 유저를 찾은 경우에는 ngx_http_auth_digest_verify_hash를 호출하며,
          // 실제 digest의 hash값 검사는 이 함수에서 진행된다.
          rc = ngx_http_auth_digest_verify_user(r, auth_fields, &passwd_line);

          // ngx_http_auth_digest_verify_user에서 유저를 못 찾은 경우에는 NGX_DECLINED.
          if (rc == NGX_HTTP_AUTH_DIGEST_USERNOTFOUND) {
            rc = NGX_DECLINED;
          }

          if (rc != NGX_DECLINED) {
            ngx_http_auth_digest_close(&file);
            //ngx_http_auth_digest_evasion_tracking 내에서는 인증에 실패했을 시에는
            //인증 실패 횟수를 증가시키며, 성공했을 시에는 초기화한다.
            //NGX_DECLINED가 아닐 경우이므로, 여기서는 실패 횟수를 초기화할 것이다.
            ngx_http_auth_digest_evasion_tracking(
                r, alcf, NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS);
            return rc;
          }
        }
        idle = 1;
        begin = i;
      } else if (idle) {
        idle = 0;
        begin = i;
      }
    }

    if (!idle) {
      tail = n + tail - begin;
      if (n == 0 && tail > 36) {
        p = ngx_cpymem(passwd_line.data, &buf[begin], tail);
        p[0] = '\0';
        passwd_line.len = i - begin;
        rc = ngx_http_auth_digest_verify_user(r, auth_fields, &passwd_line);
        if (rc == NGX_HTTP_AUTH_DIGEST_USERNOTFOUND) {
          rc = NGX_DECLINED;
        }
        if (rc != NGX_DECLINED) {
          ngx_http_auth_digest_close(&file);
          ngx_http_auth_digest_evasion_tracking(
              r, alcf, NGX_HTTP_AUTH_DIGEST_STATUS_SUCCESS);
          return rc;
        }
      } else {
        ngx_memmove(buf, &buf[begin], tail);
      }
    }

    if (n == 0) {
      break;
    }

    offset += n;
  }

  ngx_http_auth_digest_close(&file);

  // log only wrong username/password,
  // not expired hash
  int nc = ngx_hextoi(auth_fields->nc.data, auth_fields->nc.len);
  if (nc == 1) {
    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                  "invalid username or password for %*s",
                  auth_fields->username.len, auth_fields->username.data);
  }

  ngx_http_auth_digest_evasion_tracking(r, alcf,
                                        NGX_HTTP_AUTH_DIGEST_STATUS_FAILURE);

  // since no match was found based on the fields in the authorization header,
  // send a new challenge and let the client retry
  return ngx_http_auth_digest_send_challenge(r, &realm, auth_fields->stale);
}

 

위에서 살펴본 바와 같이 ngx_http_auth_digest_handler가 해주는 일은 아래와 같다.

  • ngx_http_auth_digest_evading 함수를 이용해서 인증 실패 카운트를 확인한 후, 차단된 사용자의 경우에는 NGX_HTTP_UNAUTHORIZED를 반환한다.
  • ngx_http_auth_digest_check_credentials 함수를 이용해서 request 헤더의 Authorization필드값을 읽어들인 뒤, Digest 문자열이 포함되어있는지 확인한다. 또한 인증에 필요한 값들(username, qop, realm, nonce, nc, uri, cnonce, response, nonce)을 확인하여 제대로 된 값이 포함되어있지 않은 경우 NGX_DECLINE을 반환한다.
  • 사용자 파일을 읽어들여서 사용자를 찾은 후, 사용자가 없는 경우에는 IP를 대상으로 인증 실패 카운트를 증가시킨다.
  • ngx_http_auth_digest_check_credentails의 결과값이 NGX_DECLINED인 경우에는 ngx_http_auth_digest_send_challenge함수를 호출하여 클라이언트에게 401에러와 함께 challange값을 보낸다.
  • ngx_http_auth_digest_check_credentails의 결과값이 NGX_OK인 경우에는 ngx_http_auth_digest_verify_user함수를 호출하여 사용자가 있는지 확인한다. 사용자를 찾을 수 없으면 없으면 NGX_DECLINE을 반환하고, ngx_http_auth_digest_evasion_tracking 함수를 호출하여 인증 실패 횟수를 증가시킨다. 사용자를 찾은 경우에는 ngx_http_auth_digest_verify_user 함수를 호출하여 헤더값에 기록된 값을 확인한다. 성공했을 경우에는 ngx_http_auth_digest_evasion_tracking 함수를 호출하여 인증 실패 횟수를 초기화한다. 실패했을 경우에는 ngx_http_auth_digest_evasion_tracking 함수를 호출하여 인증 실패 횟수를 증가시킨다.
  • authrorization 필드를 헤더에서 찾지 못한 경우에는 challenge값을 클라이언트에게 전달한다.

 

위에서 코드를 쭉 훑어보고나니, RFC2617과 관련된 내용은 클라이언트에게 challenge값을 전달하는 ngx_http_auth_digest_send_challenge와,ngx_http_auth_digest_verify_user내에서 digest hash값을 확인하는 ngx_http_auth_digest_verify_hash에서 확인할 수 있음을 알 수 있다. 우선 ngx_http_auth_digest_send_challenge를 살펴보도록 하자. ngx_http_auth_digest_send_challenge의 내용은 아래와 같다.

static ngx_int_t ngx_http_auth_digest_send_challenge(ngx_http_request_t *r,
                                                     ngx_str_t *realm,
                                                     ngx_uint_t is_stale) {
  ngx_str_t challenge;
  u_char *p;
  size_t realm_len = strnlen((const char *)realm->data, realm->len);

  r->headers_out.www_authenticate = ngx_list_push(&r->headers_out.headers);
  if (r->headers_out.www_authenticate == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  r->headers_out.www_authenticate->hash = 1;
  ngx_str_set(&r->headers_out.www_authenticate->key, "WWW-Authenticate");

  challenge.len =
      sizeof("Digest algorithm=\"MD5\", qop=\"auth\", realm=\"\", nonce=\"\"") -
      1 + realm_len + 16;
  if (is_stale) {
    challenge.len += sizeof(", stale=\"true\"") - 1;
  }
  challenge.data = ngx_pnalloc(r->pool, challenge.len);
  if (challenge.data == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  ngx_http_auth_digest_nonce_t nonce;
  //ngx_time()과 ngx_random()값을 조합하여 nonce값을 생성한다.
  //ngx_http_auth_digest_next_nonce의 내용을 확인해보면 ngx_http_auth_digest_rbtree에
  //중복된 nonce값이 있는지 확인하여, 있으면 다시 새로운 값을 생성한 후
  //이 값을 ngx_http_auth_digest_rbtree에 저장하게 된다.
  //nonce값을 저장하는 이유는 nonce값의 위변조를 막기 위해서이다.
  nonce = ngx_http_auth_digest_next_nonce(r);
  if (nonce.t == 0 && nonce.rnd == 0) {
    // oom error when allocating nonce session in rbtree
    return NGX_HTTP_SERVICE_UNAVAILABLE;
  }

  p = ngx_cpymem(
      challenge.data, "Digest algorithm=\"MD5\", qop=\"auth\", realm=\"",
      sizeof("Digest algorithm=\"MD5\", qop=\"auth\", realm=\"") - 1);
  p = ngx_cpymem(p, realm->data, realm_len);
  p = ngx_cpymem(p, "\", nonce=\"", sizeof("\", nonce=\"") - 1);
  p = ngx_sprintf(p, "%08xl%08xl", nonce.rnd, nonce.t);

  if (is_stale) {
    p = ngx_cpymem(p, "\", stale=\"true\"", sizeof("\", stale=\"true\""));
  } else {
    p = ngx_cpymem(p, "\"", sizeof("\""));
  }
  //헤더의 WWW-Authenticate필드에 challenge값을 할당하고,
  //NGX_HTTP_UNAUTHORIZED를 반환함으로써 HTTP STATUS를 401로 세팅한다.
  r->headers_out.www_authenticate->value = challenge;

  return NGX_HTTP_UNAUTHORIZED;
}

 

위에서 살펴본 ngx_http_auth_digest_send_challenge을 통해 이 모듈은 알고리즘을 MD5 고정값으로 사용하고 있다는 것을 알 수 있다. MD5의 경우 최근에는 쉽게 복호화가 가능해졌기 때문에, 사용하지 않는다는 점을 생각하면 수정의 여지가 있다는 점을 알아두도록 하자. 선택적으로 SHA256을 지원하는 방안도 있을수는 있겠지만, 해당사항을 구현하는 것 보다는 RFC7612를 구현하는 쪽이 낫지 않을까 싶기도 하다... (ngx_http_auth_digest_module.c의 구현부를 살펴보면 ngx_md5.h값만 참조함으로써, 애시당초 MD5 이외의 알고리즘을 지원할 생각이 없었다는 걸 알 수 있다. 앗... 아앗... x ㅈx;;;)

 

다음으로는 ngx_http_auth_digest_verify_hash를 살펴보도록 하자. 이 함수는 클라이언트에서 전달받은 Authorization 필드를 확인하여, 해쉬값을 비교하고 인증의 성공/실패 여부를 반환하도록 한다.

static ngx_int_t
ngx_http_auth_digest_verify_hash(ngx_http_request_t *r,
                                 ngx_http_auth_digest_cred_t *fields,
                                 u_char *hashed_pw) {
  u_char *p;
  ngx_str_t http_method;
  ngx_str_t HA1, HA2, ha2_key;
  ngx_str_t digest, digest_key;
  ngx_md5_t md5;
  u_char hash[16];

  // The .net Http library sends the incorrect URI as part of the Authorization
  // response. Instead of the complete URI including the query parameters it
  // sends only the basic URI without the query parameters. It also uses this
  // value in the calculations.
  // To be compatible with the .net library the following change is made to this
  // module:
  // - Compare the URI in the Authorization (A-URI) with the request URI (R-URI).
  // - If A-URI and R-URI are identical verify is executed.
  // - If A-URI and R-URI are identical up to the '?' verify is executed
  // - Otherwise the check is not executed and authorization is declined
  if (!((r->unparsed_uri.len == fields->uri.len) &&
        (ngx_strncmp(r->unparsed_uri.data, fields->uri.data, fields->uri.len) == 0)))
  { 
    if (!((r->unparsed_uri.len > fields->uri.len) &&
          (ngx_strncmp(r->unparsed_uri.data, fields->uri.data, fields->uri.len) == 0) &&
          (r->unparsed_uri.data[fields->uri.len] == '?')))
    {
      return NGX_DECLINED; 
    }
  }

  //  the hashing scheme:
  //    digest:
  //    MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(method:uri))
  //                ^- HA1                                           ^- HA2
  //    verify: fields->response ==
  //    MD5($hashed_pw:nonce:nc:cnonce:qop:MD5(method:uri))

  // ha1 was precalculated and saved to the passwd file:
  // md5(username:realm:password)
  HA1.len = 33;
  HA1.data = ngx_pcalloc(r->pool, HA1.len);
  p = ngx_cpymem(HA1.data, hashed_pw, 32);

  // calculate ha2: md5(method:uri)
  http_method.len = r->method_name.len + 1;
  http_method.data = ngx_pcalloc(r->pool, http_method.len);
  if (http_method.data == NULL)
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  p = ngx_cpymem(http_method.data, r->method_name.data, r->method_name.len);

  ha2_key.len = http_method.len + fields->uri.len + 1;
  ha2_key.data = ngx_pcalloc(r->pool, ha2_key.len);
  if (ha2_key.data == NULL)
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  p = ngx_cpymem(ha2_key.data, http_method.data, http_method.len - 1);
  *p++ = ':';
  p = ngx_cpymem(p, fields->uri.data, fields->uri.len);

  HA2.len = 33;
  HA2.data = ngx_pcalloc(r->pool, HA2.len);
  ngx_md5_init(&md5);
  ngx_md5_update(&md5, ha2_key.data, ha2_key.len - 1);
  ngx_md5_final(hash, &md5);
  ngx_hex_dump(HA2.data, hash, 16);

  // calculate digest: md5(ha1:nonce:nc:cnonce:qop:ha2)
  digest_key.len = HA1.len - 1 + fields->nonce.len + fields->nc.len +
                   fields->cnonce.len + fields->qop.len + HA2.len - 1 + 5 + 1;
  digest_key.data = ngx_pcalloc(r->pool, digest_key.len);
  if (digest_key.data == NULL)
    return NGX_HTTP_INTERNAL_SERVER_ERROR;

  p = ngx_cpymem(digest_key.data, HA1.data, HA1.len - 1);
  *p++ = ':';
  p = ngx_cpymem(p, fields->nonce.data, fields->nonce.len);
  *p++ = ':';
  p = ngx_cpymem(p, fields->nc.data, fields->nc.len);
  *p++ = ':';
  p = ngx_cpymem(p, fields->cnonce.data, fields->cnonce.len);
  *p++ = ':';
  p = ngx_cpymem(p, fields->qop.data, fields->qop.len);
  *p++ = ':';
  p = ngx_cpymem(p, HA2.data, HA2.len - 1);

  digest.len = 33;
  digest.data = ngx_pcalloc(r->pool, 33);
  if (digest.data == NULL)
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  ngx_md5_init(&md5);
  ngx_md5_update(&md5, digest_key.data, digest_key.len - 1);
  ngx_md5_final(hash, &md5);
  ngx_hex_dump(digest.data, hash, 16);

  // compare the hash of the full digest string to the response field of the
  // auth header
  // and bail out if they don't match
  if (fields->response.len != digest.len - 1 ||
      ngx_memcmp(digest.data, fields->response.data, fields->response.len) != 0)
    return NGX_DECLINED;

  ngx_http_auth_digest_nonce_t nonce;
  ngx_uint_t key;
  ngx_http_auth_digest_node_t *found;
  ngx_slab_pool_t *shpool;
  ngx_http_auth_digest_loc_conf_t *alcf;
  ngx_table_elt_t *info_header;
  ngx_str_t hkey, hval;

  shpool = (ngx_slab_pool_t *)ngx_http_auth_digest_shm_zone->shm.addr;
  alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_digest_module);
  nonce.rnd = ngx_hextoi(fields->nonce.data, 8);
  nonce.t = ngx_hextoi(&fields->nonce.data[8], 8);
  key = ngx_crc32_short((u_char *)&nonce.rnd, sizeof nonce.rnd) ^
        ngx_crc32_short((u_char *)&nonce.t, sizeof(nonce.t));

  int nc = ngx_hextoi(fields->nc.data, fields->nc.len);
  if (nc < 0 || nc >= alcf->replays) {
    fields->stale = 1;
    return NGX_DECLINED;
  }

  // make sure nonce and nc are both valid
  ngx_shmtx_lock(&shpool->mutex);
  found = (ngx_http_auth_digest_node_t *)ngx_http_auth_digest_rbtree_find(
      key, ngx_http_auth_digest_rbtree->root,
      ngx_http_auth_digest_rbtree->sentinel);
  if (found != NULL) {
    if (found->expires <= ngx_time()) {
      fields->stale = 1;
      goto invalid;
    }
    if (!ngx_bitvector_test(found->nc, nc)) {
      goto invalid;
    }
    if (ngx_bitvector_test(found->nc, 0)) {
      // if this is the first use of this nonce, switch the expiration time from
      // the timeout
      // param to now+expires. using the 0th element of the nc vector to flag
      // this...
      ngx_bitvector_set(found->nc, 0);
      found->expires = ngx_time() + alcf->expires;
      found->drop_time = ngx_time() + alcf->drop_time;
    }

    // mark this nc as ‘used’ to prevent replays
    ngx_bitvector_set(found->nc, nc);

    // todo: if the bitvector is now ‘full’, could preemptively expire the node
    // from the rbtree
    // ngx_rbtree_delete(ngx_http_auth_digest_rbtree, found);
    // ngx_slab_free_locked(shpool, found);

    ngx_shmtx_unlock(&shpool->mutex);

    // recalculate the digest with a modified HA2 value (for rspauth) and emit
    // the
    // Authentication-Info header
    ngx_memset(ha2_key.data, 0, ha2_key.len);
    p = ngx_snprintf(ha2_key.data, 1 + fields->uri.len, ":%s",
                     fields->uri.data);

    ngx_memset(HA2.data, 0, HA2.len);
    ngx_md5_init(&md5);
    ngx_md5_update(&md5, ha2_key.data, 1 + fields->uri.len);
    ngx_md5_final(hash, &md5);
    ngx_hex_dump(HA2.data, hash, 16);

    ngx_memset(digest_key.data, 0, digest_key.len);
    p = ngx_cpymem(digest_key.data, HA1.data, HA1.len - 1);
    *p++ = ':';
    p = ngx_cpymem(p, fields->nonce.data, fields->nonce.len);
    *p++ = ':';
    p = ngx_cpymem(p, fields->nc.data, fields->nc.len);
    *p++ = ':';
    p = ngx_cpymem(p, fields->cnonce.data, fields->cnonce.len);
    *p++ = ':';
    p = ngx_cpymem(p, fields->qop.data, fields->qop.len);
    *p++ = ':';
    p = ngx_cpymem(p, HA2.data, HA2.len - 1);

    ngx_md5_init(&md5);
    ngx_md5_update(&md5, digest_key.data, digest_key.len - 1);
    ngx_md5_final(hash, &md5);
    ngx_hex_dump(digest.data, hash, 16);

    ngx_str_set(&hkey, "Authentication-Info");
    // sizeof() includes the null terminator, and digest.len also counts its
    // null terminator
    hval.len = sizeof("qop=\"auth\", rspauth=\"\", cnonce=\"\", nc=") +
               fields->cnonce.len + fields->nc.len + digest.len - 2;
    hval.data = ngx_pcalloc(r->pool, hval.len + 1);
    if (hval.data == NULL)
      return NGX_HTTP_INTERNAL_SERVER_ERROR;
    p = ngx_snprintf(hval.data, hval.len,
                     "qop=\"auth\", rspauth=\"%*s\", cnonce=\"%*s\", nc=%*s",
                     digest.len - 1, digest.data, fields->cnonce.len,
                     fields->cnonce.data, fields->nc.len, fields->nc.data);
    info_header = ngx_list_push(&r->headers_out.headers);
    if (info_header == NULL)
      return NGX_HTTP_INTERNAL_SERVER_ERROR;
    info_header->key = hkey;
    info_header->value = hval;
    info_header->hash = 1;
    return NGX_OK;
  } else {
  invalid:
    // nonce is invalid/expired or client reused an nc value. suspicious...
    ngx_shmtx_unlock(&shpool->mutex);
    return NGX_DECLINED;
  }
}

ngx_http_auth_digest_verify_hash 함수는 RFC2617 권고사항을 구현한 내용이다보니, 주석에 내용이 잘 정리되어있어서 천천히 따라서 읽으면 문제될만한게 없었다. HA1, HA2값을 계산하고, response값을 계산한 뒤에, 계산한 값이 client와 일치하지 않으면 NGX_DECLINE을 반환한다.

 

만약 nonce값을 ngx_http_auth_digest_rbtree에서 찾을 수 없으면, 마찬가지로 NGX_DECLINE을 반환한다. 타임아웃이 발생했거나, 혹은 nonce값이 변조되는 사항을 방지하기 위함으로 보인다.

 

response값과 계산값이 일치하는 경우에는 헤더의 Authentication-Info필드에 인증에 성공한 값들을 기재하여, NGX_OK를 통해 200을 반환하게 된다. 이것으로 Digest인증이 완료된다.

 

이상으로 Fomr-base에 Digest 인증을 추가적으로 적용하는 작업을 진행하는데 필요한 사항을, nginx의 ngx-http-auth-digest-module 코드를 통해 알아봤다. 다음 포스팅은 이를 기반으로 실제 폼베이스에서 RFC2617을 구현하는 데 필요한 사항을 정리해볼 예정이다.

 

아니 그보다 MD5...는 조금...
이거 알고리즘부터 바꾸는 작업을 좀 진행해야 하지 않을까...
아니면 시간을 내서 RFC7616을 구현해봐야하나... 여러가지 고민과 함께 오늘의 포스팅은 여기서 끝! ' ㅅ')/

 

잘못 파악한 부분이 있다면 댓글로 남겨주세요. 빠르게 수정하도록 하겠습니다. :)

반응형