CA를 통한 인증서 검증과 Server - Client 통신
CA를 활용한 클라이언트 인증서 검증을 하기 위해서 CA의 인증서와 CA 서명된 Client인증서, Server 인증서가 필요합니다. 아래의 포스팅을 참고하면 CA, Server, Client의 키 쌍들과 CA 인증서와 Server, Client 인증서를 생성할 수 있습니다.
https://reakwon.tistory.com/239
이렇게 하면 디렉토리의 구조는 아래와 같게 됩니다.
# 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
'컴퓨터 > 운영체제(주로 리눅스)' 카테고리의 다른 글
openssl CA를 통한 Server - Client 인증서 검증 및 대칭키 공유 과정 (0) | 2023.10.02 |
---|---|
[리눅스] Datagram Socket 통신 - 간단 server - client C 소스 코드 (0) | 2023.09.24 |
[LINUX] epoll의 개념과 이를 활용한 다중입출력 방식의 서버, 클라이언트 (0) | 2023.09.23 |
[리눅스] 코드로 이해하는 저장된 사용자 ID(Saved UID)가 있는 이유 (0) | 2023.08.16 |
[리눅스] 환경 변수 개념과 환경 변수를 다루는 방법 - 환경 변수는 어쩌면 맛있는게 아닐까? (1) | 2023.08.03 |