すらいむがあらわれた

こまんど >  たたかう  にげる

Amazon CloudFrontにアクセス制限付きURLを発行する(その2)

昨日の続きです。

準備は整っているのでtrusted signer付きのCloudFront Distributionを作成します。


AWSのドキュメントではこのあたりのページで手順が紹介されています。
http://docs.amazonwebservices.com/AmazonCloudFront/latest/DeveloperGuide/RestrictingAccessPrivateContent.html#CFPolicyStatementSigners


ここでは今までと同様にAWS SDK for .NETを使用してやってみます。

        public AmazonCloudFront cfClient = Amazon.AWSClientFactory.CreateAmazonCloudFrontClient(accessKeyID, secretAccessKeyID);

        public void CreateCloudFrontDistributionWithTrustedSinger()
        {
            try
            {
                CreateDistributionRequest req = new CreateDistributionRequest();

                CloudFrontOriginAccessIdentity oai = new CloudFrontOriginAccessIdentity();
                oai.Id = "YOUR_OAI_ID";
                oai.S3CanonicalUserId = "YUOR_S3_CANONICAL_USER_ID";

                UrlTrustedSigners trustedSigners = new UrlTrustedSigners();
                trustedSigners.EnableSelf = true;
                // 引数の型はparams string[]なので複数のAccount Numberを渡せます
                trustedSigners.WithAwsAccountNumbers("ACCOUNT_NUMBER");

                CloudFrontDistributionConfig config = new CloudFrontDistributionConfig();

                config.S3Origin = new S3Origin("YOUR_S3_DOMAIN_NAME", oai);
                config.Enabled = true;
                config.TrustedSigners = trustedSigners;

                req.DistributionConfig = config;

                var response = cfClient.CreateDistribution(req);

                String xml = response.XML;
                Console.WriteLine(xml);
            }
            catch (AmazonCloudFrontException cfEx)
            {
                Console.WriteLine("An Error, number {0}, occurred when create cloud distribution with the message '{1}", cfEx.ErrorCode, cfEx.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine("UnknownError:{0}", ex.Message);
            }
        }

Account Numberはアカウントのページの右上に出ている数字です。プログラム中ではハイフンなしで使います。
https://aws-portal.amazon.com/gp/aws/developer/account
AWS-AccountNumber
AWS-AccountNumber posted by (C)kanpan


上記のコードの実行結果として以下のようなXMLを取得できます。

<?xml version="1.0"?>
<Distribution xmlns="http://cloudfront.amazonaws.com/doc/2010-11-01/">
	<Id>E30GMJ0MI621X7</Id>
	<Status>InProgress</Status>
	<LastModifiedTime>2011-12-25T10:42:15.113Z</LastModifiedTime>
	<InProgressInvalidationBatches>0</InProgressInvalidationBatches>
	<DomainName>YOUR_CLOUDFRONT_DOMAIN_NAME</DomainName>
	<ActiveTrustedSigner>
		<Signer>
			<Self></Self>
			<KeyPairId>YOUR_KEYPAIR_ID</KeyPairId>
		</Signer>
	</ActiveTrustedSigners>
	<DistributionConfig>
		<S3Origin>
			<DNSName>YOUR_S3_DOMAIN_NAME</DNSName>
			<OriginAccessIdentity>origin-accessidentity/cloudfront/XXXXXXXXX</OriginAccessIdentity>
		</S3Origin>
		<CallerReference>2011/12/25 10:42:12</CallerReference>
		<Enabled>true</Enabled>
		<TrustedSigners>
			<Self></Self>
		</TrustedSigners>
	</DistributionConfig>
</Distribution>

に注意。CloudFrontのアカウントにKey Pairが1つも紐づいていないと値が返ってきません。このKeyPair IDは署名付きURLを作成するのに必要です。


署名付きURLの作成は以下の記事を参考にしました。
Policy Signing in C# for Streaming Private Content From Amazon CloudFront - Bill Beckelman
http://beckelman.net/post/2010/03/30/Policy-Signing-in-C-for-Streaming-Private-Content-From-Amazon-CloudFront.aspx
How to encrypt Amazon CloudFront signature for private content access using canned policy - stackoverflow
http://stackoverflow.com/questions/2284206/how-to-encrypt-amazon-cloudfront-signature-for-private-content-access-using-cann


まずCloudFrontに紐づいている秘密鍵からXML文字列を生成します。
以下のページから「Opensslkey.exe」
http://www.jensign.com/opensslkey/index.html
というツールをダウンロードします。


以下のように秘密鍵を渡してXML文字列を生成します。

>opensslkey.exe

RSA public, private or PKCS #8 key file to decode: pk-**************.pem
Trying to decrypt and parse a PEM private key ..
showing components ..

Created an RSACryptoServiceProvider instance


XML RSA private key: 1024 bits

......
(略)
......


URLを生成するコード。

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;

namespace AWSTest
{
    internal class CloudFrontSecurityProvider
    {
        private readonly RSACryptoServiceProvider privateKey;
        private readonly string privateKeyId;
        private readonly SHA1Managed sha1 = new SHA1Managed();

        public CloudFrontSecurityProvider(string privateKeyId, string privateKey)
        {
            this.privateKey = new RSACryptoServiceProvider();
            RSACryptoServiceProvider.UseMachineKeyStore = false;

            this.privateKey.FromXmlString(privateKey);
            this.privateKeyId = privateKeyId;
        }
        private static int GetUnixTime(DateTime time)
        {
            DateTime referenceTime = new DateTime(1970, 1, 1);
            return (int)(time - referenceTime).TotalSeconds;

        }

        public string GetCannedUrl(string url, DateTime expiration)
        {

            string expirationEpoch = GetUnixTime(expiration).ToString();

            string policy =
                @"{""Statement"":[{""Resource"":""<url>"",""Condition"":{""DateLessThan"":{""AWS:EpochTime"":<expiration>}}}]}".
                    Replace("<url>", url).
                    Replace("<expiration>", expirationEpoch);


            string signature = GetUrlSafeString(Sign(policy));

            return url + string.Format("?Expires={0}&Signature={1}&Key-Pair-Id={2}", expirationEpoch, signature, this.privateKeyId);
        }

        private static string GetUrlSafeString(byte[] data)
        {
            return Convert.ToBase64String(data).Replace('+', '-').Replace('=', '_').Replace('/', '~');
        }

        private byte[] Sign(string data)
        {
            byte[] plainbytes = Encoding.UTF8.GetBytes(data);

            byte[] hash = sha1.ComputeHash(plainbytes);

            return this.privateKey.SignHash(hash, "SHA1");
        }
    }
}

上記のコードを呼び出してURLを生成します。
呼び出し時にKeyPair IDと秘密鍵XML文字列を渡します。
/images/Test.jpg アクセス、制限時間を5分に設定。

        public void CreateCannedPolicyUrl()
        {
            String privateKey =
                 "<RSAKeyValue>.....(略).....</RSAKeyValue>";
            String privateKeyId = "YOUR_KEYPAIR_ID";

            CloudFrontSecurityProvider provider = new CloudFrontSecurityProvider(privateKeyId, privateKey);

            // /images/Test.jpgにアクセス
            // 制限時間5分
            String cannedSignedUrl = provider.GetCannedUrl("http://xxxxxx.cloudfront.net/images/Test.jpg", DateTime.UtcNow.AddMinutes(5));

            Console.WriteLine(cannedSignedUrl);

        }


すると以下のようなURLを取得できます。
画像に5分間だけアクセスできます。
制限時間がすぎると「Access denied」になります。

http://xxxxxxxxxx.cloudfront.net/images/Test.jpg?Expires=1324813302&Signature=KFSN4bAAlldm8hSJ3LMzVFAIOMIm~hbooy1N1ohh9~51NZNQxwPkra~aFY3mOFjwuRvD3iL8AXPIEdWlE2SgusXErBzs6gC2p-pKJ2vuT85iPmz4ECMMYOIB5CAuNogiOotVKvOkHP3-u-akcKJ88gM2mVgI--7tqBaGbuVSj-k_&Key-Pair-Id=APKAIR7NQW6SFUO2XXXX