暗号化と署名検証は目的が異なる
暗号化と署名検証は、どちらもセキュリティの一部であり、データの保護を目指していますが、それぞれ異なる目的を持っています。
暗号化
暗号化は、データを保護するための手段で、不正な第三者からデータを隠蔽することが目的です。
暗号化にデータの改ざんを検知する機能はありません。たとえば、暗号化されたメッセージを改ざんして別の有効な暗号文を作成することが可能です(暗号化方式によりますが)。暗号化だけでは、データが第三者によって改ざんされたかどうかを確認することはできません。
暗号化には、大きく分けて対象鍵暗号化と非対象鍵暗号化の2つの種類があります。
対象鍵暗号化
対称鍵暗号化は、同一の鍵(シークレットキー)を使って情報を暗号化し、その鍵を使って情報を復号化します。対称鍵暗号化は高速で効率的であり、大量のデータを暗号化するのに適しています。ただし、鍵の配布が難しく、鍵が漏洩した場合、暗号化された全てのデータが危険にさらされます。
暗号アルゴリズムとしては、AES(Advanced Encryption Standard)やDES(Data Encryption Standard)などがあります。
ユースケース
データの保管と保護: データベースに保存されるデータを保護するために対称暗号化がよく使用されます。
セキュアなネットワーク接続:例えば、VPNやHTTPSを使用してデータを転送するときに、対称暗号化は通常、データの一部を暗号化します。この場合、対称鍵は通信を確立するために非対称鍵暗号化(公開鍵暗号化)を使用して安全に交換されます。
Rubyコード例
require 'openssl'
require 'base64'
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
data = "Confidential data that needs to be encrypted"
encrypted = cipher.update(data) + cipher.final
encrypted_data = Base64.encode64(encrypted)
puts "Encrypted data: #{encrypted_data}"
decipher = OpenSSL::Cipher::AES.new(256, :CBC)
decipher.decrypt
decipher.key = key
decipher.iv = iv
decrypted = decipher.update(Base64.decode64(encrypted_data)) + decipher.final
puts "Decrypted data: #{decrypted}"
非対象鍵暗号化
非対称鍵暗号化は、公開鍵と秘密鍵のペアを使います。公開鍵で情報を暗号化すれば、対応する秘密鍵でしかその情報を復号化できません。逆も可能で、秘密鍵で暗号化すれば公開鍵で復号化できます。非対称鍵暗号化は対称鍵暗号化よりも遅いですが、鍵の配布問題を解決します。
暗号アルゴリズムとしては、RSA(Rivest–Shamir–Adleman)、ECC(Elliptic Curve Cryptography)などがあります。
ユースケース
データの通信における秘密保持と身元確認。例えば、HTTPSのような安全な通信プロトコルで使われます。また、デジタル署名の生成にも使われます。デジタル署名についてはJWTを参考。
Rubyコード例
require 'openssl'
rsa_key_pair = OpenSSL::PKey::RSA.new(2048)
public_key = rsa_key_pair.public_key
private_key = rsa_key_pair
data = "Confidential data that needs to be encrypted"
encrypted_data = public_key.public_encrypt(data)
puts "Encrypted data: #{Base64.encode64(encrypted_data)}"
decrypted_data = private_key.private_decrypt(encrypted_data)
puts "Decrypted data: #{decrypted_data}"
対称暗号化と非対称暗号化の両方を用いたセキュアなデータ転送について
対称暗号化はネットワーク上でのデータ転送によく利用されます。たとえば、HTTPS(HTTP over SSL/TLS)はWeb通信のセキュリティを確保するためのプロトコルで、対称暗号化を含む複数の暗号化方式を利用しています。
HTTPSでは、最初にクライアント(通常はWebブラウザ)とサーバー間で「ハンドシェイク」が行われます。このハンドシェイクの中で、まず非対称暗号化(公開鍵暗号化)が利用されます。サーバーは公開鍵と秘密鍵のペアを持っています。公開鍵はクライアントに送られ、秘密鍵はサーバーが保持します。この公開鍵を使用して、クライアントはランダムに生成した対称鍵(セッションキー)を暗号化してサーバーに送ります。サーバーは秘密鍵を用いてこれを復号し、その後の通信で使用する共通の対称鍵を得ます。
なぜ非対称暗号化と対称暗号化の両方を使うのでしょうか?それは、非対称暗号化はセキュリティは高いものの計算コストが高く、一方で対称暗号化は計算コストが低いが鍵の交換が問題となるためです。この2つを組み合わせることで、セキュリティを保ちつつ効率的な通信を実現しています。具体的には、安全に鍵を交換するための非対称暗号化と、実際のデータ転送の高速化のための対称暗号化を組み合わせて利用します。
対称鍵を安全に交換した後は、実際のデータの暗号化と復号化にその対称鍵が使用されます。その結果、効率的に大量のデータを安全に転送することが可能になります。
署名検証
署名検証は、メッセージの完全性と認証を保証するための手段です。メッセージが改ざんされていないこと、そしてメッセージが期待する送信者から来ていることを保証します。
HMAC
HMAC(Hash-based Message Authentication Code)は、共有秘密鍵を使用してメッセージの完全性と認証を確認するための手法です。HMACは、メッセージと秘密鍵を入力として受け取り、それらを使用してハッシュを生成します。このハッシュは、メッセージと一緒に送信され、受信者は同じ秘密鍵を使用してハッシュを計算し、受信したハッシュと比較します。一致しない場合、メッセージは改ざんされたと見なされます。
ユースケース
メッセージの完全性と真正性の検証。例えば、APIリクエストの署名に使われます。署名は、APIキー(秘密鍵)を使って生成され、APIサーバーは同じキーを使って署名を検証します。
Rubyコード例
require 'openssl'
require 'base64'
data = "Data that needs to be signed"
key = "your secret key"
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, key, data)
puts "HMAC: #{Base64.encode64(hmac)}"
JWT
JWT(JSON Web Token)は、情報をJSONオブジェクトの形で安全に伝達するためのオープンスタンダード(RFC 7519)です。JWTは、情報の完全性を保証するために署名を使用します。この署名には、HMACを使用することも、RSAやECCなどの非対称暗号化を使用することもできます。
非対称暗号化を使用する場合、それはデジタル署名という形を取ります。具体的には、秘密鍵で情報を署名し、対応する公開鍵を用いてその署名を検証します。このメカニズムにより、情報の完全性が保証され、送信者の認証も行われます。
JWTは以下の3つの部分から成り立っています:
- ヘッダ(Header):ここでは、使用する暗号化アルゴリズムなどが指定されます。
- ペイロード(Payload):ここでは、実際に伝達する情報(クレームと呼ばれる)が格納されます。
- 署名(Signature):ここでは、ヘッダとペイロードを指定のアルゴリズムで署名します。この署名により、情報の完全性が保証されます。
JWTは主に認証と情報交換に使用され、一般的にはクライアントがサーバーからJWTを受け取った後、その後のリクエストでそのJWTをサーバーに送ります。サーバーは、JWTの署名を検証することで、そのJWTが信頼できるものであることを確認します。
なお、JWTのペイロード部分は、暗号化されていない場合が多く、Base64でエンコードされただけです。したがって、機密情報をJWTに含める場合には、その情報自体を暗号化するか、あるいはJWE(JSON Web Encryption)のようなスキームを使用して全体を暗号化する必要があります。
JWTにおいて、署名にHMACを用いる場合、秘密鍵はクライアントとサーバー間で共有することは基本的にはありません。サーバーは秘密鍵を使用してJWTに署名し、そのJWTをクライアントに送ります。クライアントはそのJWTを次回のリクエスト時にサーバーに送り返します。そしてサーバーは、再びその秘密鍵を使用してJWTの署名を検証します。この流れにおいて、秘密鍵自体がクライアントに公開されることはありません。
ただし、非対称暗号化(公開鍵と秘密鍵)を使用する場合、公開鍵はクライアントに送信されるか、あるいは公開鍵が公開されたエンドポイントを提供します。クライアントは公開鍵を使ってJWTの署名を検証します。この場合でも、秘密鍵はサーバー上で安全に保管され、外部に公開されることはありません。
ユースケース
ウェブサイトでログインするとき、サーバはユーザーIDとロール情報を含むJWTを生成し、そのJWTをユーザーのブラウザに送ります。その後、ブラウザはそのJWTを保存し、次にサーバにリクエストを送るときにそのJWTを含めます。サーバはそのJWTを検証し、ユーザーを認証・認可します。
Rubyコード例
JWTのトークンを作成し、それをデコードする基本的な例
require 'jwt'
# 秘密鍵(実際にはもっと安全な方法で保存・取得する必要があります)
secret = 'my$ecretK3y'
# ペイロードの作成
payload = { data: 'test', exp: Time.now.to_i + 60 * 60 }
# JWTトークンの作成
token = JWT.encode(payload, secret, 'HS256')
puts token
#=> eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJkYXRhIjogInRlc3QiLCAiZXhwIjogMTU2ODQyNzc4MH0.XBTEbYtezYikZTZNfq-dopruTzQua3d0Ty7Uq5c2Gw4
# トークンのデコード
decoded_token = JWT.decode(token, secret, true, { algorithm: 'HS256' })
puts decoded_token
#=> [{"data"=>"test", "exp"=>1568427780}, {"alg"=>"HS256", "typ"=>"JWT"}]
これらのJWTトークンは、例えば、クライアント側でHTTPヘッダーにAuthorization: Bearer の形式で設定し、APIリクエストを送信する際に使用できます。サーバーサイドでは、このトークンをデコードし、有効かどうか(署名が一致するか、有効期限が過ぎていないかなど)を確認します。
まとめ
忘備録も兼ねて、暗号化と署名検証の違いとそれぞれの用途について解説しました。暗号化は不正な第三者からデータを保護する目的があり、対称鍵暗号化と非対称鍵暗号化があります。それに対して、署名検証はデータの完全性と認証を保証します。署名検証を行うことで情報が改ざんされず、予期する送信者から送られてきたことを保証します。これらは、安全な情報通信の設計や実装において重要です。