0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SSL/TLSプロトコルをゆるく実装

Last updated at Posted at 2024-09-21

モチベーション

情報セキュリティマネジメント試験の勉強をしていたら, SSLプロトコルとやらが出てきた

HTTPS通信で利用されているっぽいっくらいの知識だったので, 改めて勉強してみようと思った.

新しいチームでTypeScriptを書き始めたので TypeScriptの勉強も兼ねて実装してみる

TL;DR

SSL/TLSプロトコルはhttps通信で利用されているプロトコル

サーバの認証と通信の暗号化ができる

サーバの認証は, SSLサーバ証明書をブラウザで検証

通信の暗号化には, セッション鍵交換方式を利用

まずSSL/TLSプロトコルの流れ

[Client] サーバに接続を要求する

[Server] 公開鍵と公開鍵に対応する秘密鍵を生成する

[Server] クライアントに公開鍵付きのSSLサーバ証明書を送付する

[Client] ブラウザで, SSLサーバ証明書を検証 

[Client] 共通鍵(セッションキー)を作成

[Client] SSLサーバ証明書に付録された公開鍵で共通鍵を暗号化し, サーバに送信する

[Server] 暗号化された共通鍵を, 秘密鍵を復号する

[Server] 復号した共通鍵を利用して, 通信を開始する

※ [4]SSLサーバ証明書の認証について

本来はブラウザに搭載されているルート証明書を利用して認証を行うが, 今回は実装しない

理由: ルート証明書を利用した検証を行うには, 信頼できる第三者のCA(認証局)で作成する必要があるが, お金がかかる. そこまではしたくない

※ [2]〜[7] までがセッション鍵交換方式

以上を踏まえて, 上記の番号に沿って実装を始める

準備

秘密鍵と公開鍵の準備 (サーバ側で生成)

// 秘密鍵の生成
openssl genrsa -out key.pem 2048

// 自己署名証明書(公開鍵)の生成
openssl req -new -x509 -key key.pem -out cert.pem -days 365

サーバ側の準備

秘密鍵と自己著名証明書(公開鍵)の読み込み

ソケット通信の開始

// 秘密鍵と証明書(公開鍵)の読み込み
const privateKey = fs.readFileSync('key.pem', 'utf8');
const certificate = fs.readFileSync('cert.pem', 'utf8');

const server = net.createServer((socket) => {

})

// サーバーを開始
server.listen(8443, () => {
    console.log('Server running on port 8443');
})

クライアント側の準備

ソケット通信の開始

// 自己署名証明書と比較するための読み込み. 本来はルート証明書を利用する
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

})

[1]: [Client] サーバに接続を要求する

今回はソケットに”SSL”と書き込むことで, SSL通信を開始する

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {
    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');
})

[2]: [Server] 公開鍵と公開鍵に対応する秘密鍵を生成する

※ 準備の段階で作成済み

[3]: [Server] クライアントに公開鍵付きのSSLサーバ証明書を送付する

クライアントから”SSL”というメッセージを受け取ったらSSLサーバ証明書を送信する

// 秘密鍵と証明書(公開鍵)の読み込み
const privateKey = fs.readFileSync('key.pem', 'utf8');
const certificate = fs.readFileSync('cert.pem', 'utf8');

const server = net.createServer((socket) => {
    if (initMessage === "SSL") {
        // クライアントにSSLサーバ証明書を送付
        socket.write(certificate)
})

[4]: [Client] ブラウザで, SSLサーバ証明書を検証 

サーバから送信されたSSLサーバ証明書を検証する.

※ 本来はルート証明書を利用する

// SSLサーバー証明書を信頼する自己署名証明書と比較するための読み込み
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');

    // サーバーから証明書を受信
    client.once('data', (data) => {
        
        // サーバから送信されたSSL証明書
        const receivedCertificate = data.toString()

        // ブラウザのルート証明書でSSL証明書を検証と仮定
        if (receivedCertificate == clientCertificate) {
            // 認証に成功した場合の処理
        }
        
    })
})

[5]: [Client] 共通鍵(セッションキー)を作成

SSLサーバ証明書の検証に成功した場合, 共通鍵(セッションキー)を作成する

