goで作るAPIのセキュリティを考えた


この記事は Goodpatch Advent Calendar 2020 の21日目です。

はじめに

golangによるAPI開発時のセキュリティについて考えてみました。

まず、サービスの前提条件。

  • ステートレスな設計であること。
  • コンテナ運用に適していること。
  • マイクロサービス化も前提とすること。
  • APIの利用先はWebサイト、もしくはアプリケーションを対象とする。

APIのセキュリティを強化する一般的な方法

  1. トークンを使用 信頼できるIDを確立してから、その後はIDに割り当てられたトークンを使用して、サービスおよびリソースへのアクセスを制御します。
  2. 暗号化と署名を使用する。
    TLSなどの手段を使用してデータを暗号化します。署名を使用して適切なユーザーがデータを復号化および変更が出来るようにします。
  3. クォータとスロットリング APIを呼び出す頻度についてクォータを設定し、使用履歴を追跡します。
  4. APIゲートウェイ APIゲートウェイは APIトラフィックの主な適用ポイントとして機能します。ゲートウェイを適切に使用すると、トラフィックを認証し、APIの使用方法を制御して分析できます。

今回はシンプルにトークンの使用を考えてみます。

APIでトークンというとJWTを使うやり方や、Cookieによるセッションを使うやり方を考えますが、前提としてステートレスを上げているので今回はJWT方式で検討します。

JWT(JSON Web Token)方式

RFC7519で定められた、属性情報(Claim)をJSONデータ構造で表現するトークンを使用する方式になります。

が、いろんな記事に書かれているようにJWTは仕様的に脆弱性があるので採用は考えてしまいます。

"alg": "none" 攻撃といった、主に署名アルゴリズムを選択可能という仕様の問題への対応が必要で、algをホワイトリスト化する対応を取ったりする方法などが紹介されています。

そういった対策を行うよりはセキュリティを考慮したJWTベースなPASETOという仕様があったので今回はそちらを採用します。

PASETO(platform-agnostic security tokens)

詳しくはこちらを参照してください、JOSE(JWT、JWE、JWS)のような構造上の欠陥は無い!って宣言で始まってますね。
基本的な言語毎の対応ライブラリも用意されているので採用しやすそうです。

見てみるとPASETOのバージョンとしてV1とV2がありました。

v1.local : Symmetric Authenticated Encryption:

  • AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC)
  • Key-splitting: HKDF-SHA384
    • Info for encryption key: paseto-encryption-key
    • Info for authentication key: paseto-auth-key-for-aead
  • 32-byte nonce (first half for AES-CTR, latter half for the HKDF salt)
  • The nonce calculated from HMAC-SHA384(message, random_bytes(32)) truncated to 32 bytes, during encryption only
  • The HMAC covers the header, nonce, and ciphertext

v1.public: Asymmetric Authentication (Public-Key Signatures):

  • 2048-bit RSA keys
  • RSASSA-PSS with
    • Hash function: SHA384 as the hash function
    • Mask generation function: MGF1+SHA384
    • Public exponent: 65537

v2.local: Symmetric Encryption:

  • XChaCha20-Poly1305 (192-bit nonce, 256-bit key, 128-bit authentication tag)
  • Encrypting: sodium_crypto_aead_xchacha20poly1305_ietf_encrypt()
  • Decrypting: sodium_crypto_aead_xchacha20poly1305_ietf_decrypt()
  • The nonce is calculated from sodium_crypto_generichash() of the message, with a BLAKE2b key provided by random_bytes(24) and an output length of 24, during encryption only

v2.public: Asymmetric Authentication (Public-Key Signatures):

  • Ed25519 (EdDSA over Curve25519)
  • Signing: sodium_crypto_sign_detached()
  • Verifying: sodium_crypto_sign_verify_detached()

違いとしては暗号化アルゴリズムの違いくらいになるので、ここは迷わずEd25519のV2を採用しましょう。

golangで実際にPASETOでトークンを生成してみる

1.Ed25519で公開鍵、密鍵の生成

package main

import (
    "crypto/ed25519"
    "log"
)

function main(){
    edPublicKey, edPrivateKey, _ := ed25519.GenerateKey(nil)

    var publicKey [32]byte
    var privateKey [64]byte
    for i, v := range edPublicKey {
        publicKey[i] = v
    }
    for i, v := range edPrivateKey {
        privateKey[i] = v
    }

    log.Printf("公開鍵:%x\n", publicKey)
    log.Printf("秘密鍵:%x\n", privateKey)
}

2. トークン生成

package main

import (
    "encoding/hex"
    "time"
    "crypto/ed25519"

    "github.com/o1egl/paseto"
)

function main(){

    now := time.Now()

    b, _ := hex.DecodeString("秘密鍵")
    privateKey := ed25519.PrivateKey(b)

    jsonToken := paseto.JSONToken{
        Audience:   "Audience",                       // 利用ユーザー判別するユニーク値
        Issuer:     "Issuer",                         // 利用システム
        Subject:    "WebAPI",                         // 利用機能
        Jti:        "UUID",                           // UUID
        Expiration: time.Now().Add(30 * time.Minute), // 失効日時
        IssuedAt:   now,                              // 発行日時
        NotBefore:  now,                              // 有効化日時
    }

    // トークンにカスタム属性情報を追加
    jsonToken.Set("KEY", "VALUE")

    // フッター
    footer := "footer"
    v2 := paseto.NewV2()

    // トークン生成
    token, _ := v2.Sign(privateKey, jsonToken, footer)

    log.Printf("トークン:%s\n", token)
}

ライブラリ用意されていると簡単ですね。

トークンの実際の運用を考える

1.処理フロー

基本パターンとして認証サービスでトークンを発行し、機能サービス側でトークンの検証を行う。
もし、トークンの有効期限が切れていた場合は有効切れトークンを使用して認証サービス側で新しいトークンを再発行する。

2.チェックポイント

  • トークンのサイズ
    PASETOでトークンを生成した場合に、属性情報を追加するためトークンのサイズが大きくなる傾向にあり、保存場所とも関連しますがCookieで保存する場合は上限が4kbなのでサイズの確認は必要です。
    ち文字列を幾つか追加した場合のトークンのサイズが、453バイトなのでよっぽど属性情報を大量に追加しない限りは大丈夫そうです。

  • トークンの保存場所

    • Cokkieの場合
      あまり利用しないパターンですが、Secure属性、HttpOnly属性を付与してXSSの対策を行えます。
      ただし、CookieヘッダでサーバへJWTを送る場合はCSRFの脆弱性はあります。
    • LocalStorageの場合
      基本的にこちらの利用がほとんどだと思います、デメリットとしてXSSでトークンが盗まれる可能性が良く指摘されます。
      XSSの対策としてはフロント側でよろしく! と言いたいところですが、サーバー側の対策として有効期限を短くして利用可能な期間を最小にするという保険的な対応を行っておきます。その分、認証サービスへのアクセスは多くなる事は考慮しておきましょう。
  • トークンの無効化
    トークンが盗まれた際の対応の一部として、サーバー側でトークンの検証制御を行える処理を組み込ます。

トークンのホワイトリストを保持して制御する方法もありますが、トークンの特性上を考えて発行日時と識別日時という日付の比較で無効化の制御を行います。

  • トークンのログアウト対応
    対応を行っていない場合にログアウトしてもブラウザ側でトークンが適切に廃棄されないとそのままログイン可能という状態が発生するので、サーバー側までログアウトを通知してトークン検証でNGとなる対応も安全のために検討しておきます。

終わりに

セキュリティ的に対応やリスクを認識しておく必要がありますが、利用するメリットもたくさんあるので検討してみる価値はあると思います。