github.com/aavshr/aws-sdk-go@v1.41.3/service/cloudfront/sign/policy.go (about)

     1  package sign
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"crypto/rand"
     7  	"crypto/rsa"
     8  	"crypto/sha1"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"net/url"
    14  	"strings"
    15  	"time"
    16  	"unicode"
    17  )
    18  
    19  // An AWSEpochTime wraps a time value providing JSON serialization needed for
    20  // AWS Policy epoch time fields.
    21  type AWSEpochTime struct {
    22  	time.Time
    23  }
    24  
    25  // NewAWSEpochTime returns a new AWSEpochTime pointer wrapping the Go time provided.
    26  func NewAWSEpochTime(t time.Time) *AWSEpochTime {
    27  	return &AWSEpochTime{t}
    28  }
    29  
    30  // MarshalJSON serializes the epoch time as AWS Profile epoch time.
    31  func (t AWSEpochTime) MarshalJSON() ([]byte, error) {
    32  	return []byte(fmt.Sprintf(`{"AWS:EpochTime":%d}`, t.UTC().Unix())), nil
    33  }
    34  
    35  // UnmarshalJSON unserializes AWS Profile epoch time.
    36  func (t *AWSEpochTime) UnmarshalJSON(data []byte) error {
    37  	var epochTime struct {
    38  		Sec int64 `json:"AWS:EpochTime"`
    39  	}
    40  	err := json.Unmarshal(data, &epochTime)
    41  	if err != nil {
    42  		return err
    43  	}
    44  	t.Time = time.Unix(epochTime.Sec, 0).UTC()
    45  	return nil
    46  }
    47  
    48  // An IPAddress wraps an IPAddress source IP providing JSON serialization information
    49  type IPAddress struct {
    50  	SourceIP string `json:"AWS:SourceIp"`
    51  }
    52  
    53  // A Condition defines the restrictions for how a signed URL can be used.
    54  type Condition struct {
    55  	// Optional IP address mask the signed URL must be requested from.
    56  	IPAddress *IPAddress `json:"IpAddress,omitempty"`
    57  
    58  	// Optional date that the signed URL cannot be used until. It is invalid
    59  	// to make requests with the signed URL prior to this date.
    60  	DateGreaterThan *AWSEpochTime `json:",omitempty"`
    61  
    62  	// Required date that the signed URL will expire. A DateLessThan is required
    63  	// sign cloud front URLs
    64  	DateLessThan *AWSEpochTime `json:",omitempty"`
    65  }
    66  
    67  // A Statement is a collection of conditions for resources
    68  type Statement struct {
    69  	// The Web or RTMP resource the URL will be signed for
    70  	Resource string
    71  
    72  	// The set of conditions for this resource
    73  	Condition Condition
    74  }
    75  
    76  // A Policy defines the resources that a signed will be signed for.
    77  //
    78  // See the following page for more information on how policies are constructed.
    79  // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
    80  type Policy struct {
    81  	// List of resource and condition statements.
    82  	// Signed URLs should only provide a single statement.
    83  	Statements []Statement `json:"Statement"`
    84  }
    85  
    86  // Override for testing to mock out usage of crypto/rand.Reader
    87  var randReader = rand.Reader
    88  
    89  // Sign will sign a policy using an RSA private key. It will return a base 64
    90  // encoded signature and policy if no error is encountered.
    91  //
    92  // The signature and policy should be added to the signed URL following the
    93  // guidelines in:
    94  // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
    95  func (p *Policy) Sign(privKey *rsa.PrivateKey) (b64Signature, b64Policy []byte, err error) {
    96  	if err = p.Validate(); err != nil {
    97  		return nil, nil, err
    98  	}
    99  
   100  	// Build and escape the policy
   101  	b64Policy, jsonPolicy, err := encodePolicy(p)
   102  	if err != nil {
   103  		return nil, nil, err
   104  	}
   105  	awsEscapeEncoded(b64Policy)
   106  
   107  	// Build and escape the signature
   108  	b64Signature, err = signEncodedPolicy(randReader, jsonPolicy, privKey)
   109  	if err != nil {
   110  		return nil, nil, err
   111  	}
   112  	awsEscapeEncoded(b64Signature)
   113  
   114  	return b64Signature, b64Policy, nil
   115  }
   116  
   117  // Validate verifies that the policy is valid and usable, and returns an
   118  // error if there is a problem.
   119  func (p *Policy) Validate() error {
   120  	if len(p.Statements) == 0 {
   121  		return fmt.Errorf("at least one policy statement is required")
   122  	}
   123  	for i, s := range p.Statements {
   124  		if s.Resource == "" {
   125  			return fmt.Errorf("statement at index %d does not have a resource", i)
   126  		}
   127  		if !isASCII(s.Resource) {
   128  			return fmt.Errorf("unable to sign resource, [%s]. "+
   129  				"Resources must only contain ascii characters. "+
   130  				"Hostnames with unicode should be encoded as Punycode, (e.g. golang.org/x/net/idna), "+
   131  				"and URL unicode path/query characters should be escaped.", s.Resource)
   132  		}
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  // CreateResource constructs, validates, and returns a resource URL string. An
   139  // error will be returned if unable to create the resource string.
   140  func CreateResource(scheme, u string) (string, error) {
   141  	scheme = strings.ToLower(scheme)
   142  
   143  	if scheme == "http" || scheme == "https" || scheme == "http*" || scheme == "*" {
   144  		return u, nil
   145  	}
   146  
   147  	if scheme == "rtmp" {
   148  		parsed, err := url.Parse(u)
   149  		if err != nil {
   150  			return "", fmt.Errorf("unable to parse rtmp URL, err: %s", err)
   151  		}
   152  
   153  		rtmpURL := strings.TrimLeft(parsed.Path, "/")
   154  		if parsed.RawQuery != "" {
   155  			rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
   156  		}
   157  
   158  		return rtmpURL, nil
   159  	}
   160  
   161  	return "", fmt.Errorf("invalid URL scheme must be http, https, or rtmp. Provided: %s", scheme)
   162  }
   163  
   164  // NewCannedPolicy returns a new Canned Policy constructed using the resource
   165  // and expires time. This can be used to generate the basic model for a Policy
   166  // that can be then augmented with additional conditions.
   167  //
   168  // See the following page for more information on how policies are constructed.
   169  // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
   170  func NewCannedPolicy(resource string, expires time.Time) *Policy {
   171  	return &Policy{
   172  		Statements: []Statement{
   173  			{
   174  				Resource: resource,
   175  				Condition: Condition{
   176  					DateLessThan: NewAWSEpochTime(expires),
   177  				},
   178  			},
   179  		},
   180  	}
   181  }
   182  
   183  // encodePolicy encodes the Policy as JSON and also base 64 encodes it.
   184  func encodePolicy(p *Policy) (b64Policy, jsonPolicy []byte, err error) {
   185  	jsonPolicy, err = encodePolicyJSON(p)
   186  	if err != nil {
   187  		return nil, nil, fmt.Errorf("failed to encode policy, %s", err.Error())
   188  	}
   189  	// Remove leading and trailing white space, JSON encoding will note include
   190  	// whitespace within the encoding.
   191  	jsonPolicy = bytes.TrimSpace(jsonPolicy)
   192  
   193  	b64Policy = make([]byte, base64.StdEncoding.EncodedLen(len(jsonPolicy)))
   194  	base64.StdEncoding.Encode(b64Policy, jsonPolicy)
   195  	return b64Policy, jsonPolicy, nil
   196  }
   197  
   198  // signEncodedPolicy will sign and base 64 encode the JSON encoded policy.
   199  func signEncodedPolicy(randReader io.Reader, jsonPolicy []byte, privKey *rsa.PrivateKey) ([]byte, error) {
   200  	hash := sha1.New()
   201  	if _, err := bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil {
   202  		return nil, fmt.Errorf("failed to calculate signing hash, %s", err.Error())
   203  	}
   204  
   205  	sig, err := rsa.SignPKCS1v15(randReader, privKey, crypto.SHA1, hash.Sum(nil))
   206  	if err != nil {
   207  		return nil, fmt.Errorf("failed to sign policy, %s", err.Error())
   208  	}
   209  
   210  	b64Sig := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
   211  	base64.StdEncoding.Encode(b64Sig, sig)
   212  	return b64Sig, nil
   213  }
   214  
   215  // special characters to be replaced with awsEscapeEncoded
   216  var invalidEncodedChar = map[byte]byte{
   217  	'+': '-',
   218  	'=': '_',
   219  	'/': '~',
   220  }
   221  
   222  // awsEscapeEncoded will replace base64 encoding's special characters to be URL safe.
   223  func awsEscapeEncoded(b []byte) {
   224  	for i, v := range b {
   225  		if r, ok := invalidEncodedChar[v]; ok {
   226  			b[i] = r
   227  		}
   228  	}
   229  }
   230  
   231  func isASCII(u string) bool {
   232  	for _, c := range u {
   233  		if c > unicode.MaxASCII {
   234  			return false
   235  		}
   236  	}
   237  	return true
   238  }