컴퓨터/운영체제(주로 리눅스)

[openssl] Server - Client 인증서 검증과 통신 소스 코드

REAKWON 2023. 10. 3. 16:10

CA를 통한 인증서 검증과 Server - Client 통신

CA를 활용한 클라이언트 인증서 검증을 하기 위해서 CA의 인증서와 CA 서명된 Client인증서, Server 인증서가 필요합니다. 아래의 포스팅을 참고하면 CA, Server, Client의 키 쌍들과 CA 인증서와 Server, Client 인증서를 생성할 수 있습니다. 

https://reakwon.tistory.com/239

 

openssl CA를 통한 Server - Client 인증서 검증 및 대칭키 공유 과정

CA 인증서로 상대방의 인증서 확인 방법 디렉토리 구조 디렉토리 구조는 아래와 같으며 각각 인증서를 생성하는 과정에서 여러 키와 인증서가 생성이 될 겁니다. # ls CA Client Server CA : Root CA로 CA

reakwon.tistory.com

이렇게 하면 디렉토리의 구조는 아래와 같게 됩니다. 

# ls
Client  RootCA  Server

 

Server/server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#include <openssl/rsa.h>    
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
 
#define CHK_NULL(x) if((x) == NULL) exit(1);
#define CHK_ERR(err, s) if((err) == -1) { perror(s); exit(1); }
#define CHK_SSL(err) if((err) == -1) { ERR_print_errors_fp(stderr); exit(2); }
static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx){

    char *str;
    X509 *cert = X509_STORE_CTX_get_current_cert(ctx);

    if (cert) {
        printf("Cert depth %d\n", X509_STORE_CTX_get_error_depth(ctx));
                
        str = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
        CHK_NULL(str);
        printf("\t subject : %s\n", str);
        OPENSSL_free(str);

        str = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
        CHK_NULL(str);
        printf("\t issuer : %s\n", str);
        OPENSSL_free(str);
    }

    return preverify_ok;
}
 
int main(void){

    int err, listen_fd, socket_fd;
    struct sockaddr_in server, client;
    size_t client_len;
   
    // SSL 관련 객체 
    SSL_CTX *ctx;
    SSL *ssl;
    X509 *client_cert;
    SSL_METHOD  *meth;
    
    char *str;
    char buf[128];
   
    printf("Server start!\n");

    // SSL 초기 셋팅 
    SSL_load_error_strings();
    SSLeay_add_ssl_algorithms();
    meth = SSLv23_server_method();
    ctx = SSL_CTX_new(meth);  
   
    if(!ctx) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 서버의 인증서 설정 
    if(SSL_CTX_use_certificate_file(ctx, "./Server.crt", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 서버의 개인키 설정 
    if(SSL_CTX_use_PrivateKey_file(ctx, "./privkey-Server.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }

    // 개인키 사용 가능성 체크
    if(!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "private key is not matched to public key.\n");
        exit(-1);
    }
    // 사용할 CA의 인증서 설정
    if(!SSL_CTX_load_verify_locations(ctx, "../RootCA/CA.crt", NULL)) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }

    // Client의 인증서를 검증하기 위한 설정, CA는 하나만 있다고 가정-> depth = 1
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, verify_callback);
    SSL_CTX_set_verify_depth(ctx, 1); 
   
    // TCP socket 생성 이후 bind, listen, accept
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    CHK_ERR(listen_fd, "socket");
   
    memset(&server, 0x00, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(12340); 
 
    err = bind(listen_fd, (struct sockaddr*)&server, sizeof(server));
    CHK_ERR(err, "bind");
   
    err = listen(listen_fd, 5);
    CHK_ERR(err, "listen");
    
    client_len = sizeof(client);
    socket_fd = accept(listen_fd, (struct sockaddr*)&client, &client_len);
    CHK_ERR(socket_fd, "accept");
    close(listen_fd);
    
    // SSL 세션 생성 
    ssl = SSL_new(ctx);
    CHK_NULL(ssl);

    // SSL 접속 대기 , SSL_accept 완료 = SSL handshake 완료
    SSL_set_fd(ssl, socket_fd);
    err = SSL_accept(ssl);    
    CHK_SSL(err);
   
    // 사용하는 Cipher 
    printf("SSL is using cipher %s\n", SSL_get_cipher(ssl));
   
    // 클라이언트의 인증서를 받고 검증
    client_cert = SSL_get_peer_certificate(ssl);
    CHK_NULL(client_cert);


    if(SSL_get_verify_result(ssl) == X509_V_OK){
        printf("verify cert OK\n");
        X509_free(client_cert);
    } else {
        printf("verify cert Failed\n");
    }

   
    // 클라이언트로부터 메시지 수신
    err = SSL_read(ssl, buf, sizeof(buf)-1);
    CHK_SSL(err);
    buf[err] = 0;
    printf("From Client '%s'\n", buf);
   
    err = SSL_write(ssl, "Hello, Client!", strlen("Hello, Client!"));
    CHK_SSL(err);
   
    //자원 해제 
    close(socket_fd);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
   
    return(0);
}

  • SSL Context : Context는 cipher, TLS 버전, 인증서 및 암호 파라미터들의 모음입니다. 이를 기반으로 SSL session이 생성됩니다.  그러니까 인증서나 키 등의 세션들이 공통적으로 사용하는 데이터를 미리 설정해둠에 따라 세션이 만들어질때마다 이런 데이터를 미리 준비하는 과정이 없어집니다. 그 말은 즉, 시간이 줄어든다는 뜻입니다.
  • SSL Session : Session은 Server와 Client간의 연결이 실제 이루어진 것을 의미합니다. 이 세션에서 데이터의 전송이 이루어집니다. Server나 Client간의 세션이 없다면 만들어지고, 만들어져있다면 세션을 다시 재활용하는 것도 가능합니다. 

인증서 검증 

Client의 인증서를 검증하기 위해서는 SSL_CTX_set_verify, 그리고 SSL_CTX_set_verify_depth를 지정해야합니다. SSL_CTX_set_verify에 SSL_VERIFY_PEER 옵션 지정 후 verify_callback 함수를 통해서 커스텀한 검증을 할 수 있는데, verify_callback함수에 preverify_ok라는 인자는 이전에 검증 과정의 결과를 알려줍니다. depth에 따라서 여러번 호출이 됩니다.

위 verify_callback 함수는 SSL_CTX_set_verify_depth에 의해서 depth를 지정해 줄 수 있는데, depth에 따라 인증서를 어느 수준까지 인증할 것이냐를 정의해줄 수 있습니다. 예를 들어 depth가 1이면 Client(depth 0)와 그 인증서를 발행한 CA(depth 1)의 인증서를 검증하게 되는 것이구요. depth가 2일 경우에는 위 단계에서 CA의 인증서를 발생한 상위의 CA(depth 2)의 인증서를 검증합니다.

이후 잘 검증되었는지를 확인하는 SSL_get_verify_result 함수를 이용해 문제없이 인증서가 검증되었는지 확인할 수 있습니다.

 

Client/client.c

#include <stdio.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
 
#define CHK_NULL(x) if((x) == NULL) exit(1);
#define CHK_ERR(err, s) if((err) == -1) { perror(s); exit(1); }
#define CHK_SSL(err) if((err) == -1) { ERR_print_errors_fp(stderr); exit(2); }
 
int main(void)
{
    int err, socket_fd;
    struct sockaddr_in server;
   
    SSL_CTX *ctx;
    SSL *ssl;
    X509 *server_cert;
    char *str;
    char buf[128];
    SSL_METHOD    *method;
   
    //초기 세팅
    SSL_load_error_strings();
    SSLeay_add_ssl_algorithms();
    method = SSLv23_client_method();
    ctx = SSL_CTX_new(method);
    CHK_NULL(ctx);
   
    // Context에서 사용할 인증서 설정
    if(SSL_CTX_use_certificate_file(ctx, "./Client.crt", SSL_FILETYPE_PEM) <= 0) {    
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // Context에서 사용할 개인키 설정
    if(SSL_CTX_use_PrivateKey_file(ctx, "./privkey-Client.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 개인키 사용가능성 확인
    if(!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "Private key is not matched to public key\n");
        exit(-1);
    }
   
    // Socket의 Connect까지 설정하는 과정
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    CHK_ERR(socket_fd, "socket error ");
   
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    server.sin_port = htons(12340); 
   
    err = connect(socket_fd, (struct sockaddr*)&server, sizeof(server));
    CHK_ERR(err, "connect error ");
   
    //SSL 세션 객체 생성
    ssl = SSL_new(ctx); 
    CHK_NULL(ssl);
   
    //SSL객체에 socket fd를 설정
    SSL_set_fd(ssl, socket_fd);
    err = SSL_connect(ssl); 
    CHK_NULL(err);
   
    printf("SSL is using cipher %s\n", SSL_get_cipher(ssl));
   
    // 서버의 인증서를 가져옴
    server_cert = SSL_get_peer_certificate(ssl);
    CHK_NULL(server_cert);

    printf("Server certificate:\n");
   
    //인증서의 몇가지 정보를 출력
    str = X509_NAME_oneline(X509_get_subject_name(server_cert), 0, 0);
    CHK_NULL(str);
    printf("\t subject: %s\n", str);
    OPENSSL_free(str);
   
    /* 인증서의 issuer를 출력한다. */
    str = X509_NAME_oneline(X509_get_issuer_name(server_cert), 0, 0);
    CHK_NULL(str);
    printf("\t issuer: %s\n", str);
    OPENSSL_free(str);
   
    X509_free(server_cert);
   
    // 서버에게 메시지를 전송
    err = SSL_write(ssl, "Hello World!", strlen("Hello World!"));
    CHK_SSL(err);
   
    // 서버로부터 메시지 수신
    err = SSL_read(ssl, buf, sizeof(buf)-1);
    CHK_SSL(err);
    
    buf[err] = 0;
    printf("From Server : '%s'\n", buf);

   
    // 세션 종료 및 ssl, ctx 자원 해제
    SSL_shutdown(ssl);   
    close(socket_fd);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
   
    return 0;
}

 

결과화면

인증서를 대충만들었기 때문에 구분이 잘 안가실텐데, 아래의 depth 1은 CA 인증서, depth 0은 Client 인증서의 정보를 나타냅니다. CA의 인증서를 보면 subject와 issuer가 같은 것을 알 수 있죠. 자기 자신의 인증서를 자신이 사이닝했습니다.

Server
# ./server
Server start!
Cert depth 1
    subject : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
    issuer : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
Cert depth 0
    subject : /C=KR/ST=Some-State/O=Internet Widgits Pty Ltd/CN=Client/emailAddress=client@dd.com
    issuer : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
SSL is using cipher TLS_AES_256_GCM_SHA384
verify cert OK
From Client 'Hello World!'
Client
# ./client
SSL is using TLS_AES_256_GCM_SHA384
Server certificate:
    subject: /C=KR/ST=Some-State/O=Internet Widgits Pty Ltd/CN=Server/emailAddress=server@dd.com
    issuer: /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
From Server : 'Hello, Client!'

 

참고한 자료

SSL Server - Client 코드 : http://pchero21.com/?p=603

CA, Server, Client 인증서 생성 : https://www.cs.toronto.edu/~arnold/427/19s/427_19S/tool/ssl/notes.pdf

인증서 검증 : https://tribal1012.tistory.com/m/213

 

반응형