솔라리스의 유저 인증에 관하여 설명합니다. 그리고 예제 프로그램을 통해서 유저에 의해 입력된 로그인 패스워드가 어떻게 비교 되는지를 보여 줍니다.

순서 소개

우리의 프로그램들이 유저의 아이덴티티를 확인할 필요가 많은데 특히 프로그램이 잠재적으로 위험하거나 권한이 필요한 작업을 수행할때가 되겠습니다. 대부분의 솔라리스 유저들이 친숙한 예제는 바로 로그인 입니다: 시스템에 어떠한 작업을 하기 전에 우리는 반드시 올바른 유저 이름과 패스워드를 입력해야 합니다. 또 다른 일반적인 예제로는 스크린이 잠겼을때 스크린 잠금을 푸는 것이 될 수 있습니다.

짧은 시리즈의 글에서 우리는 패스워드와 다른 방법들을 사용해서 유저를 인증하는 프로그램을 작성하는 방법을 살펴 볼 것입니다. 이 파트에서 우리는 기본적인 사항에 대해 알아보고 패스워드 기반 인증에서만 유효한 예제를 통해서 이러한 사항들에 대해 이해하게 될 것입니다. 파트 2에서는 우리의 프로그램을 PAM (Pluggable Authentication Module) 을 이용해서 좀 더 유연하게 만드는 방법에 대해 알아볼 것입니다.

맨위로 가기


인증(Authentication) 과 인가(Authorization)

패스워드에 대한 토론에서 종종 사용 되는 두개의 용어는 인증과 인가 입니다. 이 단어들은 종종 잘못 사용되고 있습니다. 따라서 우리는 여기서 이 단어들을 정의해 볼 것입니다. 인증(Authentication) 은 컴퓨터가 우리의 아이덴티티를 검사할때 일어 납니다: 우리가 올바른 패스워드를 이용해서 로그인 할때 운영체제 자신이 우리들을 인증 합니다. 그에 반해 인가(Authorisation) 는 우리가 주어진 작업을 수행하도록 허가 되어 있는지 혹은 특정 리소스에 접근할 수 있는지를 결정하게 됩니다.

우리가 우리자신을 인증했기 때문에 작업시에 인가를 가질 필요는 없습니다. 예를 들어 만약 우리가 보통 유저로 로그인 했다면 보통 우리는 다른 유저의 파일들에 접근할 수 없습니다: 비록 우리의 아이덴티티가 인증 되었더라도 우리는 적절한 인가 권한을 가지고 있지 않습니다. 만약 root 가 되면 시스템에 어떠한 작업도 할 수 있는 인가를 가지게 됩니다. 간단히 설명하기 위해 우리는 RBAC (Role Based Access Control) 이나 잘 분리된 권한(fine-grained privilege) 은 무시하였습니다. RBAC 에 관심있는 독자들은 System Administration Guide: Security Services 를 참고하시기 바랍니다. 잘 분리된 권한에 대한 설명은 Programming in the Solaris OS With Privileges 을 참고하시기 바랍니다.

비록 우리자신을 인증할 수 있는 여러가지 수단들이 존재하지만(예를 들어 생명공학적인 스캔, 아이덴티티 카드 등등) 우리는 오직 이 글에서 패스워드 인증만을 생각할 것입니다.

맨위로 가기

패스워드 저장 및 암호화 알고리즘

만약 우리가 입력한 패스워드 정확한 것과 비교되어 진다면 정확한 패스워드는 시스템이 어딘가에 반드시 저장되어 있어야 합니다. 솔라리스 시스템에서 유저 패스워드는 /etc/shadow 파일에 저장됩니다. 아주 예전에는 /etc/passwd 에 저장되었습니다.(파일 이름을 참고하시기 바랍니다). 여러가지 이유로 인해서 /etc/passwd 은 여러 사용자들에게 읽혀져야 합니다. 암호화된 패스워드의 노출을 막기 위해 유닉스 플랫폼의 System V 호환(솔라리스도 그중에 한가지) 들은 패스워드를 /etc/shadow 파일에 저장하며 오직 슈퍼유저들만이 읽을 수 있습니다. (주의할 점으로 이후의 글에서는 패스워드 저장소가 로컬 파일을 사용하는 것으로 가정합니다. 대형 사이트들은 보통 유저 패스워드를 NIS, LDAP 혹은 NIS+ 같은 중앙집중적인 리파지토리를 이용해서 유지 합니다.)

