1. 背景 1.1 問題点 最近の製品テスト レポートでは、PKI ベースの認証方法の使用が推奨されていました。製品には HTTPS が実装されているため、中間者攻撃に対処するために双方向認証を使用する必要があると議論し、そう考えました。 「情報セキュリティエンジニアリング」というコースで双方向認証について学びましたが、問題が 2 つあります。 1 つ目は、最終的なコース設計クライアントはブラウザであり、サーバーは Tomcat であることです。双方向認証では、両方の構成のみが必要であり、実際のコードの実装は必要ありません。 2 つ目は、コースにも双方向認証に近い実装コードがありますが、当時は Java + JCE 環境でしたが、現在は C+++OpenSSL 環境を使用する必要があります。全体的な意味は確かに似ていますが、具体的な機能やパラメータには依然として多くの違いがあります。 つまり、現在私たちが持っているのは、証明書生成のアイデア + 双方向認証を実装するというアイデアです。読者は、証明書、SSL/TSL、ソケット プログラミングなど、いくつかの概念について基本的な理解があることを前提としていますが、この記事ではそれらの概念については詳しく説明しません。 これを踏まえて、この記事で解決すべき問題は、openssl が具体的にどのように証明書を生成するか、openssl が双方向認証をどのように実装するか、ということです。 双方向認証のポイントは以下の関数です (サーバーとクライアントの両方で同じ)。残りについては、コードコメントを参照してください。 SSL_CTX_set_verify----双方向認証を有効にするように設定します SSL_CTX_load_verify_locations - 信頼されたルート証明書をロードする SSL_CTX_use_certificate_file----独自の証明書をロードする SSL_CTX_use_PrivateKey_file----独自の秘密鍵をロードする SSL_get_verify_result ---- 実際に検証するには、この関数を呼び出す必要があります。そうしないと、前の 4 つの関数は単なる構成となり、双方向の検証は実行されません。 2. 双方向認証手順の実装 2.1 opensslをインストールしてAPIを開発する apt-get で libssl-dev をインストールします 2.2 サーバーコード #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <文字列.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> #include <arpa/inet.h> #include <openssl/ssl.h> #include <openssl/err.h> #MAXBUF 1024 を定義します void 証明書を表示(SSL * ssl) { X509 *証明書; 文字 *行; 証明書 = SSL_get_peer_certificate(ssl); // SSL_get_verify_result() が重要なポイントです。SSL_CTX_set_verify() は、有効にするかどうかを設定するだけで、認証は実行しません。この関数を呼び出すと、実際に証明書が検証されます。// 検証に失敗した場合、プログラムは例外をスローして接続を終了します if (SSL_get_verify_result (ssl) == X509_V_OK) { printf("証明書の検証に合格しました\n"); } (証明書がNULLではない場合){ printf("デジタル証明書情報:\n"); 行 = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0); printf("証明書: %s\n", 行); フリー(行); 行 = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0); printf("発行者: %s\n", 行); フリー(行); X509_free(証明書); } それ以外 printf("証明書情報がありません!\n"); } int main(int argc, char **argv) { int sockfd、新しいfd; socklen_t 長さ; 構造体 sockaddr_in my_addr、their_addr; 符号なし整数 myport、lisnum; char buf[MAXBUF + 1]; SSL_CTX *ctx; (argv[1])の場合 myport = atoi(argv[1]); それ以外 マイポート = 7838; (argv[2])の場合 lisnum = atoi(argv[2]); それ以外 リスナム = 2; /* SSL ライブラリの初期化 */ SSL_library_init(); /* すべての SSL アルゴリズムをロードします */ すべてのアルゴリズムを追加します。 /* すべての SSL エラー メッセージをロードします */ SSL_load_error_strings(); /* SSL V2 および V3 標準と互換性のある方法で SSL_CTX (SSL コンテンツ テキスト) を生成します */ ctx = SSL_CTX_new(SSLv23_server_method()); /* SSLv2_server_method() または SSLv3_server_method() を使用して、V2 または V3 標準を個別に指定することもできます */ ctx == NULLの場合{ ERR_print_errors_fp(標準出力); 終了(1); } // 双方向検証 // SSL_VERIFY_PEER --- 証明書認証が必要であり、証明書が存在しない場合でも要求を通過させます // SSL_VERIFY_FAIL_IF_NO_PEER_CERT --- クライアントに証明書の提供を要求しますが、証明書が存在しない場合でも要求を通過させます SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); // 信頼されたルート証明書を設定します if (SSL_CTX_load_verify_locations(ctx, "ca.crt",NULL)<=0){ ERR_print_errors_fp(標準出力); 終了(1); } /* クライアントに送信するために使用されるユーザーのデジタル証明書を読み込みます。 証明書には公開鍵が含まれています*/ SSL_CTX_use_certificate_file(ctx, argv[3], SSL_FILETYPE_PEM) <= 0の場合{ ERR_print_errors_fp(標準出力); 終了(1); } /* ユーザーの秘密鍵を読み込む */ SSL_CTX_use_PrivateKey_file(ctx, argv[4], SSL_FILETYPE_PEM) <= 0の場合{ ERR_print_errors_fp(標準出力); 終了(1); } /* ユーザーの秘密鍵が正しいかどうかを確認します*/ if (!SSL_CTX_check_private_key(ctx)) { ERR_print_errors_fp(標準出力); 終了(1); } /* ソケットリスナーを開く */ ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) の場合 { perror("ソケット"); 終了(1); } それ以外 printf("ソケットが作成されました\n"); bzero(&my_addr, sizeof(my_addr)); my_addr.sin_family = PF_INET; my_addr.sin_port = htons(myport); my_addr.sin_addr.s_addr = INADDR_ANY; (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr))の場合 == -1) { perror("バインド"); 終了(1); } それ以外 printf("バインドされました\n"); (sockfd、lisnum)== -1の場合{ perror("聞く"); 終了(1); } それ以外 printf("リッスン開始\n"); 一方(1){ TLS 1.2 以降 len = sizeof(構造体sockaddr); /* クライアントの接続を待機中*/ ((new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len))の場合 == -1) { perror("受け入れる"); 終了(エラー番号); } それ以外 printf("サーバー: %s、ポート %d、ソケット %d から接続を取得しました\n", inet_ntoa(そのアドレス.sin_addr)、ntohs(そのアドレス.sin_port)、 新しい_fd); /* ctx に基づいて新しい SSL を生成する */ ssl = SSL_new(ctx); /* 接続されたユーザーのソケットを SSL に追加します */ SSL_set_fd(ssl, new_fd); /* SSL接続を確立する */ SSL_accept(ssl) == -1の場合{ perror("受け入れる"); new_fd を閉じます。 壊す; } 証明書を表示します(ssl); /* 新しい接続ごとにデータの送受信処理を開始します*/ bzero(バッファ、MAXBUF + 1); strcpy(buf, "サーバー->クライアント"); /* クライアントにメッセージを送信する */ len = SSL_write(ssl, buf, strlen(buf)); 長さ<= 0の場合{ printf("メッセージ '%s' の送信に失敗しました! エラーコードは %d、エラーメッセージは '%s' です\n", buf, errno, strerror(エラー番号) 終了する; } それ以外 printf("メッセージ '%s' は正常に送信されました。合計 %d バイトが送信されました!\n", buf, len); bzero(バッファ、MAXBUF + 1); /* クライアントからのメッセージを受信 */ len = SSL_read(ssl, buf, MAXBUF); 長さが0より大きい場合 printf("メッセージの受信に成功しました: '%s'、%d バイトのデータ\n", buf, len); それ以外 printf("メッセージの受信に失敗しました! エラーコードは %d、エラーメッセージは '%s' です\n", エラー番号、strerror(エラー番号)。 /* 新しい接続ごとにデータの送受信の終了を処理する*/ 仕上げる: /* SSL接続を閉じる */ SSL_shutdown(SSL); /* SSL を解放する */ SSL_free(SSL) を使用します。 /* ソケットを閉じる */ new_fd を閉じます。 } /* リスニングソケットを閉じる */ 閉じる(sockfd); /* CTX を解放する */ SSL_CTX_free(ctx); 0を返します。 } 2.3 クライアントコード #include <stdio.h> #include <文字列.h> #include <errno.h> #include <sys/socket.h> #include <resolv.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <openssl/ssl.h> #include <openssl/err.h> #MAXBUF 1024 を定義します void 証明書を表示(SSL * ssl) { X509 *証明書; 文字 *行; 証明書 = SSL_get_peer_certificate(ssl); // SSL_get_verify_result() が重要なポイントです。SSL_CTX_set_verify() は、有効にするかどうかを設定するだけで、認証は実行しません。この関数を呼び出すと、実際に証明書の認証が検証されます。// 検証に失敗した場合、プログラムは例外をスローして接続を終了します if (SSL_get_verify_result (ssl) == X509_V_OK) { printf("証明書の検証に合格しました\n"); } (証明書がNULLではない場合){ printf("デジタル証明書情報:\n"); 行 = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0); printf("証明書: %s\n", 行); フリー(行); 行 = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0); printf("発行者: %s\n", 行); フリー(行); X509_free(証明書); } それ以外 printf("証明書情報がありません!\n"); } int main(int argc, char **argv) { int sockfd、長さ; 構造体 sockaddr_in 宛先; char バッファ[MAXBUF + 1]; SSL_CTX *ctx; TLS 1.2 以降 (引数が5の場合){ printf("パラメータのフォーマットエラーです。正しい使用法は次のとおりです:\n\t\t%s IP アドレス ポート\n\t例:\t%s 127.0.0.1 80\nこのプログラムは、特定の" 「IP アドレスのサーバーは、特定のポートで最大 MAXBUF バイトのメッセージを受信します」 argv[0], argv[0]); 終了(0); } /* SSL ライブラリの初期化、ssl-server.c コードを参照*/ SSL_library_init(); すべてのアルゴリズムを追加します。 SSL_load_error_strings(); ctx = SSL_CTX_new(SSLv23_client_method()); ctx == NULLの場合{ ERR_print_errors_fp(標準出力); 終了(1); } // 双方向検証 // SSL_VERIFY_PEER --- 証明書認証が必要であり、証明書が存在しない場合でも要求を通過させます // SSL_VERIFY_FAIL_IF_NO_PEER_CERT --- クライアントに証明書の提供を要求しますが、証明書が存在しない場合でも要求を通過させます SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); // 信頼されたルート証明書を設定します if (SSL_CTX_load_verify_locations(ctx, "ca.crt",NULL)<=0){ ERR_print_errors_fp(標準出力); 終了(1); } /* クライアントに送信するために使用されるユーザーのデジタル証明書を読み込みます。 証明書には公開鍵が含まれています*/ SSL_CTX_use_certificate_file(ctx, argv[3], SSL_FILETYPE_PEM) <= 0の場合{ ERR_print_errors_fp(標準出力); 終了(1); } /* ユーザーの秘密鍵を読み込む */ SSL_CTX_use_PrivateKey_file(ctx, argv[4], SSL_FILETYPE_PEM) <= 0の場合{ ERR_print_errors_fp(標準出力); 終了(1); } /* ユーザーの秘密鍵が正しいかどうかを確認します*/ if (!SSL_CTX_check_private_key(ctx)) { ERR_print_errors_fp(標準出力); 終了(1); } /* TCP通信用のソケットを作成する */ ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) の場合 { perror("ソケット"); 終了(エラー番号); } printf("ソケットが作成されました\n"); /* サーバー(相手側)のアドレスとポート情報を初期化します*/ bzero(&dest, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(atoi(argv[2])); inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0の場合{ perror(argv[1]); 終了(エラー番号); } printf("アドレスが作成されました\n"); /* サーバーに接続 */ (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0) の場合 { perror("接続 "); 終了(エラー番号); } printf("サーバーが接続されました\n"); /* ctx に基づいて新しい SSL を生成する */ ssl = SSL_new(ctx); SSL_set_fd(ssl, sockfd); /* SSL接続を確立する */ SSL_connect(ssl) == -1の場合 ERR_print_errors_fp(標準エラー出力); それ以外 { printf("%s 暗号化で接続しました\n", SSL_get_cipher(ssl)); 証明書を表示します(ssl); } /* 相手から送信されたメッセージを受信し、最大 MAXBUF バイトを受信します*/ bzero(バッファ、MAXBUF + 1); /* サーバーからメッセージを受信 */ len = SSL_read(ssl, バッファ, MAXBUF); 長さが0より大きい場合 printf("メッセージを正常に受信しました: '%s'、合計 %d バイトのデータ\n", バッファ、長さ); それ以外 { プリント ("メッセージの受信に失敗しました! エラー コードは %d、エラー メッセージは '%s'\n", エラー番号、strerror(エラー番号)。 終了する; } bzero(バッファ、MAXBUF + 1); strcpy(バッファ、「クライアントから->サーバーへ」); /* サーバーにメッセージを送信します */ len = SSL_write(ssl, バッファ, strlen(バッファ)); 長さが 0 未満の場合 プリント ("メッセージ '%s' の送信に失敗しました! エラー コードは %d、エラー メッセージは '%s' です\n", バッファ、errno、strerror(errno)); それ以外 printf("メッセージ '%s' は正常に送信されました。合計 %d バイトが送信されました!\n", バッファ、長さ); 仕上げる: /* 接続を閉じる */ SSL_shutdown(SSL); SSL_free(SSL) を使用します。 閉じる(sockfd); SSL_CTX_free(ctx); 0を返します。 } 2.4 証明書の生成 3つの点に注意する まず、秘密鍵の暗号化パスワード (-passout パラメータ) を自分のパスワードに変更することに注意してください。次の手順はすべて、-passout パラメータを使用して秘密鍵を生成します。-nodes パラメータを使用する場合は、最後の手順「暗号化された RSA キーを暗号化されていない RSA キーに変換する」を実行する必要はありません。 第二に、証明書と鍵は、直接的なワンステップ生成とステップバイステップ生成の 2 つの形式で提供されます。2 つの形式は同等です。ここでは、直接生成形式を使用します (ステップバイステップ生成形式はコメント化されています)。 3 番目に、証明書情報を必ず自分の組織の情報に変更してください。証明書番号のパラメータの意味は次のとおりです。 C-----国名 ST----州または県名 L----市(地域名) O----会社(組織名) OU----部門(組織単位名) CN----一般名 emailAddress----メールアドレス # CA 証明書とキーの生成方法 1 - CA キーと自己署名証明書を直接生成する # 今後、秘密キー ファイル ca_rsa_private.pem を読み取るときにパスワードを入力する必要がない場合、つまり、秘密キーを暗号化して保存しない場合は、-passout pass:123456 を -nodes に置き換えます openssl req -newkey rsa:2048 -passout pass:123456 -keyout ca_rsa_private.pem -x509 -days 365 -out ca.crt -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CA/[email protected]" # CA 証明書とキーの生成方法 2 - CA キーと自己署名証明書を段階的に生成します。 # openssl genrsa -aes256 -passout pass:123456 -out ca_rsa_private.pem 2048 # openssl req -new -x509 -days 365 -key ca_rsa_private.pem -passin pass:123456 -out ca.crt -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CA/[email protected]" # サーバー証明書とキーの生成方法 1 - 署名するサーバーキーと証明書を直接生成する # 今後、秘密鍵ファイル server_rsa_private.pem を読み取るときにパスワードを入力する必要がない場合、つまり秘密鍵を暗号化して保存しない場合は、-passout pass:server を -nodes に置き換えます openssl req -newkey rsa:2048 -passout pass:server -keyout server_rsa_private.pem -out server.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=SERVER/[email protected]" # サーバー証明書とキーの生成方法 2 -- 署名するサーバーキーと証明書を段階的に生成します。# openssl genrsa -aes256 -passout pass:server -out server_rsa_private.pem 2048 # openssl req -new -key server_rsa_private.pem -passin pass:server -out server.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=SERVER/[email protected]" # CA 証明書とキーを使用してサーバー証明書に署名します。 openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca_rsa_private.pem -passin pass:123456 -CAcreateserial -out server.crt # 暗号化された RSA キーを暗号化されていない RSA キーに変換して、読み取るたびに復号化パスワードが必要にならないようにします。# パスワードは、秘密キー ファイルを生成するときに設定されたパスワードと、秘密キー ファイルを読み取るときに入力するパスワードです。たとえば、ここに「server」と入力します。 openssl rsa -in server_rsa_private.pem -out server_rsa_private.pem.unsecure # クライアント証明書とキーを生成する方法 1: 署名するクライアントキーと証明書を直接生成する # 今後、秘密キーファイル client_rsa_private.pem を読み取るときにパスワードを入力する必要がない場合、つまり秘密キーを暗号化して保存しない場合は、-passout pass:client を -nodes に置き換えます openssl req -newkey rsa:2048 -passout pass:client -keyout client_rsa_private.pem -out client.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CLIENT/[email protected]" # クライアント証明書とキーを生成する方法 2: 署名するクライアントキーと証明書を段階的に生成します。 # openssl genrsa -aes256 -passout pass:client -out client_rsa_private.pem 2048 # openssl req -new -key client_rsa_private.pem -passin pass:client -out client.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CLIENT/[email protected]" # CA 証明書とキーを使用してクライアント証明書に署名します。 openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca_rsa_private.pem -passin pass:123456 -CAcreateserial -out client.crt # 暗号化された RSA キーを暗号化されていない RSA キーに変換して、読み取るたびに復号化パスワードが必要にならないようにします。# パスワードは、秘密キー ファイルを生成するときに設定されたパスワードと、秘密キー ファイルを読み取るときに入力されるパスワードです。たとえば、ここに「client」と入力します。 openssl rsa -in client_rsa_private.pem -out client_rsa_private.pem.unsecure 2.5 開発環境の構成 オペレーティング システム ----kali-roaling。あくまでも現在の仮想マシンを使用するためのものです。Ubuntu、CentOS などを使用する場合でも違いはありません。 IDE----Eclipse。ターミナルで直接コンパイルが失敗した場合は、それ以上調査しません。Eclipse でコンパイルしても問題がない場合は、Eclipse を使用します。 2.5.1 同じアクティブディレクトリに2つのプロジェクトを作成する myclient1----クライアントコードを置くためのsrcフォルダを作成する myserver1----サーバーコードを保存するsrcフォルダを作成する (他のディレクトリは自動的に生成されるか、コンパイル後に自動的に生成されるため、心配する必要はありません。プロジェクトでエラーが報告された場合は、Eclipse またはオペレーティング システムを数回再起動してみてください) 2.5.2 SSLと暗号の指定 プロジェクト フォルダーを右クリックし、[プロパティ] をクリックして、SSL ライブラリと暗号ライブラリのディレクトリを指定します。そうしないと、コンパイル時に SSL が見つかりません。両方のプロジェクトを設定する必要があります 2.5.3 コンパイル Ctrl+Bショートカットキーを使用してコンパイルすると、Eclipseはすべてのプロジェクトをコンパイルします。 2.5.4 証明書のコピー 以前に生成したCA証明書(ca.crt)、クライアント証明書(client.crt)、およびクライアントの暗号化されていない秘密鍵ファイル()をmyclient1プロジェクトのデバッグディレクトリにコピーします。 以前に生成したCA証明書、サーバー証明書、およびサーバー側の暗号化されていない秘密鍵ファイルをmyclient1プロジェクトのデバッグディレクトリにコピーします。 2.5.5 プログラムを実行する 最初にサーバーを実行し、次にクライアントを実行します
操作結果は次のとおりです。サーバー: クライアント: 以上がこの記事の全内容です。皆様の勉強のお役に立てれば幸いです。また、123WORDPRESS.COM を応援していただければ幸いです。 以下もご興味があるかもしれません:
|
>>: JavaScript の知識: コンストラクタも関数である
目次1. 異なるビューポートを取得する方法2. 水平画面と垂直画面のJavaScript検出3. 水...
序文テストを行う際、大量のデータによる負荷に耐えるプロジェクトの能力をテストするために、通常はテスト...
序文開発プロセスにおいて、変数の定義は非常に頻繁かつ基本的なタスクです。変数の使用シナリオと範囲に応...
通常、CSS セレクターは上から下に選択し、親要素を介して子要素を選択します。では、子要素を介して親...
共通点: DIV タグと SPAN タグは、コンテンツ全体を非表示にしたり、コンテンツ全体を移動した...
1. Linux .NET Core の紹介Microsoft は常に自社のプラットフォームに対して...
この記事では、jQueryプラグインを使用してマインスイーパゲームを実装する2番目の記事を参考までに...
フロントエンド プロジェクトとバックエンド プロジェクトは分離されており、フロントエンドとバックエン...
目次概要本日正午、開発およびテスト環境の MySQL サービスで接続数が多すぎるというエラーが報告さ...
言語では、DSL を実装するためにマクロがよく使用されます。マクロを使用すると、開発者は JSX 構...
まずmysqlの圧縮バージョンをダウンロードします。公式ダウンロードアドレスは123WORDPRES...
Vue では、ほとんどの場合、テンプレートを使用して HTML を作成することを推奨しています。ただ...
シェルで変数が空かどうかを判断する方法シェルプログラミングでは、パラメータのエラーチェック項目に、変...
この記事の例では、商品詳細ページ機能を実現するためのVueの商品タブの具体的なコードを参考までに共有...
背景: SAP ECC サーバーをインストールし、XP をプレインストールしたいと考えています。XP...