Railsでデータ署名と暗号化を使ったAPIを構築し、セキュアに通信する方法

## はじめに
APIを提供する際、セキュリティが非常に重要です。この記事では、Railsでデータ署名と暗号化を組み込んだセキュアなAPIを構築し、クライアントとの安全なデータ通信を実現する方法を解説します。

## 1. プロジェクトの準備と秘密鍵の設定
### Railsプロジェクトの作成
まず、Railsプロジェクトを作成し、署名や暗号化に使う秘密鍵を設定します。

```bash
# Railsプロジェクトの作成
rails new SecureAPI
cd SecureAPI
```

### Secret Keyの設定
秘密鍵をRailsのcredentialsに追加します。これを使って署名や暗号化を行います。

```bash
# シークレットキーを設定
EDITOR="vim" bin/rails credentials:edit
```
開いたファイルに以下を追加します。

```yaml
secret_key_base: your_generated_secret_key
```

## 2. 署名・暗号化サービスの実装
### データ署名サービスの作成
署名を生成し、データ改ざんを防止するための`SignatureService`を作成します。

```bash
# servicesディレクトリを作成
mkdir -p app/services

# 署名サービスファイルを作成
touch app/services/signature_service.rb
```

`app/services/signature_service.rb`に以下を記述します。

```app/services/signature_service.rb
require 'openssl'
require 'base64'

class SignatureService
  # SECRET_KEYは署名を作る際の秘密鍵。Railsの`credentials`から取得しています。
  SECRET_KEY = Rails.application.credentials.secret_key_base

  # 署名を生成するメソッド。データに対して署名（データの改ざんが無いことを保証する証明書）を作成します。
  def self.generate_signature(data)
    digest = OpenSSL::Digest.new('sha256') # SHA256という方式でハッシュを生成します
    hmac = OpenSSL::HMAC.digest(digest, SECRET_KEY, data)  # データに署名（HMAC）をつけます
    Base64.strict_encode64(hmac) # ハッシュ化された値をBase64形式に変換します
  end

  # 署名を検証するメソッド。送られてきた署名と一致するかを確認します。
  def self.verify_signature(data, signature)
    expected_signature = generate_signature(data) # データから期待される署名を生成
    ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature) # 比較して改ざんがないか確認
  end
end
```
- `generate_signature`: 渡されたデータに対して署名を作ります。この署名を使って、データが改ざんされていないことを証明できます。
- `verify_signature`: データが改ざんされていないかを確認するために、署名を検証します。検証に失敗した場合、データが改ざんされていると判断します。

### 補足
- **SHA256という方式でハッシュを生成する理由**:
  - 256ビットの長さで出力されるため、安全性が高い
  - SHA256は、入力がわずかに異なるだけでも全く異なるハッシュ値を生成するため、データの一部が変更されてもハッシュ値の一致ができない
- **データに署名（HMAC）をつける理由**:
  - メッセージが送信された後に変更されていないかを検証できる
  - HMACは、署名を生成するために秘密鍵を使うため、正当な送信者からのメッセージかどうかを認証できる
- **ハッシュ化された値をBase64形式に変換する理由**:
  - ハッシュの結果はバイナリデータで出力されます。バイナリデータは直接通信や保存するのに適していないため、Base64に変換してテキスト化
  - バイナリデータを直接使うと、文字コードによって誤解釈される場合があります。Base64エンコードはASCII文字のみを使用するため、エンコーディングの違いによるトラブルを回避できる

### データ暗号化サービスの作成
次に、暗号化サービスを実装し、APIデータの機密性を確保します。

```bash
# 暗号化サービスファイルを作成
touch app/services/encryption_service.rb
```

`app/services/encryption_service.rb`に以下を記述します。

```app/services/encryption_service.rb
class EncryptionService
  # 暗号化のためのシークレットキー。32バイトに切り出して使用します。
  SECRET_KEY = Rails.application.credentials.secret_key_base.byteslice(0, 32)
  CIPHER = 'aes-256-gcm' # 暗号化方式はAES-256-GCM

  # データを暗号化するメソッド
  def self.encrypt(data)
    crypt = ActiveSupport::MessageEncryptor.new(SECRET_KEY, cipher: CIPHER)
    crypt.encrypt_and_sign(data) # 暗号化と署名を同時に行います
  end

  # データを復号化するメソッド
  def self.decrypt(encrypted_data)
    crypt = ActiveSupport::MessageEncryptor.new(SECRET_KEY, cipher: CIPHER)
    crypt.decrypt_and_verify(encrypted_data) # 復号と署名の確認を行います
  end
end
```
- `encrypt`: 渡されたデータを暗号化し、第三者が内容を解読できないようにします。暗号化には`SECRET_KEY`と`AES-256-GCM`という方式を使用します。
- `decrypt`: 暗号化されたデータを復号し、元のデータに戻します。このメソッドは、暗号化時に付加された署名も確認し、改ざんがないことを確認します。

