CloudFront のプライベートコンテンツ配信を試した (Kotlin で signed cookie を生成する)
- AWS の CDN である CloudFront でプライベートコンテンツを配信する方法として、signed URL を用いる方法と、signed cookie を用いる方法がある。
- 本記事では、signed cookie の生成処理を Kotlin (Java) で行う方法を説明する。
事前準備 (CloudFront 側の準備)
- アクセスする側の挙動を確認するために、試験的に CloudFront 側も準備したのでその手順を書いておく。 (AWS コンソール上で操作を行った。)
- 内容は次の 2 つ。
- CloudFront のディストリビューションを用意。
- CloudFront でコンテンツへのアクセスを制限する。
CloudFront のディストリビューションを用意
- 参考 : ディストリビューションを作成するためのステップ (概要) - Amazon CloudFront
- Delivery メソッドとして Web を選択。 (RTMP というのもあって、これはリアルタイムのストリーミングとかに使われるっぽい。 HTTP(S) での CDN として使うなら Web。)
- Origin としては適当なドメインの HTTP サーバーを指定した。
- → この段階では、新たに作成されたディストリビューションに割り当てられているドメインにアクセスすると普通にオリジンの内容が配信されることが確認できる。
コンテンツへのアクセスを制限する
キーペアの作成と CloudFront への登録
CloudFront にキーペアを登録する方法としては、ローカルマシン上で作成して公開鍵をアップロードするか、AWS コンソール上で作成して秘密鍵をダウンロードするかのどちらかとのこと。 今回はローカルマシン上で作成した。 コマンドは上記参考ページに載っている。
# 秘密鍵の作成 openssl genrsa -out private_key.pem 4096 # 公開鍵の作成 openssl rsa -pubout -in private_key.pem -out public_key.pem # Java で使う用に秘密鍵を DER 形式に変換 openssl pkcs8 -topk8 -nocrypt -in private_key.pem -inform PEM -out cloud_front_key.der -outform DER
アカウントメニューから 「セキュリティ認証情報」 画面に行くと 「CloudFront のキーペア」 という項目があるので、ここで公開鍵をアップロードする。 登録された鍵のアクセスキー ID と、上のコマンドで作成された DER 形式の鍵が後で必要になる。
信頼された署名者をディストリビューションに追加する
上で登録した鍵を使って signed cookie を生成してプライベートコンテンツにアクセスできるように、AWS アカウントを信頼された署名者として CloudFront のディストリビューションに登録する。
(ちなみにディストリビューションを作成した AWS アカウント以外のアカウントも登録できるらしい。 今回は自分自身を登録する。)
この段階で、signed cookie や signed url を使わないアクセスはエラーになるようになる。
Signed cookie を生成してプライベートコンテンツにアクセスする (Kotlin)
上で生成された DER 形式の秘密鍵とアクセスキーを使うことで、下記のようなコードでプライベートコンテンツにアクセスできる。 (下記では Java SE 11 から新たに追加された API (java.net.http
モジュール) を使用しているので、Java SE 11 でなければ動かない。)
import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.file.Files import java.nio.file.Paths import java.security.KeyFactory import java.security.PrivateKey import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec import java.time.Instant import java.time.Period import java.util.* // CloudFront のドメイン名 private val cloudFrontDomainName = "xxxxx.cloudfront.net" // AWS コンソール上に表示されている CloudFront のキーペアのアクセス ID private const val cloudFrontKeyPairId = "xxxx" // 自分で生成した DER 形式の秘密鍵 private const val cloudFrontKeyPairFile = "cloud_front_key.der" fun main() { // ポリシーステートメントを用意。 val endingDate = Instant.now().plus(Period.ofDays(1)) val cloudFrontPolicyStatement = """ { "Statement": [ { "Resource": "http://$cloudFrontDomainName/", "Condition": { "DateLessThan": { "AWS:EpochTime": ${endingDate.epochSecond} } } } ] } """.trimIndent() // CloudFront に登録された鍵の情報を用意。 val keyFactory = KeyFactory.getInstance("RSA") // すべての Java 実装で RSA がサポートされる。 Java SE 8 のドキュメントで確認。 val cloudFrontKeySpec = PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(cloudFrontKeyPairFile))) val cloudFrontPrivateKey = keyFactory.generatePrivate(cloudFrontKeySpec) val cloudFrontKey = CloudFrontKey(cloudFrontKeyPairId, cloudFrontPrivateKey) // Signed cookie の値を生成。 val cloudFrontCookieValue = CloudFrontSignedCookieValue.create(cloudFrontPolicyStatement, cloudFrontKey) // Signed cookie を使用してプライベートコンテンツにアクセス。 val targetUri = URI.create("http://$cloudFrontDomainName/") val response = request(targetUri, HttpResponse.BodyHandlers.ofString(), cloudFrontCookieValue) println(response.body()) } fun <T> request( uri: URI, responseBodyHandler: HttpResponse.BodyHandler<T>, cloudFrontCookieValue: CloudFrontSignedCookieValue ): HttpResponse<T> { val client = HttpClient.newBuilder() .build() val request = HttpRequest.newBuilder() .uri(uri) .headers( "Cookie", "CloudFront-Policy=${cloudFrontCookieValue.policy}", "Cookie", "CloudFront-Signature=${cloudFrontCookieValue.signature}", "Cookie", "CloudFront-Key-Pair-Id=${cloudFrontCookieValue.keyPairId}" ) .build() return client.send(request, responseBodyHandler) } data class CloudFrontSignedCookieValue( val policy: String, val signature: String, val keyPairId: String ) { companion object { fun create(policyStatement: String, key: CloudFrontKey): CloudFrontSignedCookieValue { val canonicalPolicyStatement = Regex("\\s").replace(policyStatement, "") val encodedPolicyStatement = encodeForCookieValue(canonicalPolicyStatement.toByteArray()) val encodedSignature = sign(canonicalPolicyStatement.toByteArray(), key.privateKey) return CloudFrontSignedCookieValue(encodedPolicyStatement, encodedSignature, key.keyPairId) } private fun sign(base: ByteArray, privateKey: PrivateKey): String { val signatureInstance = Signature.getInstance("SHA1withRSA") // すべての Java 実装で SHA1withRSA がサポートされる。 Java SE 8 のドキュメントで確認。 signatureInstance.initSign(privateKey) signatureInstance.update(base) val signature = signatureInstance.sign() return encodeForCookieValue(signature) } private fun encodeForCookieValue(value: ByteArray) = Base64.getEncoder().encodeToString(value) .replace('+', '-') .replace('=', '_') .replace('/', '~') } } data class CloudFrontKey( val keyPairId: String, val privateKey: PrivateKey )
AWS のドキュメントには 『エンコーダが正常に機能するように、Bouncy Castle の Java 用暗号 API の jar をプロジェクトに追加してから Bouncy Castle プロバイダを追加します。』 と書かれているが、Java SE に含まれるエンコーダだけで問題なく署名できるはず。
AWS SDK for Java
AWS SDK for Java に CloudFrontCookieSigner
というクラスがあり、これを使うことで signed cookie を生成することも可能。 通常はこれを使うのが良さそう。