7167: move perms code from keepstore into keepclient go SDK.
authorradhika <radhika@curoverse.com>
Wed, 7 Oct 2015 20:47:56 +0000 (16:47 -0400)
committerTom Clegg <tom@curoverse.com>
Thu, 8 Oct 2015 21:08:36 +0000 (17:08 -0400)
sdk/go/keepclient/perms.go [new file with mode: 0644]
sdk/go/keepclient/perms_test.go [new file with mode: 0644]
services/keepstore/perms.go
services/keepstore/perms_test.go

diff --git a/sdk/go/keepclient/perms.go b/sdk/go/keepclient/perms.go
new file mode 100644 (file)
index 0000000..5b792a8
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+Permissions management on Arvados locator hashes.
+
+The permissions structure for Arvados is as follows (from
+https://arvados.org/issues/2328)
+
+A Keep locator string has the following format:
+
+    [hash]+[size]+A[signature]@[timestamp]
+
+The "signature" string here is a cryptographic hash, expressed as a
+string of hexadecimal digits, and timestamp is a 32-bit Unix timestamp
+expressed as a hexadecimal number.  e.g.:
+
+    acbd18db4cc2f85cedef654fccc4a4d8+3+A257f3f5f5f0a4e4626a18fc74bd42ec34dcb228a@7fffffff
+
+The signature represents a guarantee that this locator was generated
+by either Keep or the API server for use with the supplied API token.
+If a request to Keep includes a locator with a valid signature and is
+accompanied by the proper API token, the user has permission to GET
+that object.
+
+The signature may be generated either by Keep (after the user writes a
+block) or by the API server (if the user has can_read permission on
+the specified object). Keep and API server share a secret that is used
+to generate signatures.
+
+To verify a permission hint, Keep generates a new hint for the
+requested object (using the locator string, the timestamp, the
+permission secret and the user's API token, which must appear in the
+request headers) and compares it against the hint included in the
+request. If the permissions do not match, or if the API token is not
+present, Keep returns a 401 error.
+*/
+
+package keepclient
+
+import (
+       "crypto/hmac"
+       "crypto/sha1"
+       "fmt"
+       "regexp"
+       "strconv"
+       "strings"
+       "time"
+)
+
+// KeepError types.
+//
+type KeepError struct {
+       HTTPCode int
+       ErrMsg   string
+}
+
+var (
+       PermissionError = &KeepError{403, "Forbidden"}
+       ExpiredError    = &KeepError{401, "Expired permission signature"}
+)
+
+func (e *KeepError) Error() string {
+       return e.ErrMsg
+}
+
+// makePermSignature returns a string representing the signed permission
+// hint for the blob identified by blobHash, apiToken, expiration timestamp, and permission secret.
+//
+// The permissionSecret is the secret key used to generate SHA1 digests
+// for permission hints. apiserver and Keep must use the same key.
+func makePermSignature(blobHash string, apiToken string, expiry string, permissionSecret []byte) string {
+       hmac := hmac.New(sha1.New, permissionSecret)
+       hmac.Write([]byte(blobHash))
+       hmac.Write([]byte("@"))
+       hmac.Write([]byte(apiToken))
+       hmac.Write([]byte("@"))
+       hmac.Write([]byte(expiry))
+       digest := hmac.Sum(nil)
+       return fmt.Sprintf("%x", digest)
+}
+
+// SignLocator takes a blobLocator, an apiToken, an expiry time, and a permission secret
+// and returns a signed locator string.
+func SignLocator(blobLocator string, apiToken string, expiry time.Time, permissionSecret []byte) string {
+       // If no permission secret or API token is available,
+       // return an unsigned locator.
+       if permissionSecret == nil || apiToken == "" {
+               return blobLocator
+       }
+       // Extract the hash from the blob locator, omitting any size hint that may be present.
+       blobHash := strings.Split(blobLocator, "+")[0]
+       // Return the signed locator string.
+       timestampHex := fmt.Sprintf("%08x", expiry.Unix())
+       return blobLocator +
+               "+A" + makePermSignature(blobHash, apiToken, timestampHex, permissionSecret) +
+               "@" + timestampHex
+}
+
+var signedLocatorRe = regexp.MustCompile(`^([[:xdigit:]]{32}).*\+A([[:xdigit:]]{40})@([[:xdigit:]]{8})`)
+
+// VerifySignature returns nil if the signature on the signedLocator
+// can be verified using the given apiToken. Otherwise it returns
+// either ExpiredError (if the timestamp has expired, which is
+// something the client could have figured out independently) or
+// PermissionError.
+func VerifySignature(signedLocator string, apiToken string, permissionSecret []byte) error {
+       matches := signedLocatorRe.FindStringSubmatch(signedLocator)
+       if matches == nil {
+               // Could not find a permission signature at all
+               return PermissionError
+       }
+       blobHash := matches[1]
+       sigHex := matches[2]
+       expHex := matches[3]
+       if expTime, err := parseHexTimestamp(expHex); err != nil {
+               return PermissionError
+       } else if expTime.Before(time.Now()) {
+               return ExpiredError
+       }
+       if sigHex != makePermSignature(blobHash, apiToken, expHex, permissionSecret) {
+               return PermissionError
+       }
+       return nil
+}
+
+// parseHexTimestamp parses timestamp
+func parseHexTimestamp(timestampHex string) (ts time.Time, err error) {
+       if tsInt, e := strconv.ParseInt(timestampHex, 16, 0); e == nil {
+               ts = time.Unix(tsInt, 0)
+       } else {
+               err = e
+       }
+       return ts, err
+}
diff --git a/sdk/go/keepclient/perms_test.go b/sdk/go/keepclient/perms_test.go
new file mode 100644 (file)
index 0000000..61d10c1
--- /dev/null
@@ -0,0 +1,98 @@
+package keepclient
+
+import (
+       "testing"
+       "time"
+)
+
+const (
+       knownHash    = "acbd18db4cc2f85cedef654fccc4a4d8"
+       knownLocator = knownHash + "+3"
+       knownToken   = "hocfupkn2pjhrpgp2vxv8rsku7tvtx49arbc9s4bvu7p7wxqvk"
+       knownKey     = "13u9fkuccnboeewr0ne3mvapk28epf68a3bhj9q8sb4l6e4e5mkk" +
+               "p6nhj2mmpscgu1zze5h5enydxfe3j215024u16ij4hjaiqs5u4pzsl3nczmaoxnc" +
+               "ljkm4875xqn4xv058koz3vkptmzhyheiy6wzevzjmdvxhvcqsvr5abhl15c2d4o4" +
+               "jhl0s91lojy1mtrzqqvprqcverls0xvy9vai9t1l1lvvazpuadafm71jl4mrwq2y" +
+               "gokee3eamvjy8qq1fvy238838enjmy5wzy2md7yvsitp5vztft6j4q866efym7e6" +
+               "vu5wm9fpnwjyxfldw3vbo01mgjs75rgo7qioh8z8ij7jpyp8508okhgbbex3ceei" +
+               "786u5rw2a9gx743dj3fgq2irk"
+       knownSignature     = "257f3f5f5f0a4e4626a18fc74bd42ec34dcb228a"
+       knownTimestamp     = "7fffffff"
+       knownSigHint       = "+A" + knownSignature + "@" + knownTimestamp
+       knownSignedLocator = knownLocator + knownSigHint
+)
+
+func TestSignLocator(t *testing.T) {
+       if ts, err := parseHexTimestamp(knownTimestamp); err != nil {
+               t.Errorf("bad knownTimestamp %s", knownTimestamp)
+       } else {
+               if knownSignedLocator != SignLocator(knownLocator, knownToken, ts, []byte(knownKey)) {
+                       t.Fail()
+               }
+       }
+}
+
+func TestVerifySignature(t *testing.T) {
+       if VerifySignature(knownSignedLocator, knownToken, []byte(knownKey)) != nil {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureExtraHints(t *testing.T) {
+       if VerifySignature(knownLocator+"+K@xyzzy"+knownSigHint, knownToken, []byte(knownKey)) != nil {
+               t.Fatal("Verify cannot handle hint before permission signature")
+       }
+
+       if VerifySignature(knownLocator+knownSigHint+"+Zfoo", knownToken, []byte(knownKey)) != nil {
+               t.Fatal("Verify cannot handle hint after permission signature")
+       }
+
+       if VerifySignature(knownLocator+"+K@xyzzy"+knownSigHint+"+Zfoo", knownToken, []byte(knownKey)) != nil {
+               t.Fatal("Verify cannot handle hints around permission signature")
+       }
+}
+
+// The size hint on the locator string should not affect signature validation.
+func TestVerifySignatureWrongSize(t *testing.T) {
+       if VerifySignature(knownHash+"+999999"+knownSigHint, knownToken, []byte(knownKey)) != nil {
+               t.Fatal("Verify cannot handle incorrect size hint")
+       }
+
+       if VerifySignature(knownHash+knownSigHint, knownToken, []byte(knownKey)) != nil {
+               t.Fatal("Verify cannot handle missing size hint")
+       }
+}
+
+func TestVerifySignatureBadSig(t *testing.T) {
+       badLocator := knownLocator + "+Aaaaaaaaaaaaaaaa@" + knownTimestamp
+       if VerifySignature(badLocator, knownToken, []byte(knownKey)) != PermissionError {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadTimestamp(t *testing.T) {
+       badLocator := knownLocator + "+A" + knownSignature + "@OOOOOOOl"
+       if VerifySignature(badLocator, knownToken, []byte(knownKey)) != PermissionError {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadSecret(t *testing.T) {
+       if VerifySignature(knownSignedLocator, knownToken, []byte("00000000000000000000")) != PermissionError {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadToken(t *testing.T) {
+       if VerifySignature(knownSignedLocator, "00000000", []byte(knownKey)) != PermissionError {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureExpired(t *testing.T) {
+       yesterday := time.Now().AddDate(0, 0, -1)
+       expiredLocator := SignLocator(knownHash, knownToken, yesterday, []byte(knownKey))
+       if VerifySignature(expiredLocator, knownToken, []byte(knownKey)) != ExpiredError {
+               t.Fail()
+       }
+}
index 5579238112b65ed0747ec33e82e582e580e74e6d..494e6b7f58340738eb46ef547b1f55761266016c 100644 (file)
@@ -36,12 +36,7 @@ present, Keep returns a 401 error.
 package main
 
 import (
-       "crypto/hmac"
-       "crypto/sha1"
-       "fmt"
-       "regexp"
-       "strconv"
-       "strings"
+       "git.curoverse.com/arvados.git/sdk/go/keepclient"
        "time"
 )
 
@@ -50,69 +45,25 @@ import (
 // key.
 var PermissionSecret []byte
 
-// MakePermSignature returns a string representing the signed permission
-// hint for the blob identified by blobHash, apiToken and expiration timestamp.
-func MakePermSignature(blobHash string, apiToken string, expiry string) string {
-       hmac := hmac.New(sha1.New, PermissionSecret)
-       hmac.Write([]byte(blobHash))
-       hmac.Write([]byte("@"))
-       hmac.Write([]byte(apiToken))
-       hmac.Write([]byte("@"))
-       hmac.Write([]byte(expiry))
-       digest := hmac.Sum(nil)
-       return fmt.Sprintf("%x", digest)
-}
-
 // SignLocator takes a blobLocator, an apiToken and an expiry time, and
 // returns a signed locator string.
 func SignLocator(blobLocator string, apiToken string, expiry time.Time) string {
-       // If no permission secret or API token is available,
-       // return an unsigned locator.
-       if PermissionSecret == nil || apiToken == "" {
-               return blobLocator
-       }
-       // Extract the hash from the blob locator, omitting any size hint that may be present.
-       blobHash := strings.Split(blobLocator, "+")[0]
-       // Return the signed locator string.
-       timestampHex := fmt.Sprintf("%08x", expiry.Unix())
-       return blobLocator +
-               "+A" + MakePermSignature(blobHash, apiToken, timestampHex) +
-               "@" + timestampHex
+       return keepclient.SignLocator(blobLocator, apiToken, expiry, PermissionSecret)
 }
 
-var signedLocatorRe = regexp.MustCompile(`^([[:xdigit:]]{32}).*\+A([[:xdigit:]]{40})@([[:xdigit:]]{8})`)
-
 // VerifySignature returns nil if the signature on the signedLocator
 // can be verified using the given apiToken. Otherwise it returns
 // either ExpiredError (if the timestamp has expired, which is
 // something the client could have figured out independently) or
 // PermissionError.
 func VerifySignature(signedLocator string, apiToken string) error {
-       matches := signedLocatorRe.FindStringSubmatch(signedLocator)
-       if matches == nil {
-               // Could not find a permission signature at all
-               return PermissionError
-       }
-       blobHash := matches[1]
-       sigHex := matches[2]
-       expHex := matches[3]
-       if expTime, err := ParseHexTimestamp(expHex); err != nil {
-               return PermissionError
-       } else if expTime.Before(time.Now()) {
-               return ExpiredError
-       }
-       if sigHex != MakePermSignature(blobHash, apiToken, expHex) {
-               return PermissionError
-       }
-       return nil
-}
-
-// ParseHexTimestamp parses timestamp
-func ParseHexTimestamp(timestampHex string) (ts time.Time, err error) {
-       if tsInt, e := strconv.ParseInt(timestampHex, 16, 0); e == nil {
-               ts = time.Unix(tsInt, 0)
-       } else {
-               err = e
+       err := keepclient.VerifySignature(signedLocator, apiToken, PermissionSecret)
+       if err != nil {
+               if err == keepclient.PermissionError {
+                       return PermissionError
+               } else if err == keepclient.ExpiredError {
+                       return ExpiredError
+               }
        }
-       return ts, err
+       return err
 }
index 59516af85f898efd223389f42199901c6ae65862..9b4e30abe3db2de2caae82e2e13ee59dcbcbb148 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
+       "strconv"
        "testing"
        "time"
 )
@@ -26,12 +27,12 @@ func TestSignLocator(t *testing.T) {
        PermissionSecret = []byte(knownKey)
        defer func() { PermissionSecret = nil }()
 
-       if ts, err := ParseHexTimestamp(knownTimestamp); err != nil {
-               t.Errorf("bad knownTimestamp %s", knownTimestamp)
-       } else {
-               if knownSignedLocator != SignLocator(knownLocator, knownToken, ts) {
-                       t.Fail()
-               }
+       tsInt, err := strconv.ParseInt(knownTimestamp, 16, 0)
+       if err != nil {
+               t.Fail()
+       }
+       if knownSignedLocator != SignLocator(knownLocator, knownToken, time.Unix(tsInt, 0)) {
+               t.Fail()
        }
 }