--- /dev/null
+/*
+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
+}
--- /dev/null
+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()
+ }
+}
package main
import (
- "crypto/hmac"
- "crypto/sha1"
- "fmt"
- "regexp"
- "strconv"
- "strings"
+ "git.curoverse.com/arvados.git/sdk/go/keepclient"
"time"
)
// 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
}
package main
import (
+ "strconv"
"testing"
"time"
)
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()
}
}