### 補足
- **暗号化のためのシークレットキーを32バイトに切り出して使用する理由**:
  - AES-256は、最もセキュアで強力なバージョンであるため、256ビット長の鍵が必須です。Railsの`secret_key_base`は通常128ビット（16バイト）以上のランダムな文字列を生成しますが、長さが32バイトを超える場合は、そのうちの最初の32バイトを使ってAES-256に対応した鍵として使用。
- **暗号化方式にAES-256-GCMを選択する理由**:
  - AES-256は最も強力なAES暗号で、256ビットの鍵を使うため解読が困難
  - GCMモードは、暗号化されたデータの一部に**メッセージ認証コード**（MAC）を付与します。これにより、復号時にデータが改ざんされていないかを確認できます。
## 3. APIエンドポイントの構築
次に、署名と暗号化を使ってデータを送受信するAPIを作成します。

### コントローラの作成
まず、APIのコントローラを作成し、暗号化・署名付きでリクエストを受け付けるエンドポイントを設定します。

```bash
# APIコントローラを作成
rails generate controller api/v1/messages
```

`app/controllers/api/v1/messages_controller.rb`に以下を記述します。

```app/controllers/api/v1/messages_controller.rb
class Api::V1::MessagesController < ApplicationController
  skip_before_action :verify_authenticity_token # CSRFチェックを無効化（APIの場合は不要）

  def create
    data = params[:data] # 送られた暗号化されたデータ
    signature = params[:signature] # 送られた署名

    # 署名を検証して改ざんがないか確認
    unless SignatureService.verify_signature(data, signature)
      render json: { error: 'Invalid signature' }, status: :unauthorized and return
    end

    # データを復号
    begin
      decrypted_data = EncryptionService.decrypt(data)
    rescue ActiveSupport::MessageEncryptor::InvalidMessage
      render json: { error: 'Invalid data' }, status: :bad_request and return
    end

    # 成功時のレスポンス
    render json: { message: "Received: #{decrypted_data}" }
  end
end
```
- **署名の検証**: クライアントから送られてきたデータが改ざんされていないかを`SignatureService.verify_signature`で検証します。不正な場合はエラーレスポンスを返します。
- **データの復号化**: `EncryptionService.decrypt`を使用してデータを復号化し、内容を元に戻します。
- **レスポンス**: 署名の検証と復号化が成功した場合、元のデータ内容を含むレスポンスを返します。

### ルーティングの設定
次に、ルーティングにエンドポイントを設定します。

```config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      post 'messages', to: 'messages#create'
    end
  end
end
```

## 4. クライアントでのリクエスト署名・暗号化
APIにデータを送信するクライアント側での署名と暗号化の例を示します。ここでは、簡単なRubyスクリプトで実装します。

### クライアントサイドの実装
`client.rb`を作成し、以下を記述します。

```client.rb
require 'net/http'
require 'uri'
require 'json'
# Rails環境の読み込み
require_relative './config/environment'

# 必要なサービスの読み込み
require_relative './app/services/signature_service'
require_relative './app/services/encryption_service'

# APIエンドポイント
uri = URI.parse("http://localhost:3000/api/v1/messages")

# データを暗号化
data = "Hello, secure world!"
encrypted_data = EncryptionService.encrypt(data)

# 暗号化データの署名を生成
signature = SignatureService.generate_signature(encrypted_data)

# リクエストを送信
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
request.body = { data: encrypted_data, signature: signature }.to_json

response = http.request(request)
puts "Response: #{response.body}"
```

1.**データを暗号化**:
- `data = "Hello, secure world!"`で送信するデータを用意。
- `encrypted_data = EncryptionService.encrypt(data)`でデータを暗号化します。この暗号化されたデータはcreateアクションで元に戻されます。

2.**署名を生成**:
- `signature = SignatureService.generate_signature(encrypted_data)`で暗号化したデータに署名を付けます。この署名はサーバーで検証され、不正がないことを確認します。

3.**APIにデータ送信**:
- `Net::HTTP`でAPIリクエストを送信します。データ（`encrypted_data`）と署名（`signature`）が送信され、サーバーで検証・復号されます。

### 実行手順
以下のコマンドでクライアントスクリプトを実行し、サーバーがデータを受信できるか確認します。

```bash
ruby client.rb
# => Response: {"message":"Received: Hello, secure world!"}
```

以上で、Railsを使用して署名と暗号化を利用したセキュアなAPIが完成しました。

## 参考文献
- [Ruby 3.3 リファレンスマニュアル: class OpenSSL::Digest](https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aDigest.html)
- [Ruby 3.3 リファレンスマニュアル: class OpenSSL::HMAC](https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aHMAC.html)
- [Ruby 3.3 リファレンスマニュアル: singleton method OpenSSL::HMAC.digest](https://docs.ruby-lang.org/ja/latest/method/OpenSSL=3a=3aHMAC/s/digest.html)
- [Active Support Message Encryptor](https://api.rubyonrails.org/v7.2/classes/ActiveSupport/MessageEncryptor.html)