const ALGORITHM = 'aes-256-cbc';
const IV = Buffer.alloc(16, 0)

// SSLサーバー証明書を信頼する自己署名証明書と比較するための読み込み
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');

    // サーバーから証明書を受信
    client.once('data', (data) => {
        
        // サーバから送信されたSSL証明書
        const receivedCertificate = data.toString()

        // ブラウザのルート証明書でSSL証明書を検証と仮定
        if (receivedCertificate == clientCertificate) {
            // 認証に成功した場合の処理
            // クライアント側で共通鍵を生成
            const sessionKey = randomBytes(32);
        }
        
    })
})

[6]: [Client] SSLサーバ証明書に付録された公開鍵で共通鍵を暗号化し, サーバに送信する

※ 暗号化については, 説明を省略

// SSLサーバー証明書を信頼する自己署名証明書と比較するための読み込み
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');

    // サーバーから証明書を受信
    client.once('data', (data) => {
        
        // サーバから送信されたSSL証明書
        const receivedCertificate = data.toString()

        // ブラウザのルート証明書でSSL証明書を検証と仮定
        if (receivedCertificate == clientCertificate) {
            // 認証に成功した場合の処理
            // クライアント側で共通鍵を生成
            const sessionKey = randomBytes(32);
            
            const encryptedSessionKey = publicEncrypt({
                key: receivedCertificate, // サーバ側の公開鍵
                padding: constants.RSA_PKCS1_PADDING
            }, sessionKey);
            
            // サーバに, サーバの公開鍵で暗号化された共通鍵を送信
            client.write(encryptedSessionKey) 
        }
        
    })
})

[7]: [Server] 暗号化された共通鍵を, 秘密鍵を復号する

// 秘密鍵と証明書(公開鍵)の読み込み
const privateKey = fs.readFileSync('key.pem', 'utf8');
const certificate = fs.readFileSync('cert.pem', 'utf8');

const server = net.createServer((socket) => {
    if (initMessage === "SSL") {
        // クライアントにSSLサーバ証明書を送付
        socket.write(certificate)
        
        socket.on("data", (encryptedSessionKey) => {

                // サーバー側の秘密鍵で共通鍵を復号化
                const sessionKey = privateDecrypt({
                    key: privateKey, // サーバ側の秘密鍵
                    padding: constants.RSA_PKCS1_PADDING
                }, encryptedSessionKey);
            })
})

[8]: [Server] 復号した共通鍵を利用して, 通信を開始する

// 秘密鍵と証明書(公開鍵)の読み込み
const privateKey = fs.readFileSync('key.pem', 'utf8');
const certificate = fs.readFileSync('cert.pem', 'utf8');

const server = net.createServer((socket) => {
    if (initMessage === "SSL") {
        // クライアントにSSLサーバ証明書を送付
        socket.write(certificate)
        
        socket.on("data", (encryptedSessionKey) => {

                // サーバー側の秘密鍵で共通鍵を復号化
                const sessionKey = privateDecrypt({
                    key: privateKey, // サーバ側の秘密鍵
                    padding: constants.RSA_PKCS1_PADDING
                }, encryptedSessionKey);
                
                const text = "Hello, World!";

                // 共通鍵を利用してテキストデータを暗号化
                const cipher = createCipheriv(ALGORITHM, sessionKey, IV);
                let encryptedText = cipher.update(text, 'utf8', 'hex');
                encryptedText += cipher.final('hex');

                // 暗号化されたテキストをクライアントに送信
                socket.write(encryptedText)
            })
})

[9]: [Client] 共通鍵(セッションキー)で暗号化されたメッセージを複合化する

// SSLサーバー証明書を信頼する自己署名証明書と比較するための読み込み
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');

// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');

    // サーバーから証明書を受信
    client.once('data', (data) => {
        
        // サーバから送信されたSSL証明書
        const receivedCertificate = data.toString()

        // ブラウザのルート証明書でSSL証明書を検証と仮定
        if (receivedCertificate == clientCertificate) {
            // 認証に成功した場合の処理
            // クライアント側で共通鍵を生成
            const sessionKey = randomBytes(32);
            
            const encryptedSessionKey = publicEncrypt({
                key: receivedCertificate, // サーバ側の公開鍵
                padding: constants.RSA_PKCS1_PADDING
            }, sessionKey);
            
            // サーバに, サーバの公開鍵で暗号化された共通鍵を送信
            client.write(encryptedSessionKey) 
            
            // サーバからデータを受信
            client.on("data", (encryptedTextBuffer) => {
                
                // サーバーから受信した暗号化されたテキストを復号
                const decipher = createDecipheriv(ALGORITHM, sessionKey, IV);
                const encryptedText = encryptedTextBuffer.toString()
                let decryptedText = decipher.update(encryptedText, 'hex', 'utf8');
                decryptedText += decipher.final('utf8');

                console.log("decryptedText", decryptedText)
                
                client.end()
            })
        }
    })
})

