프로그래밍/C++

C++] boost에 openssl을 사용해 ssl 적용하기.

Hwan2 2020. 7. 15. 16:26
반응형

1. 구축환경

 
  • 운영체제 : Ubuntu 20.04
  • openssl : 1.1.1f 2020/03
  • g++ : 9.3.0
  • boost : 1.71
 
※ 이 외에도 저는 cpprestSDK를 사용하기 때문에 라이브러리를 불러오는 과정에서 약간의 차이점이 있을 수 있습니다.
 
※ 해당 예제는 클라이언트에서 load_verify_file() 를 사용해 서버의 인증서를 확인하는 예제입니다.
 
 

2. openssl로 인증서 만들기

 

1) Root certificate

  • root private key

 

$ openssl genrsa -out rootca.key 2048

 

※ 위 인증서는 인증서 요청을 할 때 암호를 일일히 안쳐줘도 되는 인증서입니다. 

클라이언트 측에서 인증서를 요청할 때 해당 인증서의 비밀번호를 요구하는 인증서를 만들 경우에는 '-des3' 를 추가로 입력해주면 됩니다.

 

 

  • root certificate based on private key
$ openssl req -x509 -new -nodes -key rootca.key -days 20000 -out rootca.crt

 

위 명령어는 위에서 생성한 root private key를 가지고 ca 인증서를 만드는 명령어 입니다.

x509암호 알고리즘을 사용하고 기한은 20000일(약 50년)으로 설정합니다.

 

 

 

위 명령어를 실행하고나면 사진과 같이 몇가지 설정하라는 커멘드가 나옵니다.

 

되도록 Country Namestate or province Name위 예제와 같이 동일하게 적어주시기 바랍니다.

 

나머지 부분은 생략해도 되는 부분이며, Common Name 부분은 반드시 적어줘야합니다.

 

 

.key와 .crt 파일이 완성되었다면 해당 디렉토리에 다음과 같은 파일이 생성되어야 합니다.

 

 

 

2) User certificate

  • user private key
$ openssl genrsa -out user.key 2048

 

  • user Certificate Signing Request based on private key
$ openssl req -new -key user.key -out user.csr

 

※ 빨간색 네모박스 안에 있는 커멘드 내용은 위 rootca.crt 파일을 만들때와 동일하지만

밑에 Common name 부분은 반드시 다르게 작성해주시기 바랍니다.

 

그 후 나오는 Challenge password 부분은 사용자 마음대로 설정해주시면 됩니다.

해당 암호는 인증서에 대한 암호를 주는 것입니다.

 

 

  • user.csr based on rootCA.crt
 $ openssl x509 -req -in user.csr -CA rootca.crt -CAkey rootca.key -CAcreateserial -out user.crt -days 20000

 

 

3) Check

$ openssl verify -CAfile rootca.crt rootca.crt  //OK $ openssl verify -CAfile rootca.crt user.crt    //OK $ openssl verify -CAfile user.crt user.crt      //Fail

 

첫번째 명령어의 경우 OK표시가 떠야합니다. (루트 인증서에 대한 자체 서명이므로... 즉, 본인이 본인깨 맞는지 확인하는 작업)

 

두번째 명령어의 경우에도 OK표시가 떠야합니다. (user.crt는 rootca.crt를 기반으로 만들어 졌기때문)

 

세번째 명령어의 경우에는 Fail이 떠야합니다. 만약 OK가 뜬다면 이는 잘못만들어진 것이므로 다시 만드세요!!

 

 

 

 

4) Diffie-Hellman

$ openssl dhparam -out dh2048.pem 2048

해당 .pem은 TLS 프로토콜의 취약점을 보완해주는 암호화 기법입니다.

정확히는 Logjam을 막기위한 용도입니다. (자세한 레퍼런스는 맨 아레에 기제하겠습니다.)

 

 

※ 이제 rootCA.crt를 기반으로 만든 user.crt가 잘 해독되는지 확인해볼 것입니다.

 

 

 

 

2. Server.cpp sample code

 
#include <cstdlib>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using boost::asio::ip::tcp;

class session : public std::enable_shared_from_this<session>
{
public:
    session(tcp::socket socket, boost::asio::ssl::context& context)
        : socket_(std::move(socket), context)
    {
    }

    void start()
    {
        do_handshake();
    }

private:
    void do_handshake()
    {
        auto self(shared_from_this());
        socket_.async_handshake(boost::asio::ssl::stream_base::server,
            [this, self](const boost::system::error_code& error)
            {
                if (!error)
                {
                    do_read();
                }
            });
    }

    void do_read()
    {
        auto self(shared_from_this());
        socket_.async_read_some(boost::asio::buffer(data_),
            [this, self](const boost::system::error_code& ec, std::size_t length)
            {
                if (!ec)
                {
                    std::cout << "Received: ";
                    std::cout.write(data_, length);
                    std::cout << "\n";
                    std::cout << "Sending back again\n";
                    do_write(length);
                }
            });
    }

    void do_write(std::size_t length)
    {
        auto self(shared_from_this());
        boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
            [this, self](const boost::system::error_code& ec,
                std::size_t /*length*/)
            {
                if (!ec)
                {
                    do_read();
                }
            });
    }

    boost::asio::ssl::stream<tcp::socket> socket_;
    char data_[1024];
};