/etc/shadow 에 저장된 패스워드는 단방향 해싱 알고르짐에 의해 암호화 됩니다. 왜냐하면 패스워드를 일반 텍스트로 저장하는 것--즉 암호화 되지 않은 형태--은 좋은 생각이 아니기 떄문입니다. 패스워드를 암호화 하는데 사용되는 알고리즘의 단방향적인 성질은 암호화된 형태에서 패스워드를 유출할 방법이 없다는 것을 의미 합니다. 솔라리스9 12/02 이전에암호화 알고리즘은 DES( Data Encryption Standard) 에 기반한 변형된 형태를 사용해서 하드웨어를 통한 키 검색을 방지하도록 의도되었습니다.

DES 의 문제는 1977년 이후(DES 가 처음 소개된 해) 컴퓨터의 처리 속도가 엄청나게 빨라졌기 때문에 DES-암호화 된 패스워드의 brutal-force 크래킹이 실제적인 위협이 되었고 패스워드가 오직 8자 길이로 제한되었기 때문입니다. 이러한 위협을 완화시키기 위해 솔라리스 9 12/02 는 새로운 암호화 기반을 소개하였는데 이것은 좀 더 강력한 암호화 알고리즘인 MD5 와 Blowfish 등의 사용을 가능하게 하였습니다. (증가된 복잡함과 더불어 새로운 알고르짐들이 8자 이상의 패스워드 사용이 가능하도록 하였습니다) 이러한 키길이의 증가와 알고리즘의 복잡도 증가로 인해 brute-force 크래킹이 DES 로 암호화된 패스워드를 크래킹하는 것에 비해 엄청나게 어려워 졌습니다.

이 시점에서 우리는 다음과 같은 질문을 할 수 있습니다: "패스워드를 암호화 하는데 어떠한 알고리즘을 사용할지 어떻게 결정 할 수 있을까요?" 이 질문에 답변하기 위해서 우리는 몇개의 shadow 파일 항목들을 살펴볼 필요가 있습니다. 아래에는 필자의 머신에 저장된 shadow 항목의 예 입니다:

rich:TISFHhgBSAtgU:13608::::::


shadow(4) 멘 페이지는 이러한 필드들에 대해 자세히 설명하고 있습니다. 우리들은 앞의 두개의 필드에 집중할 것입니다. 첫번째 필드는 유저 이름이고 두번째 필드는 암호화된 패스워드 입니다 만약 패스워드의 제일 앞이 "$" 가 아니면 DES-기반의 암호화가 사용되었음을 뜻합니다. 이 13자 문자열의 첫 두번째 문자는 salt 로 불리고 4096 개중 하나의 DES 알고리즘이 쓰였음을 의미하는 일종의 교란문자 입니다. 나머지 11개의 문자들이 실제 암호화된 패스워드 입니다. 만약 첫번째 문자가 "$" 이라면 다음 "$" 까지의 문자들이 사용된 암호화 알고리즘을 의미하고 그 이후 부터 3번째 "$" 까지가 salt 입니다.

필자의 머신에 다른 예에서 다른 암호화 알고리즘이 사용 되었습니다:

rich:$2a$04$NZJWn7W2skvQRC5lW3H7q.ZTE8bz4xbCAtU1ttzUOy63si3phphUu:13613::::::


위에서 첫번째 "$" 문자가 있고 이것이 다른 알고리즘이 사용 되었음을 가르 킵니다(이 경우 "2a" 로 나타나고 있는데 이것은 Blowfish 를 나타 냅니다) 각각의 해당되는 라이브러리들의 매핑은 /etc/security/crypt.conf 에 지정되어 있습니다. 각 알고리즘은 고유의 멘 페이지를 가지고 있고 좀 더 자세한 정보를 포함하고 있습니다. 우리는 Blowfish 를 예제로 사용했고 crypt_bsdbf(5) 에 설명되어 있습니다.

현존하는 패스워드와 비교하기 위해 사용되는 알고리즘은 shadow 파일 항목에서 추출됩니다. 그러나 새로운 패스워드를 생성할때 어떠한 알고리즘을 사용할지는 어떻게 결정할 수 있을까요? 답은 새로운 패스워드를 생성할때 사용되는 알고리즘은 /etc/security/policy.conf 파일에 CRYPT_DEFAULT 에 지정되어 있다 입니다. 예외는 만약 패스워드가 변경되었을때 기존 패스워드가 동일한 파일 내에 CRYPT_ALGORITHMS_ALLOW 에 의해 지정된 알고리즘에 의해 암호화 되었을때 입니다.

