policy.go 6.8 KB

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