Added permission helper functions.
authorTim Pierce <twp@curoverse.com>
Fri, 2 May 2014 19:05:07 +0000 (15:05 -0400)
committerTim Pierce <twp@curoverse.com>
Fri, 2 May 2014 19:05:07 +0000 (15:05 -0400)
GeneratePerms returns a string representing the signed permission hint
for the blob identified by blob_hash, api_token and timestamp.

SignLocator takes a blob_locator, an api_token and a timestamp, and
returns a signed locator string.

VerifySignature returns true if the signature on the signed_locator can
be verified using the given api_token.

Refs #2328.

services/keep/src/keep/perms.go [new file with mode: 0644]
services/keep/src/keep/perms_test.go [new file with mode: 0644]

diff --git a/services/keep/src/keep/perms.go b/services/keep/src/keep/perms.go
new file mode 100644 (file)
index 0000000..7bbae4f
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+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 the user 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
+perform any action on that object (GET, PUT or DELETE).
+
+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 main
+
+import (
+       "crypto/hmac"
+       "crypto/sha1"
+       "fmt"
+       "regexp"
+       "strings"
+)
+
+// The PermissionSecret is the secret key used to generate SHA1 digests
+// for permission hints. apiserver and Keep must use the same key.
+var PermissionSecret []byte
+
+// GeneratePerms returns a string representing the permission hint for a blob
+// with the given hash, API token and timestamp.
+func GeneratePerms(blob_hash string, api_token string, timestamp string) string {
+       hmac := hmac.New(sha1.New, PermissionSecret)
+       hmac.Write([]byte(blob_hash))
+       hmac.Write([]byte("@"))
+       hmac.Write([]byte(api_token))
+       hmac.Write([]byte("@"))
+       hmac.Write([]byte(timestamp))
+       digest := hmac.Sum(nil)
+       return fmt.Sprintf("%x", digest)
+}
+
+func SignLocator(blob_locator string, api_token string, timestamp string) string {
+       // Extract the hash from the blob locator, omitting any size hint that may be present.
+       blob_hash := strings.Split(blob_locator, "+")[0]
+       // Return the signed locator string.
+       return blob_locator + "+A" + GeneratePerms(blob_hash, api_token, timestamp) + "@" + timestamp
+}
+
+func VerifySignature(signed_locator string, api_token string) bool {
+       if re, err := regexp.Compile(`^(.*)\+A(.*)@(.*)$`); err == nil {
+               if matches := re.FindStringSubmatch(signed_locator); matches != nil {
+                       blob_locator := matches[1]
+                       timestamp := matches[3]
+                       return signed_locator == SignLocator(blob_locator, api_token, timestamp)
+               }
+       }
+       return false
+}
diff --git a/services/keep/src/keep/perms_test.go b/services/keep/src/keep/perms_test.go
new file mode 100644 (file)
index 0000000..fcd17e1
--- /dev/null
@@ -0,0 +1,97 @@
+package main
+
+import (
+       "testing"
+)
+
+var (
+       known_hash    = "acbd18db4cc2f85cedef654fccc4a4d8"
+       known_locator = known_hash + "+3"
+       known_token   = "hocfupkn2pjhrpgp2vxv8rsku7tvtx49arbc9s4bvu7p7wxqvk"
+       known_key     = "13u9fkuccnboeewr0ne3mvapk28epf68a3bhj9q8sb4l6e4e5mkk" +
+               "p6nhj2mmpscgu1zze5h5enydxfe3j215024u16ij4hjaiqs5u4pzsl3nczmaoxnc" +
+               "ljkm4875xqn4xv058koz3vkptmzhyheiy6wzevzjmdvxhvcqsvr5abhl15c2d4o4" +
+               "jhl0s91lojy1mtrzqqvprqcverls0xvy9vai9t1l1lvvazpuadafm71jl4mrwq2y" +
+               "gokee3eamvjy8qq1fvy238838enjmy5wzy2md7yvsitp5vztft6j4q866efym7e6" +
+               "vu5wm9fpnwjyxfldw3vbo01mgjs75rgo7qioh8z8ij7jpyp8508okhgbbex3ceei" +
+               "786u5rw2a9gx743dj3fgq2irk"
+       known_signature      = "257f3f5f5f0a4e4626a18fc74bd42ec34dcb228a"
+       known_timestamp      = "7fffffff"
+       known_signed_locator = known_locator + "+A" + known_signature + "@" + known_timestamp
+)
+
+func TestGeneratePerms(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       if known_signature != GeneratePerms(known_hash, known_token, known_timestamp) {
+               t.Fail()
+       }
+}
+
+func TestSignLocator(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       if known_signed_locator != SignLocator(known_locator, known_token, known_timestamp) {
+               t.Fail()
+       }
+}
+
+func TestVerifySignature(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       if !VerifySignature(known_signed_locator, known_token) {
+               t.Fail()
+       }
+}
+
+// The size hint on the locator string should not affect signature validation.
+func TestVerifySignatureWrongSize(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       signed_locator_wrong_size := known_hash + "+999999+A" + known_signature + "@" + known_timestamp
+       if !VerifySignature(signed_locator_wrong_size, known_token) {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadSig(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       bad_locator := known_locator + "+Aaaaaaaaaaaaaaaa@" + known_timestamp
+       if VerifySignature(bad_locator, known_token) {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadTimestamp(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       bad_locator := known_locator + "+A" + known_signature + "@00000000"
+       if VerifySignature(bad_locator, known_token) {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadSecret(t *testing.T) {
+       PermissionSecret = []byte("00000000000000000000")
+       defer func() { PermissionSecret = nil }()
+
+       if VerifySignature(known_signed_locator, known_token) {
+               t.Fail()
+       }
+}
+
+func TestVerifySignatureBadToken(t *testing.T) {
+       PermissionSecret = []byte(known_key)
+       defer func() { PermissionSecret = nil }()
+
+       if VerifySignature(known_signed_locator, "00000000") {
+               t.Fail()
+       }
+}