OpenSSL 사용법

OpenSSL을 사용하려고 하면 몇가지 문제에 봉착하게 된다.

우선 쉽게 설명이 잘 되어 있는 문서가 전무하다. OpenSSL 웹사이트를 가면 몇가지 설명을 볼 수가 있는데, 대개 제대로 설명이 안되어 있다.  예를 들어 DES로 encryption과 decryption을 한다고 하자. MAN -s 3 ssl 로 OpenSSL에 대한 설명을 보면, 뭘 어디서부터 손대야 할지 알 수가 없다. 그래서 찾다 찾다 DES를 직접 건드려보기로 했다. 이때 중요한 함수가 몇개 있다.

void DES_random_key(DES_cblock *ret);

int DES_set_key(const_DES_cblock *key, DES_key_schedule *schedule);
int DES_key_sched(const_DES_cblock *key, DES_key_schedule *schedule);
int DES_set_key_checked(const_DES_cblock *key,
       DES_key_schedule *schedule);
void DES_set_key_unchecked(const_DES_cblock *key,
       DES_key_schedule *schedule);

void DES_set_odd_parity(DES_cblock *key);
int DES_is_weak_key(const_DES_cblock *key);

void DES_ecb_encrypt(const_DES_cblock *input, DES_cblock *output,
       DES_key_schedule *ks, int enc);

여기서 어떤 함수를 써야 하는지 파악하기 위해서, 간단한 예제라도 있으면 좋겠는데, 전혀 그렇지 않다.
굳이 찾자면, OpenSSL의 test 폴더에 있는 샘플 코드를 보는 것이다.
아무튼 MAN 페이지를 아무리 찾아봐도, 저 schedule 변수가 뭐할때 쓰는건지, checked와 unchecked key의 차이는 뭔지 알 수가 없다. 물론 이것은 암호화 자체의 spec에 준하는 함수이기 때문에 DES 자체를 이해하는 것이 필요하다. 하지만 DES 문서를 봐도 때로는 알 수가 없다.
그러다가 OpenSSL의 mailing list에서, 직접 특정 암호화 함수를 사용하지 말고 high-level 함수를 이용하는 것이 더 좋다라는 답변을 들었다. 음.. 그게 뭐지? 힌트로 얻은 것이 EVP_EncryptInit_ex()였다.
아.. command라인에서 쓰더라도 openssl 명령을 쓰고 그 옆에 어떤 암호화 기법을 쓸지를 정해주지 않던가? (물론 바로 암호화 명령을 써도 되지만) 음.. 함수들도 그렇게 구성이 되어 있구나.. 그런 것을 알게 되었다. 근데 이런 설명이 MAN 페이지엔 제대로 되어 있지 않다. (혹 이 시점에서 MAN 페이지 구석 구석 찾아보고 “있는데?” 하는 분들 있을지 모르겠다. 여기서 말하고자 하는 건, 눈에 확 띄지 않는다는 뜻이다. )

그래서 MAN 페이지를 뚤어지게 찾아보다가 찾아내서 만든 코드가 아래의 코드다.

- (char *)generateCipherText:(NSString *)inputString withCipherTextLength:(int *)cipherTextLength
{
	BOOL isSuccessful = YES;

	const char *inputCString = [inputString cStringUsingEncoding:NSASCIIStringEncoding];
	int sourceLength = strlen( inputCString );

	int result = 0;
	unsigned char inputBuffer[16], outputBuffer[16];
	int outputLength = 0;
	int inputLength = 0;

	bzero( inputBuffer, 16 );
	bzero( outputBuffer, 16 );

	char *finalBuffer;
	finalBuffer = (char *)malloc( 64 );
	bzero( finalBuffer, 64 );

	// Initialize cipher context
	EVP_CIPHER_CTX ctx;
	EVP_CIPHER_CTX_init( &ctx );

	// Copy a key
	unsigned char keyForDES[8];
	memcpy( keyForDES, kKeyText, 8 ); // "LXFExpor"

	result = EVP_EncryptInit(&ctx, EVP_des_ecb(), keyForDES, NULL);
	if( result )
	{
		int byteLeft = sourceLength;
		int bytesToCopy;
		int inputLoc = 0, outputLoc = 0;

		while( byteLeft > 0 )
		{
			if( byteLeft < 8 )
			{
				bytesToCopy = byteLeft;
				byteLeft = 0;
			}
			else
			{
				bytesToCopy = 8;
				byteLeft -= 8;
			}

			bzero( inputBuffer, 16 );
			memcpy( inputBuffer, inputCString+inputLoc, bytesToCopy );
			inputLoc += bytesToCopy;

			bzero( outputBuffer, 16 );
			inputLength = bytesToCopy;
			result = EVP_EncryptUpdate(&ctx, outputBuffer, &outputLength, inputBuffer, inputLength);
			if( result )
			{
				memcpy( finalBuffer+outputLoc, outputBuffer, outputLength );
				outputLoc += outputLength;
			}
			else
			{
				isSuccessful = NO;
				break;
			}
		}

		bzero( outputBuffer, 16 );
		result = EVP_EncryptFinal( &ctx, outputBuffer, &outputLength );
		if( result )
		{
			memcpy( finalBuffer+outputLoc, outputBuffer, outputLength );
			outputLoc += outputLength;

			*cipherTextLength = outputLoc;

			isSuccessful = YES;
		}
		else
		{
			isSuccessful = NO;
		}
	}
	else
		isSuccessful = NO;

	EVP_CIPHER_CTX_cleanup(&ctx);

	if( isSuccessful == NO )
	{
		NSGetCriticalAlertPanel( @"Cirtical Information", @"It couldn't create a registration file.", @"OK", nil, nil );

		return (char *)-1;
	}
	else
		return finalBuffer;
}

