C++ OpenSSL 라이브러리 사용

THXX·2023년 9월 26일
0

Introduce

Library

이 세계에서는 수 많은 개발자가 존재하며 그들이 협업하여 소프트웨어를 만든다. 그 중 하나의 수단은 라이브러리를 제작하여 배포하는 것이고 그것을 활용하는 것이다.
Python이나 Node는 pip나 npm등의 패키지 매니저를 이용하여 다른 개발자들이 제작한 라이브러리(혹은 모듈)를 쉽게 설치하고 사용할 수 있다.
그러나 이 세상에선 Python이나 Node로만 프로그래밍하지 않는다. 가령 Microsoft Windows라는 거대한 소프트웨어를 생각하자. 이는 거의 C, C++ 및 hard-coded assembly 언어로 개발되었다. 마이크로소프트 사내에서 수 많은 라이브러리가 이를 위해 제작되었고 활용되었을 것이다. C 언어 계통의 언어로 말이다.


C/C++

C/C++는 많은 사람들이 어려워 한다. 사람들에게 익숙한 Python, Java, Javascript와 같은 언어들은 언어 자체에서 많은 기능을 자체 제공해주며 이를 쉽게 사용할 수 있게 설계되어 있다. 그러나 C/C++는 이러한 언어들보단 Low-Level한 언어이며 좀 더 기계에 친숙한 프로그래밍 언어이다. 그렇기에 저들보단 더욱 성능이 좋게 만들 수 있다. 개발자의 역량이 도와준다면 말이다.


우리는 C++로 OpenSSL(보안 메커니즘 라이브러리)를 활용하는 것에 초점을 두고 진행할 것이다.

What we need

  • C++ Compiler (g++)
  • OpenSSL 1.1.1f

g++는 있다고 가정을 하겠다.

현재 환경은 WSL2 Ubuntu 20.04.6 LTS이다.
우리가 C++에서 라이브러리를 사용하기 위해서는 #include 를 사용해야 한다는 것 쯤은 모두가 안다. 그러나 우리에겐 OpenSSL include 파일이 존재하지 않는다. 설치해야 한다.

sudo apt install openssl
sudo apt install libssl-dev

위 두 개의 패키지를 설치하면 /usr/include에 openssl include 파일이 담겨 있는 폴더가 생기며 OpenSSL 1.1.1f 라이브러리가 설치된다.

이제 C++ 소스코드에서 #include를 사용하여 OpenSSL 라이브러리에 접근할 수 있다.

Let's GO

OpenSSL 라이브러리를 이용하여 EC-DSA (타원곡선 암호화 전자서명)를 재현해보자.

include

#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/sha.h>

using namespace std;

보통 커스텀 헤더는 #include "asdf.h" 이런 식으로 include한다는 것을 아는 사람이 있을 것이다. 그러나 지금 OpenSSL include 파일들은 /usr/include, 즉 < > 로 접근할 수 있는 위치에 있으므로 저렇게 사용한다.

ECDSA key-pair를 생성하는 함수

EC_KEY* generate_ecdsa_key_pair(){
    EC_KEY *eckey = EC_KEY_new_by_curve_name(NID_secp256k1);    // EC_KEY 내 타원곡선 group 지정
    if(eckey == NULL){
        cerr << "Error occurred during allocating new ECKey." << endl;
        abort();
    }
    if(!EC_KEY_generate_key(eckey)){            // EC_KEY 내 비밀키와 공개키 생성
        cerr << "Error occurred during generating ECKey pair." << endl;
        abort();
    }
    return eckey;
}

EC_KEY* 라는 EC(Elliptic Curve) 타원곡선 비밀키-공개키 페어 객체에 대한 포인터를 반환하는 함수이다.
EC_KEY* EC_KEY_new_by_curve_name(int nid); 는 사용하고자 하는 타원곡선 암호의 #를 넣으면 내부적으로 메모리를 동적 할당하여 필요한 메모리 공간을 확보하고 해당 공간의 주소를 반환한다. 즉, 우리는 껍데기를 만든 것이다.

NID_secp256k1은 include 했던 헤더에서 매크로로 define 되어 있는 nid이다. 비트코인에서도 사용한다는 256비트 타원곡선 도메인이다.

다음, 우리는 early abort 형식의 코드를 보고 있다. EC_KEY_new_by_curve_name이 실패했다면(eckey==null) 프로그램은 "취소"된다.

다음, EC_KEY_generate_key(eckey) 함수를 실행하는 것을 볼 수 있다. 껍데기만 있던 EC_KEY* eckey에 내용물을 채워주는, 즉 실제로 Key-pair를 생성하는 함수이다. 성공한다면 1을, 실패한다면 0을 반환한다. 따라서 !를 함수 호출 앞에 붙인 것을 볼 수 있으며, 실패 시 또 프로그램이 취소된다.

여기까지 모두 성공했다면 이제 EC_KEY*인 eckey를 반환한다.

C++가 어려운 이유

이 이야기를 꼭 해야 한다. 위와 같이 C++에서 본격적으로 라이브러리를 사용하기 시작하면 동적 할당한 메모리 관리가 어렵게 된다. 지금이야 뭐 소스코드도 짧고 어디서 메모리가 할당되는지 유추하기 쉽지만, 윈도우 급이 된다고 생각해보면 끔찍하다. 왜 이런 말을 하는가? 우리는 new나 malloc을 쓰지 않았는데? 왜 이 함수가, EC_KEY_new_by_curve_name 함수가 포인터를 반환하는가? 이는 함수 내부에서 malloc이나 new를 사용하여 메모리를 할당했다는 것을 의미한다. 따라서 우리의 의지와는 상관없이 메모리 할당이 되었다는 사실을 인지해야 하며, 이를 제거하는 함수를 나중에 호출해줘야 함을 잊지 말아야 한다. 이에 실패했을 경우 메모리 누수라는 숨겨진 문제를 양산하게 되리라.


계속 진행하겠다.

ECDSA 서명하기

ECDSA_SIG* ecdsa_sign(const char* message, EC_KEY* eckey){
    SHA256_CTX c;
    ECDSA_SIG *sig;
    unsigned char m[SHA256_DIGEST_LENGTH];

    // SHA256
    SHA256_Init(&c);
    SHA256_Update(&c,message,sizeof(message));
    SHA256_Final(m,&c);
    OPENSSL_cleanse(&c,sizeof(c));

    sig=ECDSA_do_sign(m,SHA256_DIGEST_LENGTH,eckey);
    if(sig==NULL){
        cerr<<"Error occurred during ECDSA Signing." << endl;
        abort();
    }

    return sig;
}

본격적으로 전자서명 과정에 들어서게 된다. 위는 서명을 하는 함수이다. message와 keypair인 EC_KEY를 받으면, 전자서명인 ECDSA_SIG가 만들어지는 것이다.
Keypair에서 사용되는 key는 공개키가 될 것이다.

ECDSA 표준에서 해쉬함수로 SHA256을 쓰기 때문에 include에서 sha.h를 추가했다.

코드를 한 줄 한 줄 설명하기엔 글이 너무 길어진다. 대충 시퀀스는 다음과 같다.

  • message를 이용하여 m에 SHA256 digest를 생성.
  • ECDSA_SIG *sig에 ECDSA_do_sign(Digest,Digest_Length,ECKeyPair)를 이용하여 전자서명
  • 전자서명 실패 시 프로그램 취소

우리는 전자서명에 성공했다. 이제 검증해야 한다. 전자서명 전체과정은 sign->verify이므로...

ECDSA 검증하기

int ecdsa_verify(const char* message, EC_KEY* eckey, ECDSA_SIG* sig){
    SHA256_CTX c;
    unsigned char m[SHA256_DIGEST_LENGTH];

    // SHA256
    SHA256_Init(&c);
    SHA256_Update(&c,message,sizeof(message));
    SHA256_Final(m,&c);
    OPENSSL_cleanse(&c,sizeof(c));

    return ECDSA_do_verify(m,SHA256_DIGEST_LENGTH,sig,eckey);
}

전달받은 message와 Keypair eckey, 전자서명 sig를 받아서 검증 과정을 실시한다.
이 과정에서 사용되는 key는 비밀키가 될 것이다.

