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

### モチベーション

情報セキュリティマネジメント試験の勉強をしていたら, 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

#### サーバ側の準備

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

ソケット通信の開始

```typescript
// 秘密鍵と証明書(公開鍵)の読み込み
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');
})
```

#### クライアント側の準備

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

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

})
```


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

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

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


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

※ 準備の段階で作成済み

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

クライアントから”SSL”というメッセージを受け取ったらSSLサーバ証明書を送信する
```typescript
// 秘密鍵と証明書(公開鍵)の読み込み
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サーバ証明書を検証する.

※ 本来はルート証明書を利用する
```typescript
// 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サーバ証明書の検証に成功した場合, 共通鍵(セッションキー)を作成する
```typescript
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サーバ証明書に付録された公開鍵で共通鍵を暗号化し, サーバに送信する

※ 暗号化については, 説明を省略
```typescript
// 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] 暗号化された共通鍵を, 秘密鍵を復号する
```typescript
// 秘密鍵と証明書(公開鍵)の読み込み
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] 復号した共通鍵を利用して, 通信を開始する
```typescript
// 秘密鍵と証明書(公開鍵)の読み込み
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] 共通鍵(セッションキー)で暗号化されたメッセージを複合化する
```typescript
// 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
```typescript
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
```typescript
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()
            })
        }
        
    })
})
```