全体像

server.ts

import * as net from 'net';
import * as fs from 'fs';
import { privateDecrypt, constants, createCipheriv } from 'crypto';

const ALGORITHM = 'aes-256-cbc';
const IV = Buffer.alloc(16, 0)

// 秘密鍵と証明書(公開鍵)の読み込み
const privateKey = fs.readFileSync('key.pem', 'utf8');
const certificate = fs.readFileSync('cert.pem', 'utf8');

const server = net.createServer((socket) => {

    socket.once("data", (data) => {
        const initMessage = data.toString()

        if (initMessage === "SSL") {

            // クライアントにSSL証明書を送付
            socket.write(certificate)

            socket.on("data", (encryptedSessionKey) => {

                // サーバー側の秘密鍵で共通鍵を復号化
                const sessionKey = privateDecrypt({
                    key: privateKey, // サーバ側の秘密鍵
                    padding: constants.RSA_PKCS1_PADDING
                }, encryptedSessionKey);

                console.log("sessionKey", sessionKey.toString('hex'))
                
                const text = "Hello, World!";

                // 共通鍵を利用してテキストデータを暗号化
                const cipher = createCipheriv(ALGORITHM, sessionKey, IV);
                let encryptedText = cipher.update(text, 'utf8', 'hex');
                encryptedText += cipher.final('hex');

                // 暗号化されたテキストをクライアントに送信
                socket.write(encryptedText)
        
            })
        } else {
            socket.end()
        }
    })
})

// サーバーを開始
server.listen(8443, () => {
    console.log('Server running on port 8443');
});

client.ts

import { randomBytes, createDecipheriv, publicEncrypt, constants } from 'crypto';
import * as net from 'net';
import * as fs from 'fs';

const ALGORITHM = 'aes-256-cbc';
const IV = Buffer.alloc(16, 0)

// SSLサーバー証明書を信頼する自己署名証明書と比較するための読み込み
const clientCertificate = fs.readFileSync('cert.pem', 'utf8');


// サーバーへの接続
const client = net.connect(8443, 'localhost', () => {

    // サーバーに "SSL" メッセージを送信して、SSL通信を要求
    client.write('SSL');

    // サーバーから証明書を受信
    client.once('data', (data) => {
        
        // サーバから送信されたSSL証明書
        const receivedCertificate = data.toString()

        // ブラウザのルート証明書でSSL証明書を検証と仮定
        if (receivedCertificate == clientCertificate) {
            // 認証に成功した場合の処理

            // クライアント側で共通鍵を生成
            const sessionKey = randomBytes(32);
            console.log("sessionKey", sessionKey.toString('hex'))

            const encryptedSessionKey = publicEncrypt({
                key: receivedCertificate, // サーバ側の公開鍵
                padding: constants.RSA_PKCS1_PADDING
            }, sessionKey);
            

            // サーバに, サーバの公開鍵で暗号化された共通鍵を送信
            client.write(encryptedSessionKey)

            // サーバからデータを受信
            client.on("data", (encryptedTextBuffer) => {
                
                // サーバーから受信した暗号化されたテキストを復号
                const decipher = createDecipheriv(ALGORITHM, sessionKey, IV);
                const encryptedText = encryptedTextBuffer.toString()
                let decryptedText = decipher.update(encryptedText, 'hex', 'utf8');
                decryptedText += decipher.final('utf8');

                console.log("decryptedText", decryptedText)
                
                client.end()
            })
        }
        
    })
})
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?