sign 과정과 비슷해 보인다. 검증 대상 메시지를 SHA256 digest하여 함수를 돌린다. 결과가 1이면 검증 성공, 0이면 검증 실패, 즉 메시지가 오염되었거나, 키 페어가 맞지 않음을 의미한다.

위와 같이 간단하게 설명했지만 사실 ECDSA 과정은 "갈루아 필드(GF) 내에서의 타원곡선 연산" 과정이 필요하다. 즉 수학적 지식이 있어야 해석할 수 있다. 그러나 우리는 라이브러리를 사용해서 해당 지식들 하나 없이 ECDSA를 사용할 수 있었다. 이것이 라이브러리의 강점인 것이다.

Test Function

void test_ecdsa(const char* originalmessage, const char* testmessage){
    EC_KEY* eckey = generate_ecdsa_key_pair();
    ECDSA_SIG* sig = ecdsa_sign(originalmessage,eckey);
    cout << "Original Message: " << originalmessage << "\n";    
    cout << "Test Message: " << testmessage << "\n";

    if(ecdsa_verify(testmessage,eckey,sig)){
        cout << "Successfully verified." << endl;
    }else{
        cout << "Verification FAILED." << endl;
    }
    ECDSA_SIG_free(sig);    // 메모리 누수 방지
    EC_KEY_free(eckey);     // 메모리 누수 방지
}

기껏 함수를 다 만들어놓고 사용을 안하면 안된다. 테스트 함수를 만들었다.

  1. 타원곡선 비밀키-공개키 페어 생성
  2. originalmessage를 전자서명
  3. testmessage로 검증
  4. 메모리 할당 제거

메모리 할당 제거가 사실 가장 중요한 요소일 수도 있겠다...

originalmessage와 testmessage는 argv로 가져온다. 즉 프로그램을 터미널에서 실행할 때 옆에 메시지를 붙여주면 된다.

argv 활용에 문제가 발생할 가능성이 크다. 나도 이미 그랬다. 이는 argv가 저장되는 방식 때문이다. strncpy를 이용하여 따로 복사해줘야 한다.

컴파일 및 실행

우리가 사용하려고 하는 OpenSSL Library가 이제 명시적으로 LINK 되어야 한다. -lcrypto가 해답이다.

libcrypto가 우리가 사용하려고 하는 라이브러리이다. lib은 지우고 -l 플래그 뒤에 crypto만 적어주면 OK. gcc나 g++을 라이브러리와 사용하는 방법이다.

기본적 컴퓨터 지식이다. 우리는 헤더만 include했다. 그럼 함수의 구현들은 다 어디 있다는 건가? 이미 전부 컴파일되어 라이브러리 파일로 존재하고 있는 것이다. 이를 이용하여 컴파일러가 사용하는 함수들을 라이브러리에서 꺼내쓸 수 있게 된다. 마치 도서관(라이브러리)에서 책(함수)을 꺼내 읽는 것 처럼...

libcrypto는 어디 있는 건가? g++는 어디서 라이브러리를 찾는 거지? : /usr/lib 비슷한 경로에 있을 것이다.

위 사진은 컴파일 및 실행을 동시에 진행한 결과이다. 잘 된다. ~완~


간단하게 C++ 개발 환경에서 오픈소스 보안 라이브러리인 OpenSSL을 사용해 보았다. 안타깝게도 이 글에는 허점이 많다. 확인하고 싶은데, 현재 컴퓨터의 가상화를 사용할 수 없는 관계로 WSL이 켜지지 않는다. 그렇다고 우분투를 실기기에 설치하고 싶지는 않아서 일단 놔두려고 한다. 전체적 흐름이 중요한 것이다.

사소한 문제가 하나 있었다. 라이브러리를 사용하는 데에 있어 필수 덕목은 공식 Documentation을 읽을 수 있는 능력이다. 그러나 나는 1.1.1f 버전이 깔린지도 모른채 3.0.0 버전의 Documentation을 읽는 참사를 경험한 것이다. 아무튼 빠르게 파악해서 다행이었다. 3.0.0 버전의 문서로는 도당체 아무것도 되질 않았으니까.

아무튼... C++는 도전정신을 요구한다.

~完~

profile
THXX FOR EVERYTHING

0개의 댓글