7167: move perms code from keepstore into keepclient go SDK.
[arvados.git] / sdk / go / keepclient / perms.go
1 /*
2 Permissions management on Arvados locator hashes.
3
4 The permissions structure for Arvados is as follows (from
5 https://arvados.org/issues/2328)
6
7 A Keep locator string has the following format:
8
9     [hash]+[size]+A[signature]@[timestamp]
10
11 The "signature" string here is a cryptographic hash, expressed as a
12 string of hexadecimal digits, and timestamp is a 32-bit Unix timestamp
13 expressed as a hexadecimal number.  e.g.:
14
15     acbd18db4cc2f85cedef654fccc4a4d8+3+A257f3f5f5f0a4e4626a18fc74bd42ec34dcb228a@7fffffff
16
17 The signature represents a guarantee that this locator was generated
18 by either Keep or the API server for use with the supplied API token.
19 If a request to Keep includes a locator with a valid signature and is
20 accompanied by the proper API token, the user has permission to GET
21 that object.
22
23 The signature may be generated either by Keep (after the user writes a
24 block) or by the API server (if the user has can_read permission on
25 the specified object). Keep and API server share a secret that is used
26 to generate signatures.
27
28 To verify a permission hint, Keep generates a new hint for the
29 requested object (using the locator string, the timestamp, the
30 permission secret and the user's API token, which must appear in the
31 request headers) and compares it against the hint included in the
32 request. If the permissions do not match, or if the API token is not
33 present, Keep returns a 401 error.
34 */
35
36 package keepclient
37
38 import (
39         "crypto/hmac"
40         "crypto/sha1"
41         "fmt"
42         "regexp"
43         "strconv"
44         "strings"
45         "time"
46 )
47
48 // KeepError types.
49 //
50 type KeepError struct {
51         HTTPCode int
52         ErrMsg   string
53 }
54
55 var (
56         PermissionError = &KeepError{403, "Forbidden"}
57         ExpiredError    = &KeepError{401, "Expired permission signature"}
58 )
59
60 func (e *KeepError) Error() string {
61         return e.ErrMsg
62 }
63
64 // makePermSignature returns a string representing the signed permission
65 // hint for the blob identified by blobHash, apiToken, expiration timestamp, and permission secret.
66 //
67 // The permissionSecret is the secret key used to generate SHA1 digests
68 // for permission hints. apiserver and Keep must use the same key.
69 func makePermSignature(blobHash string, apiToken string, expiry string, permissionSecret []byte) string {
70         hmac := hmac.New(sha1.New, permissionSecret)
71         hmac.Write([]byte(blobHash))
72         hmac.Write([]byte("@"))
73         hmac.Write([]byte(apiToken))
74         hmac.Write([]byte("@"))
75         hmac.Write([]byte(expiry))
76         digest := hmac.Sum(nil)
77         return fmt.Sprintf("%x", digest)
78 }
79
80 // SignLocator takes a blobLocator, an apiToken, an expiry time, and a permission secret
81 // and returns a signed locator string.
82 func SignLocator(blobLocator string, apiToken string, expiry time.Time, permissionSecret []byte) string {
83         // If no permission secret or API token is available,
84         // return an unsigned locator.
85         if permissionSecret == nil || apiToken == "" {
86                 return blobLocator
87         }
88         // Extract the hash from the blob locator, omitting any size hint that may be present.
89         blobHash := strings.Split(blobLocator, "+")[0]
90         // Return the signed locator string.
91         timestampHex := fmt.Sprintf("%08x", expiry.Unix())
92         return blobLocator +
93                 "+A" + makePermSignature(blobHash, apiToken, timestampHex, permissionSecret) +
94                 "@" + timestampHex
95 }
96
97 var signedLocatorRe = regexp.MustCompile(`^([[:xdigit:]]{32}).*\+A([[:xdigit:]]{40})@([[:xdigit:]]{8})`)
98
99 // VerifySignature returns nil if the signature on the signedLocator
100 // can be verified using the given apiToken. Otherwise it returns
101 // either ExpiredError (if the timestamp has expired, which is
102 // something the client could have figured out independently) or
103 // PermissionError.
104 func VerifySignature(signedLocator string, apiToken string, permissionSecret []byte) error {
105         matches := signedLocatorRe.FindStringSubmatch(signedLocator)
106         if matches == nil {
107                 // Could not find a permission signature at all
108                 return PermissionError
109         }
110         blobHash := matches[1]
111         sigHex := matches[2]
112         expHex := matches[3]
113         if expTime, err := parseHexTimestamp(expHex); err != nil {
114                 return PermissionError
115         } else if expTime.Before(time.Now()) {
116                 return ExpiredError
117         }
118         if sigHex != makePermSignature(blobHash, apiToken, expHex, permissionSecret) {
119                 return PermissionError
120         }
121         return nil
122 }
123
124 // parseHexTimestamp parses timestamp
125 func parseHexTimestamp(timestampHex string) (ts time.Time, err error) {
126         if tsInt, e := strconv.ParseInt(timestampHex, 16, 0); e == nil {
127                 ts = time.Unix(tsInt, 0)
128         } else {
129                 err = e
130         }
131         return ts, err
132 }