/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; using Amazon.CloudFront.Model; using Amazon.Runtime; using Amazon.Util; using ThirdParty.BouncyCastle.OpenSsl; using System.Globalization; #pragma warning disable 1591 namespace Amazon.CloudFront { /// /// This utility class provides methods for creating signed URLs for /// Amazon CloudFront distributions using canned or custom policies. /// public static class AmazonCloudFrontUrlSigner { public enum Protocol { http, https, rtmp } /// /// Returns a signed URL that grants universal access to private content until a given date. /// /// The protocol of the URL /// The domain name of the distribution /// The path for the resource, or the name of the stream for rtmp /// The private key file. RSA private key (.pem) are supported. /// The key pair id corresponding to the private key file given. /// The expiration date of the signed URL /// The signed URL. public static string GetCannedSignedURL(Protocol protocol, string distributionDomain, FileSystemInfo privateKey, string resourcePath, string keyPairId, DateTime expiresOn) { using (StreamReader reader = new StreamReader(File.OpenRead(privateKey.FullName))) { return GetCannedSignedURL(protocol, distributionDomain, reader, resourcePath, keyPairId, expiresOn); } } /// /// Returns a signed URL that grants universal access to private content until a given date. /// /// The protocol of the URL /// The domain name of the distribution /// The path for the resource, or the name of the stream for rtmp /// The private key file. RSA private key (.pem) are supported. /// The key pair id corresponding to the private key file given. /// The expiration date of the signed URL /// The signed URL. public static string GetCannedSignedURL(Protocol protocol, string distributionDomain, TextReader privateKey, string resourcePath, string keyPairId, DateTime expiresOn) { string url = GenerateResourcePath(protocol, distributionDomain, resourcePath); return GetCannedSignedURL(url, privateKey, keyPairId, expiresOn); } /// /// Returns a signed URL that grants universal access to private content until a given date. /// /// The full url (protocol + domain + resource path) to the resource, or the name of the stream for rtmp /// The private key file. RSA private key (.pem) are supported. /// The key pair id corresponding to the private key file given. /// The expiration date of the signed URL /// The signed URL. public static string GetCannedSignedURL(string url, TextReader privateKey, string keyPairId, DateTime expiresOn) { string signedUrlCanned = SignUrlCanned(url, keyPairId, privateKey, expiresOn); return signedUrlCanned; } /// /// Returns a signed URL that provides tailored access to private content based on an access time window and an ip range. /// /// The protocol of the URL /// The domain name of the distribution /// Your private key file. RSA private key (.pem) are supported. /// The path for the resource, or the name of the stream for rtmp /// The key pair id corresponding to the private key file given /// The expiration date of the signed URL /// The beginning valid date of the signed URL /// The allowed IP address range of the client making the GET request, in CIDR form (e.g. 192.168.0.1/24). /// The signed URL. public static string GetCustomSignedURL(Protocol protocol, string distributionDomain, FileSystemInfo privateKey, string resourcePath, string keyPairId, DateTime expiresOn, DateTime activeFrom, string ipRange) { using (StreamReader reader = new StreamReader(File.OpenRead(privateKey.FullName))) { return GetCustomSignedURL(protocol, distributionDomain, reader, resourcePath, keyPairId, expiresOn, activeFrom, ipRange); } } /// /// Returns a signed URL that provides tailored access to private content based on an access time window and an ip range. /// /// The protocol of the URL /// The domain name of the distribution /// Your private key file. RSA private key (.pem) are supported. /// The path for the resource, or the name of the stream for rtmp /// The key pair id corresponding to the private key file given /// The expiration date of the signed URL /// The beginning valid date of the signed URL /// The allowed IP address range of the client making the GET request, in CIDR form (e.g. 192.168.0.1/24). /// The signed URL. public static string GetCustomSignedURL(Protocol protocol, string distributionDomain, TextReader privateKey, string resourcePath, string keyPairId, DateTime expiresOn, DateTime activeFrom, string ipRange) { string path = GenerateResourcePath(protocol, distributionDomain, resourcePath); return GetCustomSignedURL(path, privateKey, keyPairId, expiresOn, activeFrom, ipRange); } /// /// Returns a signed URL that provides tailored access to private content based on an access time window and an ip range. /// /// The protocol of the URL /// The domain name of the distribution /// Your private key file. RSA private key (.pem) are supported. /// The path for the resource, or the name of the stream for rtmp /// The key pair id corresponding to the private key file given /// The expiration date of the signed URL /// The allowed IP address range of the client making the GET request, in CIDR form (e.g. 192.168.0.1/24). /// The signed URL. public static string GetCustomSignedURL(Protocol protocol, string distributionDomain, TextReader privateKey, string resourcePath, string keyPairId, DateTime expiresOn, string ipRange) { string path = GenerateResourcePath(protocol, distributionDomain, resourcePath); return GetCustomSignedURL(path, privateKey, keyPairId, expiresOn, ipRange); } /// /// Returns a signed URL that provides tailored access to private content based on an access time window and an ip range. /// /// The protocol of the URL /// Your private key file. RSA private key (.pem) are supported. /// The key pair id corresponding to the private key file given /// The expiration date of the signed URL /// The beginning valid date of the signed URL /// The allowed IP address range of the client making the GET request, in CIDR form (e.g. 192.168.0.1/24). /// The signed URL. public static string GetCustomSignedURL(string url, TextReader privateKey, string keyPairId, DateTime expiresOn, DateTime activeFrom, string ipRange) { string policy = BuildPolicyForSignedUrl(url, expiresOn, ipRange, activeFrom); return SignUrl(url, keyPairId, privateKey, policy); } /// /// Returns a signed URL that provides tailored access to private content based on an access time window and an ip range. /// /// The protocol of the URL /// Your private key file. RSA private key (.pem) are supported. /// The key pair id corresponding to the private key file given /// The expiration date of the signed URL /// The allowed IP address range of the client making the GET request, in CIDR form (e.g. 192.168.0.1/24). /// The signed URL. public static string GetCustomSignedURL(string url, TextReader privateKey, string keyPairId, DateTime expiresOn, string ipRange) { string policy = BuildPolicyForSignedUrl(url, expiresOn, ipRange); return SignUrl(url, keyPairId, privateKey, policy); } /// /// Generate a signed URL that allows access to distribution and resource path /// by applying access restrictions specified in a custom policy document. /// /// /// The URL or path that uniquely identifies a resource within a /// distribution. For standard distributions the resource URL will /// be "http://" + distributionName + "/" + path /// (may also include URL parameters. For distributions with the /// HTTPS required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. /// /// Identifier of a public/private certificate keypair already configured in your Amazon Web Services account. /// The RSA private key data that corresponding to the certificate keypair identified by keyPairId. /// A policy document that describes the access permissions that will be applied by /// the signed URL. /// A signed URL that will permit access to distribution and resource path as specified in the policy document. public static string SignUrl(string resourceUrlOrPath, string keyPairId, FileInfo privateKey, string policy) { using (StreamReader reader = new StreamReader(File.OpenRead(privateKey.FullName))) { return SignUrl(resourceUrlOrPath, keyPairId, reader, policy); } } /// /// Generate a signed URL that allows access to distribution and resource path /// by applying access restrictions specified in a custom policy document. /// /// /// The URL or path that uniquely identifies a resource within a /// distribution. For standard distributions the resource URL will /// be "http://" + distributionName + "/" + path /// (may also include URL parameters. For distributions with the /// HTTPS required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. /// /// Identifier of a public/private certificate keypair already configured in your Amazon Web Services account. /// The RSA private key data that corresponding to the certificate keypair identified by keyPairId. /// A policy document that describes the access permissions that will be applied by /// the signed URL. /// A signed URL that will permit access to distribution and S3 objects as specified in the policy document. public static string SignUrl(string resourceUrlOrPath, string keyPairId, TextReader privateKey, string policy) { RSAParameters rsaParameters = ConvertPEMToRSAParameters(privateKey); byte[] signatureBytes = SignWithSha1RSA(UTF8Encoding.UTF8.GetBytes(policy), rsaParameters); string urlSafePolicy = MakeStringUrlSafe(policy); string urlSafeSignature = MakeBytesUrlSafe(signatureBytes); string signedUrl = resourceUrlOrPath + (resourceUrlOrPath.IndexOf('?') >= 0 ? "&" : "?") + "Policy=" + urlSafePolicy + "&Signature=" + urlSafeSignature + "&Key-Pair-Id=" + keyPairId; return signedUrl; } /// /// Generate a signed URL that allows access to a specific distribution and /// resource path by applying a access restrictions from a "canned" (simplified) /// policy document. /// /// /// The URL or path that uniquely identifies a resource within a /// distribution. For standard distributions the resource URL will /// be "http://" + distributionName + "/" + path /// (may also include URL parameters. For distributions with the /// HTTPS required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. /// /// Identifier of a public/private certificate keypair already configured in your Amazon Web Services account. /// The RSA private key data that corresponding to the certificate keypair identified by keyPairId. /// The time and date when the signed URL will expire. public static String SignUrlCanned(string resourceUrlOrPath, string keyPairId, FileInfo privateKey, DateTime expiresOn) { using (StreamReader reader = new StreamReader(File.OpenRead(privateKey.FullName))) { return SignUrlCanned(resourceUrlOrPath, keyPairId, reader, expiresOn); } } /// /// Generate a signed URL that allows access to a specific distribution and /// resource path by applying a access restrictions from a "canned" (simplified) /// policy document. /// /// /// The URL or path that uniquely identifies a resource within a /// distribution. For standard distributions the resource URL will /// be "http://" + distributionName + "/" + path /// (may also include URL parameters. For distributions with the /// HTTPS required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. /// /// Identifier of a public/private certificate keypair already configured in your Amazon Web Services account. /// The RSA private key data that corresponding to the certificate keypair identified by keyPairId. /// The time and date when the signed URL will expire. public static String SignUrlCanned(string resourceUrlOrPath, string keyPairId, TextReader privateKey, DateTime expiresOn) { string epochSeconds = AWSSDKUtils.ConvertToUnixEpochSecondsString(expiresOn.ToUniversalTime()); RSAParameters rsaParameters = ConvertPEMToRSAParameters(privateKey); string cannedPolicy = "{\"Statement\":[{\"Resource\":\"" + resourceUrlOrPath + "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":" + epochSeconds + "}}}]}"; byte[] signatureBytes = SignWithSha1RSA(UTF8Encoding.UTF8.GetBytes(cannedPolicy), rsaParameters); string urlSafeSignature = MakeBytesUrlSafe(signatureBytes); string signedUrl = resourceUrlOrPath + (resourceUrlOrPath.IndexOf('?') >= 0 ? "&" : "?") + "Expires=" + epochSeconds + "&Signature=" + urlSafeSignature + "&Key-Pair-Id=" + keyPairId; return signedUrl; } /// /// Generate a policy document that describes custom access permissions to /// apply via a private distribution's signed URL. /// /// /// An optional HTTP/S or RTMP resource path that restricts which /// distribution and S3 objects will be accessible in a signed /// URL. For standard distributions the resource URL will be /// "http://" + distributionName + "/" + path (may /// also include URL parameters. For distributions with the HTTPS /// required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. The '*' and '?' characters can be used as a wildcards to /// allow multi-character or single-character matches /// respectively: /// /// If this parameter is null the policy will permit access to all /// distributions and S3 objects associated with the certificate /// keypair used to generate the signed URL. /// /// The time and date when the signed URL will expire. /// An optional range of client IP addresses that will be allowed /// to access the distribution, specified as a CIDR range. If /// null or empty any client will be permitted. /// An optional UTC time and date when the signed URL will become /// active. A value of DateTime.MinValue (the default value of DateTime) is ignored. /// /// A policy document describing the access permission to apply when /// generating a signed URL. public static string BuildPolicyForSignedUrl(string resourcePath, DateTime expiresOn, string limitToIpAddressCIDR, DateTime activeFrom) { // Reference: // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement // Validate if activeFrom (time at which the URL will be active) // is less than expiresOn (time at which the URL will expire) if (activeFrom > expiresOn) { throw new AmazonClientException("The parameter activeFrom (time at which the URL will be active)" + "must be less than expiresOn (time at which the URL will expire)."); } if (resourcePath == null) { resourcePath = "*"; } string policy = "{\"Statement\": [{" + "\"Resource\":\"" + resourcePath + "\"" + ",\"Condition\":{" + "\"DateLessThan\":{\"AWS:EpochTime\":" + AWSSDKUtils.ConvertToUnixEpochSecondsString(expiresOn.ToUniversalTime()) + "}" // omitting IpAddress parameter indicates any ip address access + (string.IsNullOrEmpty(limitToIpAddressCIDR) ? "" : ",\"IpAddress\":{\"AWS:SourceIp\":\"" + limitToIpAddressCIDR + "\"}") // Ignore epochDateGreaterThan if its value is DateTime.MinValue, the default value of DateTime. + (activeFrom > DateTime.MinValue ? ",\"DateGreaterThan\":{\"AWS:EpochTime\":" + AWSSDKUtils.ConvertToUnixEpochSecondsString(activeFrom.ToUniversalTime()) + "}" : string.Empty) + "}}]}"; return policy; } /// /// Generate a policy document that describes custom access permissions to /// apply via a private distribution's signed URL. /// /// /// An optional HTTP/S or RTMP resource path that restricts which /// distribution and resource path will be accessible in a signed /// URL. For standard distributions the resource URL will be /// "http://" + distributionName + "/" + path (may /// also include URL parameters. For distributions with the HTTPS /// required protocol, the resource URL must start with /// "https://". RTMP resources do not take the form of a /// URL, and instead the resource path is nothing but the stream's /// name. The '*' and '?' characters can be used as a wildcards to /// allow multi-character or single-character matches /// respectively: /// /// If this parameter is null the policy will permit access to all /// distributions and resource path associated with the certificate /// keypair used to generate the signed URL. /// /// The time and date when the signed URL will expire. /// An optional range of client IP addresses that will be allowed /// to access the distribution, specified as a CIDR range. If null, or empty, any client will /// be permitted. /// A policy document describing the access permission to apply when /// generating a signed URL. public static string BuildPolicyForSignedUrl(string resourcePath, DateTime expiresOn, string limitToIpAddressCIDR) { return BuildPolicyForSignedUrl(resourcePath, expiresOn, limitToIpAddressCIDR, DateTime.MinValue); } /// /// Converts the given data to be safe for use in signed URLs for a private /// distribution by using specialized Base64 encoding. /// internal static string MakeBytesUrlSafe(byte[] bytes) { return Convert.ToBase64String(bytes).Replace('+', '-').Replace('=', '_').Replace('/', '~'); } /// /// Converts the given string to be safe for use in signed URLs for a private distribution. /// internal static String MakeStringUrlSafe(string str) { return MakeBytesUrlSafe(UTF8Encoding.UTF8.GetBytes(str)); } /// /// Returns the resource path for the given distribution, object, and protocol. /// private static string GenerateResourcePath(Protocol protocol, string distributionDomain, string path) { if (protocol == Protocol.http || protocol == Protocol.https) { return protocol.ToString() + "://" + distributionDomain + "/" + path; } else { return path; } } /// /// Signs the data given with the private key given, using the SHA1withRSA /// algorithm provided by bouncy castle. /// internal static byte[] SignWithSha1RSA(byte[] dataToSign, RSAParameters rsaParameters) { using (SHA1 cryptoSHA1 = GetSHA1Provider()) { var providerRSA = RSA.Create(); providerRSA.ImportParameters(rsaParameters); byte[] hashedData = cryptoSHA1.ComputeHash(dataToSign); return GetRSAPKCS1SignatureFromSHA1(hashedData, providerRSA); } } internal static RSAParameters ConvertPEMToRSAParameters(TextReader privateKeyReader) { RSAParameters rsaParams; try { rsaParams = new PemReader(privateKeyReader).ReadPrivatekey(); } catch (Exception e) { throw new AmazonClientException("Invalid RSA Private Key", e); } var rsa = RSA.Create(); rsa.ImportParameters(rsaParams); return rsaParams; } private static SHA1 GetSHA1Provider() { #if NETSTANDARD return SHA1.Create(); #else return new SHA1CryptoServiceProvider(); #endif } private static byte[] GetRSAPKCS1SignatureFromSHA1(byte[] hashedData, RSA providerRSA) { // Format the RSACryptoServiceProvider and create the signature. #if NETSTANDARD return providerRSA.SignHash(hashedData, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); #else RSAPKCS1SignatureFormatter rsaFormatter = new RSAPKCS1SignatureFormatter(providerRSA); rsaFormatter.SetHashAlgorithm("SHA1"); return rsaFormatter.CreateSignature(hashedData); #endif } } }