어떠한 암호화 알고리즘이 사용되더라도 유저를 인증하기 전에 우리는 반드시 유저가 입력한 패스워드를 수집해야 합니다.

맨위로 가기

유저에게 패스워드를 입력받기

유저에게 패스워드를 입력하도록 요구할때 떠오르는 한가지 방법은 printffgets 함수를 사용해서 각각 프롬프트를 출력하고 패스워드를 입력받는 것입니다. 이러한 접근에 문제점은 다른 작업들을 하더라도 입력된 패스워드가 스크린상에 에코되는 것입니다: 절대적으로 보안의 문제점입니다! echo 를 비활성화 하는 함수를 직접 작성할 수 있지만 이렇게 직접 작성하는 대신 기본적으로 제공되는 함수를 사용하는 것이 좋습니다. 이러한 함수중에 하나는 바로 getpass 입니다:

char *getpass (const char *prompt);


getpass 함수는 프롬프트 를 출력하고 echo 를 비활성화 하고 엔터로 종료되는 문자열을 터미널에서 읽은 다음 터미널의 이전 상태를 복구하여 입력된 문자열의 포인터를 리턴합니다. 비록 표준 호환의 함수이지만 getpass 를 사용하는 것을 피해야 합니다. 왜냐하면 한가지 중요한 결점을 가지고 있기 때문입니다: 오직 첫 8글자 만이 호출자로 리턴 됩니다. 그러므로 패스워드를 8자로(어떠한 알고리즘이 사용되더라도) 제한해야 합니다. 이러한 문제를 피하기 위해서 우리는 getpassphrase 를 사용해야 합니다:

char *getpassphrase (const char *prompt);


이 함수는 getpass 와 동일하지만 257 문자 이상의 문자열을 읽고 리턴할 수 있습니다. 다음의 예제에서 우리는 이 함수를 사용할 것입니다. 비록 현재 어떠한 정식적인 표준에 의해서도 지정되어 있지는 않지만 getpassphrase 는 어떠한 이식성에 대한 우려 없이도 사용할 수 있습니다. 다시 말해서 getpassphrase 는 de facto standard(시장 표준) 입니다.

맨위로 가기

패스워드 암호화하기

유저에게서 패스워드를 입력 받은 다음에 우리는 이것을 shadow 파일에 저장된 것과 비교해보아야 합니다. 첫번째 단계로는 이 것을 암호화 하는 것입니다. 만약 새로운 암호화 알고리즘을 지원하는것에 관심이 없다면 단순히 다음의 crypt 함수를 호출 하면 됩니다:

char *crypt (const char *key, const char *salt);


만약 shadow 파일에 저장된 것과 입력된 패스워드를 비교한다면 올바른 saltcrypt 에 전달하는 가장 쉬운 방법은 유저의 암호화된 패스워드를 전달하는 것입니다. 다음 섹션에서는 어떻게 유저의 shadow 파일을 읽는지 방법을 보여줄 것입니다.

만약 입력된 패스워드를 저장된 것과 비교하는 것 대신에 새로운 패스워드를 생성해서 새로운 암호화 알고리즘을 지원하기를 원하다면 우리는 crypt_gensalt 를 호출해서 crypt 에 전달할 salt 를 생성해야 합니다:

char *crypt_gensalt (const char *oldsalt, const struct passwd *userinfo);


만약 oldsalt 가 NULL 이라면 /etc/security/policy.conf 파일에서 CRYPT_DEFAULT 에 지정된 알고리즘이 사용 될 것입니다. userinfo 구조체는 getpwnam 함수의 패밀리중 하나를 사용함으로써 생성됩니다 (이것에 대한 설명은 이 글의 범위를 벗어납니다).


맨위로 가기

유저의 Shadow 파일 항목 읽기

이전에 설명했듯이 유저가 입력한 패스워드를 암호화한 것과 유저의 현재 암호를 비교할때 우리는 반드시 적절한 salt 를 전달해야 합니다. 가장 쉬운 방법은 유저의 암호화된 패스워드를 crypt 함수에 넘기는 것입니다. 그리고 이렇게 하기 위해서는 유저의 패스워드 파일을 shadow 파일에서 일거야 합니다. 이 작업을 위해서 우리는 getspnam 함수를 이용합니다:

struct spwd *getspnam (const char *name);


getspnam 함수는 유저가 name 으로 지정한 유저의 shadow 파일 정보를 포함하고 있는 구조체의 포인터를 리턴합니다. 이 구조체는 몇개의 멤버들로 구성되어 있습니다. 하지만 오직 우리가 관심 있는 것은 이것입니다: sp_pwdp 는 암호화된 패스워드를 가르키는 포인터 입니다. 이 포인터가 crypt 에 salt 를 넘길때에 필요한 것입니다.

주의할 점은 /etc/shadow 파일이 오직 루트에 의해서만 읽기가 가능하기 때문에 오직 effective 유저 ID 0 으로 지정된 프로세스들 만이 성공적으로 getspnam 을 호출할 수 있습니다. (권한-기반의 프로세스들은 FILE_DAC_READ 권한이 유효한 권한 셋 안에 포함되어 있어야 합니다.)


맨위로 가기


한곳으로 모으기

이제 우리는 이제까지 배운것을 적용시켜서 패스워드 인증을 수행하는 간단한 어플리케이션을 작성할 준비가 되었습니다. 우리의 예제는 정확한 패스워드가 입력될때 까지 계속해서 현재 유저의 패스워드를 요구 하고 정확한 패스워드가 입력되면 종료 합니다. (현재 유저는 프로세서의 실제 유저 ID 에 의해 결정됩니다.) 예제 프로그램의 소스는 다음과 같습니다:

 1 #include <sys/types.h>
 2 #include <unistd.h>
 3 #include <pwd.h>
 4 #include <stdio.h>
 5 #include <stdlib.h>
 6 #include <shadow.h>
 7 #include <crypt.h>
 8 #include <string.h>
   
 9 int main (void)
10 {
11     struct passwd *pwd_info;
12     struct spwd *spwd_info;
13     char *ct_passwd;
14     char *enc_passwd;
   
15     if ((pwd_info = getpwuid (getuid ())) == NULL) {
16             fprintf (stderr, "Call to getpwuid failed\n");
17             exit (1);
18     }
   
19     for (;;) {
20         ct_passwd = getpassphrase ("Enter password: ");
   
21         if ((spwd_info = getspnam (pwd_info -> pw_name)) == NULL) {
22             fprintf (stderr, "Call to getspnam failed\n");
23             exit (1);
24         }
   
25         enc_passwd = crypt (ct_passwd, spwd_info -> sp_pwdp);
   
26         if (strcmp (enc_passwd, spwd_info -> sp_pwdp) == 0) {
27             printf ("Passwords match.\n");
28             break;
29         } else
30             printf ("Passwords don't match.\n");
31     }
   
32     return (0);
33 }


이 33줄 짜리 프로그램을 자세히 살펴 봅시다.

1-14: 헤더파일 포함시키고 변수 정의하기.

15-18: getpwuid 을 호출해서 프로세스의 실제 유저 ID 를 기반으로 현재 유저의 패스워드 파일 정보를 얻습니다. getlogin 혹은 cuserid 를 대신 사용할 수도 있지만 보안이 중요한 프로그램에서는 이러한 함수들의 사용을 자제해야 합니다. 왜냐하면 이것들은 /var/adm/utmpx 의 내용을 기반으로 하고 있기 때문입니다. 몇몇 유닉스 플랫폼에서, 비록 솔라리스 플랫폼에서는 아니더라도, 이 파일은 누구든 쓰기가 가능합니다. 그러므로 수상한 유저들이 파일의 항목을 변경할 수 있고 우리의 프로그램은 이러한 것을 발견해 내지 못할 것입니다. 이 작업을 하는 이유는 후에 getspnam 에 유저의 유저이름을 전달해야 하기 때문입니다.

20: 유저에게 패스워드 프롬프트를 출력하고 패스워드를 읽습니다.

21-24: getspnam 을 호출해서 유저의 shadow 파일 항목을 검색해 옵니다. 무엇보다도 이 정보는 암호화된 형태의 유저 패스워드를 포함하고 있습니다. 우리는 이 작업을 루프의 매 실행마다 반복하고 있는데 왜냐하면 프로그램 실행 중간에 유저의 패스워드가 변경될 수도 있기 때문입니다.

25: 유저가 입력한 패스워드를 암호화 하고 암호화된 버전의 패스워드를 salt 로 전달합니다.