class server
{
public:
    server(boost::asio::io_context& io_context, unsigned short port)
        : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)),
        context_(boost::asio::ssl::context::sslv23)
    {
        context_.set_options(
            boost::asio::ssl::context::default_workarounds
            | boost::asio::ssl::context::no_sslv2
            | boost::asio::ssl::context::single_dh_use);
        context_.set_password_callback(std::bind(&server::get_password, this));
        context_.use_certificate_chain_file("user.crt");
        context_.use_private_key_file("user.key", boost::asio::ssl::context::pem);
        context_.use_tmp_dh_file("dh2048.pem");

        do_accept();
    }

private:
    std::string get_password() const
    {
        return "test";
    }

    void do_accept()
    {
        acceptor_.async_accept(
            [this](const boost::system::error_code& error, tcp::socket socket)
            {
                if (!error)
                {
                    std::make_shared<session>(std::move(socket), context_)->start();
                }

                do_accept();
            });
    }

    tcp::acceptor acceptor_;
    boost::asio::ssl::context context_;
};

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 2)
        {
            std::cerr << "Usage: server <port>\n";
            return 1;
        }

        boost::asio::io_context io_context;

        using namespace std; // For atoi.
        server s(io_context, atoi(argv[1]));

        io_context.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}
$ g++ server.cpp -lboost_system -lcrypto -lssl -lpthread -o server.exe

 

 

 

3. Client.cpp sample code

#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using boost::asio::ip::tcp;
using std::placeholders::_1;
using std::placeholders::_2;

enum { max_length = 1024 };

class client
{
public:
    client(boost::asio::io_context& io_context,
        boost::asio::ssl::context& context,
        const tcp::resolver::results_type& endpoints)
        : socket_(io_context, context)
    {
        socket_.set_verify_mode(boost::asio::ssl::verify_peer);
        socket_.set_verify_callback(
            std::bind(&client::verify_certificate, this, _1, _2));

        connect(endpoints);
    }

private:
    bool verify_certificate(bool preverified,
        boost::asio::ssl::verify_context& ctx)
    {
        // The verify callback can be used to check whether the certificate that is
        // being presented is valid for the peer. For example, RFC 2818 describes
        // the steps involved in doing this for HTTPS. Consult the OpenSSL
        // documentation for more details. Note that the callback is called once
        // for each certificate in the certificate chain, starting from the root
        // certificate authority.

        // In this example we will simply print the certificate's subject name.
        char subject_name[256];
        X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
        X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
        std::cout << "Verifying " << subject_name << "\n";

        return preverified;
    }

    void connect(const tcp::resolver::results_type& endpoints)
    {
        boost::asio::async_connect(socket_.lowest_layer(), endpoints,
            [this](const boost::system::error_code& error,
                const tcp::endpoint& /*endpoint*/)
            {
                if (!error)
                {
                    handshake();
                }
                else
                {
                    std::cout << "Connect failed: " << error.message() << "\n";
                }
            });
    }

    void handshake()
    {
        socket_.async_handshake(boost::asio::ssl::stream_base::client,
            [this](const boost::system::error_code& error)
            {
                if (!error)
                {
                    send_request();
                }
                else
                {
                    std::cout << "Handshake failed: " << error.message() << "\n";
                }
            });
    }

    void send_request()
    {
        std::cout << "Enter message: ";
        std::cin.getline(request_, max_length);
        size_t request_length = std::strlen(request_);

        boost::asio::async_write(socket_,
            boost::asio::buffer(request_, request_length),
            [this](const boost::system::error_code& error, std::size_t length)
            {
                if (!error)
                {
                    receive_response(length);
                }
                else
                {
                    std::cout << "Write failed: " << error.message() << "\n";
                }
            });
    }

    void receive_response(std::size_t length)
    {
        boost::asio::async_read(socket_,
            boost::asio::buffer(reply_, length),
            [this](const boost::system::error_code& error, std::size_t length)
            {
                if (!error)
                {
                    std::cout << "Reply: ";
                    std::cout.write(reply_, length);
                    std::cout << "\n";
                }
                else
                {
                    std::cout << "Read failed: " << error.message() << "\n";
                }
            });
    }

    boost::asio::ssl::stream<tcp::socket> socket_;
    char request_[max_length];
    char reply_[max_length];
};

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 3)
        {
            std::cerr << "Usage: client <host> <port>\n";
            return 1;
        }

        boost::asio::io_context io_context;

        tcp::resolver resolver(io_context);
        auto endpoints = resolver.resolve(argv[1], argv[2]);

        boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
        ctx.load_verify_file("rootca.crt");

        client c(io_context, ctx, endpoints);

        io_context.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

 

$ g++ client.cpp -lboost_system -lcrypto -lssl -lpthread -o client.exe

 

 

4. 결과

 

 

 

 

와이어샤크로 찍어봐도 정상적으로 hello 패킷과 인증서교환을 하는걸 확인할 수 있습니다.

 

※해당 sample코드에서 서버는 user인증서를 사용하고 클라이언트는 rootca인증서를 사용했는데,

이는 rootca로 만든 user.crt가 클라이언트 측에서 정상적으로 작동되는지 확인하기 위해 저렇게 바뀐겁니다.

ssl을 이용해 http통신을 하고싶은 경우에는 서버에서 rootca를 사용하시면 됩니다.

 

 

참고 레퍼런스

1. samplecode 참조 : https://nextbigthings.info/secured-tls-connection-using-boost-asio-and-openssl-for-windows/

 

2. boost.asio_ssl : https://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/reference/ssl__context.html

 

3. 인증서 발급 참조 : https://heodolf.tistory.com/94

 

4. logjam : https://blog.alyac.co.kr/330

 

 

 

 

반응형