/*
* 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:
///
/// - * : All distributions/objects will be accessible
/// - a1b2c3d4e5f6g7.cloudfront.net/* : All objects
/// within the distribution a1b2c3d4e5f6g7 will be accessible
/// - a1b2c3d4e5f6g7.cloudfront.net/path/to/object.txt
/// : Only the S3 object named path/to/object.txt in the
/// distribution a1b2c3d4e5f6g7 will be accessible.
///
/// 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:
///
/// - * : All distributions/objects will be accessible
/// - a1b2c3d4e5f6g7.cloudfront.net/* : All objects
/// within the distribution a1b2c3d4e5f6g7 will be accessible
/// - a1b2c3d4e5f6g7.cloudfront.net/path/to/object.txt
/// : Only the resource named path/to/object.txt in the
/// distribution a1b2c3d4e5f6g7 will be accessible.
///
/// 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
}
}
}