モチベーション
情報セキュリティマネジメント試験の勉強をしていたら, 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()
})
}
})
})