🛥️

署名付きCookie(Cloud CDN)を用いてCloud Storageのオブジェクトを配信する

に公開

こんにちは。AI ShiftでWebフロントエンドエンジニアをしている安井です。今回はGoogle CloudのCloud CDNとCloud Storageを用いて署名付きCookieを活用したオブジェクトの配信を紹介します。

はじめに

AI Shiftでは企業専用のAIエージェント構築プラットフォームであるAI Workerの開発を行なっています。AI Workerではお客様の貴重なデータをもとにLLMが回答を生成するため、そのデータの扱いには慎重になる必要があります。

https://www.aiworker.jp/

仮に添付した画像とともにエージェントへ作業を依頼するケースを考えます。ここで添付される画像には企業の機密情報が含まれる可能性があります。そのため、URLを知っているユーザでアクティブである限り誰でもアクセスが可能なCloud Storageの署名付きURLでは要件を満たすために不十分でした。

https://cloud.google.com/storage/docs/access-control/signed-urls?hl=ja

そこで今回はCloud CDNをCloud Storageの前段に配置し、署名付きCookieによる検証を行うことでユーザの画像ファイルが不意にアクセスされることを回避します。

https://cloud.google.com/cdn/docs/using-signed-cookies?hl=ja

全体像

処理の流れとしては図のような形です。

  1. まずClientからAPIサーバーに対して画像URLを取得するリクエストを送信します。
  2. APIサーバーはユーザの認証を行い、画像のURLを返却するとともに署名付きCookieを発行してSetします。
  3. Clientは受け取った画像のURLをimgタグに埋め込みリクエストします。この時APIサーバー側でSet-Cookieされた署名付きCookieもHeaderに含まれています。
  4. Cloud CDNはリクエストに含まれる署名付きCookieを検証して成功した場合Cloud Storageへ リクエストします。仮に署名付きCookieの検証に失敗した場合は403が返却されます。
  5. 最終的にCookieの検証を通してCloud StorageのオブジェクトにアクセスしてClientへ返却されます。

署名付きCookieを発行する

署名付きCookieをプログラムで作成するExampleが公式のドキュメントに掲載されています。

Goでの実装(公式)
import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

// signCookie creates a signed cookie for an endpoint served by Cloud CDN.
//
// - urlPrefix must start with "https://" and should include the path prefix
// for which the cookie will authorize access to.
// - key should be in raw form (not base64url-encoded) which is
// 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signCookie(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	encodedURLPrefix := base64.URLEncoding.EncodeToString([]byte(urlPrefix))
	input := fmt.Sprintf("URLPrefix=%s:Expires=%d:KeyName=%s",
		encodedURLPrefix, expiration.Unix(), keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(input))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

	signedValue := fmt.Sprintf("%s:Signature=%s",
		input,
		sig,
	)

	return signedValue, nil
}

公式ドキュメントではGo, Java, Pythonの例しかありませんでしたが、本記事はTypeScript(Node.js)を前提に実装をします。

https://cloud.google.com/cdn/docs/using-signed-cookies?hl=ja#programmatically_creating_signed_cookies

鍵を共有する

Cloud CDNで署名付きCookieのサポートを有効にするには、Cloud CDNが有効化されたバックエンド サービス、バックエンド バケット、またはその両方で1つ以上の鍵を作成します。

ここで自動生成された鍵をアプリケーション側で使用して署名しますが、その際に自動生成された鍵は事前にbase64url encodeされていることに注意してください。

// Cloud CDNで設定された鍵をbase64URL decodeする
const signedURLKey = Buffer.from(env.CDN_SIGNED_URL_KEY, 'base64url')

各種フィールドの設定

署名付きCookieでは以下4つの必須フィールドがあります。

  • URLPrefix
  • Expires
  • KeyName
  • Signature

URLPrefix

URLPrefixは、URLの先頭部分を示しています。これはbase64url形式でencodeされており、そのencodeされた接頭辞に一致するすべてのパスに対して署名が有効になります。

例えば、https://media.example.com/videosをURLPrefixとして設定すると、https://media.example.com/videos/137138595のようなURLに対するリクエストが適用対象になります。

Expires

Expiresは、署名付きCookieの期限を設定します。また、Unixタイムスタンプ(1970年1月1日からの秒数)形式で指定をする必要があります。

KeyName

KeyNameは、バックエンド バケットまたはバックエンド サービスに対して作成された鍵の名前です。

Signature