위의 코드는 암호화를 하는 부분, 즉 cipher 과정이다. 혹 암호화에 익숙하지 않으신 분들에겐 encryption이란 말이 더 편하게 들릴지 모르겠다. encryption == cipher, decryption == decipher로 이해하시면 되겠다.

간단해 보이지 않는가? 암호화 하는 부분만 떼어내면 참 간단한 코드다. 그런데 이게 예가 MAN page에 나오질 않는다.
Oreilly에서 OpenSSL에 관련된 책이 단 한권 나오긴 한다. 물론 SSL에 대한 것이어서 암호화에 대한 것보단 socket을 열어서 SSH로 connection을 하는 쪽에 더 촛점이 맞추어져 있는거 같긴하다. 그 부분으로써 암호화에 대한 설명이 나온다. 왜냐하면 SSH 세션을 열면, 그 안에서 통신의 안전을 위해서 암호화 기법이 사용되기 때문이다.

근데 보기엔 간단해 보이지만, 저 코드를 완성하는데 꼬박 하루가 걸렸다. 왜일까?
OpenSSL의 구현에 묘한 점이 있기때문인데, 바로 EVP_EncryptUpdate()이 호출되는 부분이다. 앞에서 언급한 “도대체 어디서부터 시작해야 하나”라는 것이 OpenSSL을 사용하기 힘들게 만드는 첫번째 이유라면, 두번째가 바로 이 함수때문 같다. (암호화 자체를 이해하는거야… 당연히 암호화에 대해 아는 사람이 OpenSSL을 시도하는 거라고 생각하니 제끼고.. )

이 함수가 동작하는데 좀 묘한 면이 있는데.. 사실 Encryption을 할때와 Decryptiond을 할때가 조금 패턴이 다르다.
즉 이 함수가 호출될때, input에 들어있는 original text가 output에 encryption이 되어서 바로 들어갈때도 있고, 아닐때도 있다는 것이다. 아마 기억에 encryption때는 바로 바로 들어갔던거 같고, decryption때가 바로 안들어가고, 그 다음 iteration때 들어갔던 것 같다. 그때, outputLength가 실제 변환된 것만큼 되지 않고 0으로 되더라는 것이다. 그래서.. 아.. 뭘 잘못 사용하고 있나보다 하고 그거 알아내려다가 하루 웬 종일 지나갔다. 알고보니 다음 단계에서 세팅되는 것이다.
아마 decryption때 그런것 같은데, encryption때는 EVP_EncryptFinal()가 호출되기 직전, 즉 마지막으로 encryption을 할때 그랬던 것같다. 아 맞다. 그런 거 같다. 그래서 outputLength가 0이 되더라도 놀라지 말고 끝까지, 즉 EVP_EncryptFinal()를 호출하라는 것이다.

OpenSSL이 이렇게 움직이는 이유는 bit stuffing때문인거 같다. MPEG4/H-264의 CABAC을 구현할때랑 JPEG Encoder/Decoder를 구현할때, 이 bit stuffing을 많이 사용했는데, 처음 구현할때는 고생했다. (USC에서..) 근데 회사에서 하니 아무래도 한번 해 본거라, 대충 어떻게 할지 방향이 이미 잡혀 있기에 고생 자체는 안했다. 시간이 들었을 뿐.
근데 내가 bit stuffing을 구현하는 것은 modulo 연산을 이용해서, 실제 일정 길이의 비트가 필요하기 전에, 그것을 충분히 대응해 줄 수있게 버퍼를 미리 채워주게 구현해서 아무런 문제가 없는데, 아마 그렇게 구현하지 않으면 (modulo 연산이 생각보다 헷갈린 면이 있어서 ), 해당 스텝내에 encoding 혹은 decoding을 하려 하면, 일정양의 비트 데이터를 확보해야 하는데, 미처 준비가 안될 수있다. 그럼 다음 스템으로 그 처리를 넘기면 그때는 확실하게 확보가 되기 때문에 (아닐 수도 있다. 데이터의 끝부분에서 미쳐 준비가 안되는 일이 생기곤 한다. ) 그렇게 구현이 되었겠다 싶다.

이런 설명이 있어야 한다. 없으니 제대로 동작하는데도 마치 안되는 것처럼 생각되어서 하루를 낭비하지 않는가?
으… USC에서 내 개인 프로젝트 못하고 이것으로 하루 웬종일 썼다.

다음에는 decoding 부분을 포스팅해 보겠다.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: