rubyで暗号技術入門


本日紹介するのは以下5項目

  • 対称暗号(共通鍵暗号)
  • 公開鍵暗号
  • 一方向ハッシュ関数
  • デジタル署名
  • 証明書

対称暗号(共通鍵暗号)

一つの鍵で暗号化し、同じ鍵で復号化する

require 'openssl'

# 暗号化したいデータを用意
data = '*secret data*'

# 32byte共有鍵、16byte初期化ベクトルを暗号アルゴリズムが求める長さで適当に用意する
key = 'a' * 32
iv = 'i' * 16

# 暗号化
enc = OpenSSL::Cipher.new('AES-256-CBC')
enc.encrypt
enc.key = key
enc.iv = iv
encrypted_data = enc.update(data) + enc.final

# 復号化
dec = OpenSSL::Cipher.new('AES-256-CBC')
dec.decrypt
dec.key = key
dec.iv = iv
decrypted_data = dec.update(encrypted_data) + dec.final

まあ、そうだよね。

公開鍵暗号

鍵配送問題とは

安全な共有鍵暗号アルゴリズムを使っている。
しかし、鍵を送らなければ、受信者は復号化できない。鍵を送れば、盗聴者も復号化できる。
鍵を送れなければならないのに、鍵を送っていはいけない。これが、対称暗号の鍵配送問題。

公開鍵暗号

公開鍵で暗号化し、秘密鍵で復号化する。暗号化する鍵と復号化する鍵が異なる。

# 鍵対を生成
rsa = OpenSSL::PKey::RSA.generate(2048)

# 暗号化するデータ
data = '*secret data*'

# 公開鍵で暗号化
encrypt_data = rsa.public_encrypt(data)
# 秘密鍵で復号化
decrypted_data = rsa.private_decrypt(encrypt_data)
# 公開鍵をpem形式で表示
puts rsa.public_key.export

よく見るやつ。もちろん秘密鍵もexportできる。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5MjWr3cTLnLN2rZ5gM3
IniiY2IpE/qZXDvcojRqEoTZ6seZnCNG8oJD9d8L0G1WxJAh73GyStJrheiDnADf
WkJR+354kDx4vyQhKzfisMEgecWG/Yu4G4Y29SSkPYlNd4myZ8et0xV1f1dShMeZ
qaZsgRofuEAvtavGus5LMgLTqaoCcSvnGtDtjWDgueoXr+MfRch+Lb1OoSAcmWHe
ZGIS/ZkI6nuf/omPH3687agti7NH2XM3t4EDMMCXgeBfB1aXUUV8WPAkGCTXYvjN
BWKQgJbQ493Vris2wNjSZq7WPtwyI/7K0cqp+Y6f9bm55s8rvaHHsPDUu1vNfi9N
TwIDAQAB
-----END PUBLIC KEY-----

一方向ハッシュ

任意長のメッセージから固定長のハッシュ値を計算する

  • MD4, MD5
  • SHA-1
  • SHA-2 (SHA-256, SHA-384, SHA-512)

MD4, MD5, SHA-1 の強衝突耐性はすでに破られている。

require 'openssl'

data = 'hello'
digest = OpenSSL::Digest.new('sha256')
digest.update(data)
puts digest.hexdigest()

8byteの入力でも80Gbyteの入力でも同じ長さのバイナリが生成できる

2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

鍵あり

key = 'password'
digest = OpenSSL::HMAC.new(key, 'sha256')
digest.update(data)
puts digest.hexdigest()
7ae615e698567e5e1512dd8140e740bd4d65dfa4db195d80ca327de6302b4a63

デジタル署名

このメッセージを書いたのは誰か

  • メッセージを秘密鍵で暗号化する。それが署名。
  • 署名を公開鍵で復号化してメッセージを得る
  • 2つのメッセージを比較することで改竄を検知できる
# 鍵対を生成
rsa = OpenSSL::PKey::RSA.generate(2048)

# メッセージ
data = 'foobar'

# 秘密鍵で署名
sign = rsa.sign('sha256', data)

# 公開鍵で検証
pp rsa.public_key.verify('sha256', sign, data)

# 不正なデータを検証
pp rsa.public_key.verify('sha256', sign, 'foobarbaz')
true
false

証明書

公開鍵へのデジタル署名

デジタル署名の前提は、「署名の検証を行うときに用いる公開鍵が、本物の送信者の公開鍵であること」。

自分の証明書を他の証明書に証明してもらう。
最終的には、プラットフォームの証明書に証明してもらう。

httpsの例を見てみる。
ipaのサーバにアクセスした。今アクセスしているサーバーが本当にipaのサーバーかどうか確認するために、
サーバー証明書を使う。サーバー証明書には、www.ipa.go.jpの記載がある。この証明書は本物か?

# ipaのサイトにsslで接続
soc = TCPSocket.new('www.ipa.go.jp', 443)
ssl = OpenSSL::SSL::SSLSocket.new(soc)
ssl.connect

# 接続相手の証明書からルート CA の証明書までのリスト
# [接続相手の証明書, 下位CAの証明書,... 中間CAの証明書]
ssl.peer_cert_chain.each do |cert|
  # 発行者を出力
  pp cert.issuer
end

ipaの証明書はSECOM発行のEV証明書と2つの中間CAの証明書。

#<OpenSSL::X509::Name CN=SECOM Passport for Web EV 2.0 CA,O=SECOM Trust Systems CO.\,LTD.,C=JP>
#<OpenSSL::X509::Name OU=Security Communication RootCA2,O=SECOM Trust Systems CO.\,LTD.,C=JP>
#<OpenSSL::X509::Name OU=Security Communication RootCA1,O=SECOM Trust.net,C=JP>

ipaの証明書を順に検証してみる

cert1 = nil
ssl.peer_cert_chain.each do |cert2|
  unless cert1.nil?
    pp cert1.verify(cert2.public_key)
  end

  cert1 = cert2
end

検証成功。

true
true

最後の中間CAの証明書だけ検証されていない。

# 中間CAの証明書
mid_cert = ssl.peer_cert_chain.last

OpenSSL付属のroot証明書を読み込んでみる
このファイルは複数のroot証明書が連結しているので分割。

cert_file  = OpenSSL::X509::DEFAULT_CERT_FILE

s = '-----END CERTIFICATE-----'
root_certs = File.read(cert_file).split(s).map {|p| p + s}.map do |pem|
  OpenSSL::X509::Certificate.new(pem)
end

確認した環境では、164個のroot証明書が入っていた。
その中に最後の中間CAの証明書を検証できるroot証明書が存在するか確認する

root_certs.each do |root_cert|
  begin
    if mid_cert.verify(root_cert.public_key)
       # 検証が成功した発行者の情報を表示
       pp root_cert.issuer
    end
  rescue OpenSSL::X509::CertificateError
  end
end

見つかりました。

#<OpenSSL::X509::Name OU=Security Communication RootCA1,O=SECOM Trust.net,C=JP>

こんな感じ。

まとめ

  • 対称暗号(共通鍵暗号)
  • 公開鍵暗号
  • 一方向ハッシュ関数
  • デジタル署名
  • 証明書