Signatureは、base64urlでencodeされた、Cookieポリシーを構成するフィールドのHMAC-SHA1署名です。

Signatureを除く3つのフィールドデータを用意
// 各フィールドの設定
const cdnURL = env.CDN_URL
const signedURLKeyName = env.CDN_SIGNED_URL_KEY_NAME
const urlPrefix = `${cdnURL}/`

// urlPrefixをbase64url encode
const encodedURLPrefix = Buffer.from(urlPrefix).toString('base64url')

// 署名付きCookieの期限を設定
const currentUTCDate = new Date()
const expiration = new Date(currentUTCDate.getTime() + 30 * 24 * 60 * 60 * 1000)
const expirationUnix = Math.floor(expiration.getTime() / 1000)

// フィールドデータを作成
const input = `URLPrefix=${encodedURLPrefix}:Expires=${expirationUnix}:KeyName=${signedURLKeyName}`

まずはSignatureを除く3つのフィールドデータを用意します。

urlPrefixにはhttpsから始まるCDNのURLを指定しbase64url encodeします。そして、署名付きCookieの期限をUnixタイムスタンプ形式で設定し、それらを組み合わせてフィールドデータを作成します。

Signatureの生成

Signatureは、base64urlでencodeされた、Cookieポリシーを構成するフィールドの HMAC-SHA1署名です。

まずはsignedURLKeyを使用し、HMAC-SHA1のハッシュを生成します。

// signedURLKeyを使用し、HMAC-SHA1のハッシュを生成
const hmac = crypto.createHmac('sha1', signedURLKey)

次に用意したCookieポリシーを構成するフィールドを元に、HMACに入力データを追加し計算結果をbase64url encodeします。

// HMACの計算にデータを追加
// `URLPrefix=${encodedURLPrefix}:Expires=${expirationUnix}:KeyName=${signedURLKeyName}`
hmac.update(input)

// HMACの計算結果をbase64url encode
const sig = hmac.digest('base64url')

CookieにSetする

これまでで署名付きCookieのポリシーを作成しました。最後にこのポリシーをCookieにSetします。

// 最後にポリシーをCookieにSetする
const cookie = `${input}:Signature=${sig}`
const domainName = env.DOMAIN_NAME
c.header(
  'Set-Cookie',
  `Cloud-CDN-Cookie=${cookie}; HttpOnly; Secure; Path=/; Domain=${domainName}`
)

これで画像URLのリクエストとともに署名付きCookieがHeaderに含まれることによって、Cloud CDNでの検証に成功して無事Cloud Storageからオブジェクトを取得することができます。

まとめ

今回は、署名付きCookie(Cloud CDN)を使ってCloud Storageのオブジェクトを配信するための、署名付きCookieの発行処理に焦点を当てて解説しました。

コードの全体像は以下に掲載します。

コードの全体像

// Cloud CDNで設定された鍵をbase64url decodeする
const signedURLKey = Buffer.from(env.CDN_SIGNED_URL_KEY, 'base64url')

// 各フィールドの設定
const cdnURL = env.CDN_URL
const signedURLKeyName = env.CDN_SIGNED_URL_KEY_NAME
const urlPrefix = `${cdnURL}/`

// urlPrefixをbase64url encode
const encodedURLPrefix = Buffer.from(urlPrefix).toString('base64url')

// 署名付きCookieの期限を設定
const currentUTCDate = new Date()
const expiration = new Date(currentUTCDate.getTime() + 30 * 24 * 60 * 60 * 1000)
const expirationUnix = Math.floor(expiration.getTime() / 1000)

// フィールドデータを作成
const input = `URLPrefix=${encodedURLPrefix}:Expires=${expirationUnix}:KeyName=${signedURLKeyName}`

// signedURLKeyを使用し、HMAC-SHA1のハッシュを生成
const hmac = crypto.createHmac('sha1', signedURLKey)

// HMACの計算にデータを追加
// `URLPrefix=${encodedURLPrefix}:Expires=${expirationUnix}:KeyName=${signedURLKeyName}`
hmac.update(input)

// HMACの結果をbase64url encode
const sig = hmac.digest('base64url')

// 最後にポリシーをCookieにSetする
const cookie = `${input}:Signature=${sig}`
const domainName = env.DOMAIN_NAME
c.header(
  'Set-Cookie',
  `Cloud-CDN-Cookie=${cookie}; HttpOnly; Secure; Path=/; Domain=${domainName}`
)

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion