policy.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package sign
  2. import (
  3. "bytes"
  4. "crypto"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/sha1"
  8. "encoding/base64"
  9. "encoding/json"
  10. "fmt"
  11. "io"
  12. "net/url"
  13. "strings"
  14. "time"
  15. )
  16. // An AWSEpochTime wraps a time value providing JSON serialization needed for
  17. // AWS Policy epoch time fields.
  18. type AWSEpochTime struct {
  19. time.Time
  20. }
  21. // NewAWSEpochTime returns a new AWSEpochTime pointer wrapping the Go time provided.
  22. func NewAWSEpochTime(t time.Time) *AWSEpochTime {
  23. return &AWSEpochTime{t}
  24. }
  25. // MarshalJSON serializes the epoch time as AWS Profile epoch time.
  26. func (t AWSEpochTime) MarshalJSON() ([]byte, error) {
  27. return []byte(fmt.Sprintf(`{"AWS:EpochTime":%d}`, t.UTC().Unix())), nil
  28. }
  29. // An IPAddress wraps an IPAddress source IP providing JSON serialization information
  30. type IPAddress struct {
  31. SourceIP string `json:"AWS:SourceIp"`
  32. }
  33. // A Condition defines the restrictions for how a signed URL can be used.
  34. type Condition struct {
  35. // Optional IP address mask the signed URL must be requested from.
  36. IPAddress *IPAddress `json:"IpAddress,omitempty"`
  37. // Optional date that the signed URL cannot be used until. It is invalid
  38. // to make requests with the signed URL prior to this date.
  39. DateGreaterThan *AWSEpochTime `json:",omitempty"`
  40. // Required date that the signed URL will expire. A DateLessThan is required
  41. // sign cloud front URLs
  42. DateLessThan *AWSEpochTime `json:",omitempty"`
  43. }
  44. // A Statement is a collection of conditions for resources
  45. type Statement struct {
  46. // The Web or RTMP resource the URL will be signed for
  47. Resource string
  48. // The set of conditions for this resource
  49. Condition Condition
  50. }
  51. // A Policy defines the resources that a signed will be signed for.
  52. //
  53. // See the following page for more information on how policies are constructed.
  54. // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
  55. type Policy struct {
  56. // List of resource and condition statements.
  57. // Signed URLs should only provide a single statement.
  58. Statements []Statement `json:"Statement"`
  59. }
  60. // Override for testing to mock out usage of crypto/rand.Reader
  61. var randReader = rand.Reader
  62. // Sign will sign a policy using an RSA private key. It will return a base 64
  63. // encoded signature and policy if no error is encountered.
  64. //
  65. // The signature and policy should be added to the signed URL following the
  66. // guidelines in:
  67. // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
  68. func (p *Policy) Sign(privKey *rsa.PrivateKey) (b64Signature, b64Policy []byte, err error) {
  69. if len(p.Statements) != 1 {
  70. return nil, nil, fmt.Errorf("invalid number of policy statements expected 1 got %d", len(p.Statements))
  71. }
  72. if p.Statements[0].Resource == "" {
  73. return nil, nil, fmt.Errorf("no resource in profile statement")
  74. }
  75. // Build and escape the policy
  76. b64Policy, jsonPolicy, err := encodePolicy(p)
  77. if err != nil {
  78. return nil, nil, err
  79. }
  80. awsEscapeEncoded(b64Policy)
  81. // Build and escape the signature
  82. b64Signature, err = signEncodedPolicy(randReader, jsonPolicy, privKey)
  83. if err != nil {
  84. return nil, nil, err
  85. }
  86. awsEscapeEncoded(b64Signature)
  87. return b64Signature, b64Policy, nil
  88. }
  89. // CreateResource constructs, validates, and returns a resource URL string. An
  90. // error will be returned if unable to create the resource string.
  91. func CreateResource(scheme, u string) (string, error) {
  92. scheme = strings.ToLower(scheme)
  93. if scheme == "http" || scheme == "https" {
  94. return u, nil
  95. }
  96. if scheme == "rtmp" {
  97. parsed, err := url.Parse(u)
  98. if err != nil {
  99. return "", fmt.Errorf("unable to parse rtmp URL, err: %s", err)
  100. }
  101. rtmpURL := strings.TrimLeft(parsed.Path, "/")
  102. if parsed.RawQuery != "" {
  103. rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
  104. }
  105. return rtmpURL, nil
  106. }
  107. return "", fmt.Errorf("invalid URL scheme must be http, https, or rtmp. Provided: %s", scheme)
  108. }
  109. // NewCannedPolicy returns a new Canned Policy constructed using the resource
  110. // and expires time. This can be used to generate the basic model for a Policy
  111. // that can be then augmented with additional conditions.
  112. //
  113. // See the following page for more information on how policies are constructed.
  114. // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
  115. func NewCannedPolicy(resource string, expires time.Time) *Policy {
  116. return &Policy{
  117. Statements: []Statement{
  118. {
  119. Resource: resource,
  120. Condition: Condition{
  121. DateLessThan: NewAWSEpochTime(expires),
  122. },
  123. },
  124. },
  125. }
  126. }
  127. // encodePolicy encodes the Policy as JSON and also base 64 encodes it.
  128. func encodePolicy(p *Policy) (b64Policy, jsonPolicy []byte, err error) {
  129. jsonPolicy, err = json.Marshal(p)
  130. if err != nil {
  131. return nil, nil, fmt.Errorf("failed to encode policy, %s", err.Error())
  132. }
  133. // Remove leading and trailing white space, JSON encoding will note include
  134. // whitespace within the encoding.
  135. jsonPolicy = bytes.TrimSpace(jsonPolicy)
  136. b64Policy = make([]byte, base64.StdEncoding.EncodedLen(len(jsonPolicy)))
  137. base64.StdEncoding.Encode(b64Policy, jsonPolicy)
  138. return b64Policy, jsonPolicy, nil
  139. }
  140. // signEncodedPolicy will sign and base 64 encode the JSON encoded policy.
  141. func signEncodedPolicy(randReader io.Reader, jsonPolicy []byte, privKey *rsa.PrivateKey) ([]byte, error) {
  142. hash := sha1.New()
  143. if _, err := bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil {
  144. return nil, fmt.Errorf("failed to calculate signing hash, %s", err.Error())
  145. }
  146. sig, err := rsa.SignPKCS1v15(randReader, privKey, crypto.SHA1, hash.Sum(nil))
  147. if err != nil {
  148. return nil, fmt.Errorf("failed to sign policy, %s", err.Error())
  149. }
  150. b64Sig := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
  151. base64.StdEncoding.Encode(b64Sig, sig)
  152. return b64Sig, nil
  153. }
  154. // special characters to be replaced with awsEscapeEncoded
  155. var invalidEncodedChar = map[byte]byte{
  156. '+': '-',
  157. '=': '_',
  158. '/': '~',
  159. }
  160. // awsEscapeEncoded will replace base64 encoding's special characters to be URL safe.
  161. func awsEscapeEncoded(b []byte) {
  162. for i, v := range b {
  163. if r, ok := invalidEncodedChar[v]; ok {
  164. b[i] = r
  165. }
  166. }
  167. }