26-31: 새롭게 암호화된 패스워드를 shadow 파일에서 검색해 온 것과 비교합니다. 비교하고 있따는 메세지를 출력하고 만약 두개가 일치하면 종료합니다. 그렇지 않다면 실패 메세지를 출력하고 다시 시도 합니다.

프로그램을 컴파일 한 후에 실행해서 어떠한 작업이 이루어 지는지 살펴 봅시다:

rich@marrakesh4156# make check_pass
cc    -o check_pass check_pass.c
rich@marrakesh4157# ./check_pass
Enter password:
Call to getspnam failed


getspnam 호출이 실패 했습니다. 왜냐하면 유저 "rich" 가 권한이 부여되지 않았기 때문입니다. 루트로 로그인한다음 다시 시도해 봅시다:

rich@marrakesh4158# su
Password:
# ./check_pass
Enter password:
Passwords don't match.
Enter password:
Passwords match.


이번에는 첫번째에 잘못된 패스워드를 입력한다음 두번째로는 성공했습니다.

아주 약간의 작업으로 이 예제는 간단한 터미널 락킹 프로그램으로 사용될 수 있습니다. 이 것은 여러분들에게 맡기겠습니다.

맨위로 가기

요약

이 글에서 우리는 기본적인 패스워드 기반의 유저 인증에 관해 알아 보았습니다. 우리는 패스워드에 관해 얘기할때 가장 중요한 두가지 단어에 대해 정의하고 설명하는 것으로 시작했습니다: 인증(authentication) 및 인가(Authorization), 전자는 우리의 아이덴티티를 증명하는 것이고 후자는 우리가 특정 작업 혹은 리소스에 대한 접근 권한이 있는지 확인 하는 것입니다.

그다음 우리는 로컬 파일 리파지토리가 사용될때 어떻게 패스워드가 저장되는지 알아 보았습니다. 그리고 솔라리스 시스템 관리자가 어떻게 어떠한 패스워드 알고리즘을 사용할지 정의하는 방법에 대해 배웠습니다.(솔라리스는 원조 DES-기반 알고리즘을 제공하고 그와 더불어 MD5 와 Blowfish 같은 알고리즘 같은 돔 더 안전한 알고리즘들도 제공합니다.)

패스워드 인증에 대한 몇가지 이론들을 설명한 다음 우리는 솔라리스가 다양한 관련 작업들을 수행하기 위해 제공하는 API 들을 살펴 보고( getpassphrase 가 이러한 작업에서 가장 적절하다는 것도 설명했습니다), crypt 를 이용해서 패스워드를 암호화 하고 유저의 shadow 파일 항목에서 getspnam 을 이용해 정보를 읽어 오는 것도 설명했습니다.

최종적으로 우리는 이러한 모든 개념들을 한대 묶어서 유저에게 패스워드를 요구하고 입력받은 정보를 로그인 패스워드와 비교하는 간단한 프로그램을 작성하였 습니다.

이 시리즈의 다음 글에서는 좀더 유연한 패스워드 인증을 수행하는 프로그램을 짜야 할때 PAM(Pluggable Authentication Module) 을 사용해서 프로그램을 하는 방법을 설명합니다.

감사의 인사

이 글을 리뷰해준 Glenn Brunette 에게 감사의 인사를 전합니다.


맨위로 가기


참고자료 저자에 관하여

Rich Teer 는 My Online Home Inventory 의 CEO 이며 독립 솔라리스 컨설턴트로 솔라리스 커뮤니티의 10년 이상 활동한 멤버 입니다. 썬의 베스트 셀러인 Solaris Systems Programming 의 저자이고 다양한 글을 작성했습니다. 오픈솔라리스 파일럿 프로그램의 멤버였고 현재는 오픈솔라리스 Governing Board(OGB) 중에 한명입니다. Rich 는 현재 Britich Columbia, Kelowna 에 그의 와이프 Jenny 와 살고 있습니다. 그의 웹 사이트는 www.rite-group.com/rich 입니다.

맨위로 가기


이 아티클의 영문 원본은
http://developers.sun.com/solaris/artic ··· is1.html
에서 볼수 있습니다.

"개발자코너" 카테고리의 다른 글

2007/10/21 23:36 2007/10/21 23:36

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/450

댓글을 달아 주세요

[로그인][오픈아이디란?]

◀ Prev 1  ... 195 196 197 198 199 200 201 202 203  ... 624  Next ▶