X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/6762d1501f67860180045bbce3e63ef573d07fec..74afa26b1ab0349f5a07f0cd88a9ed0e7e5e9545:/sdk/go/arvados/blob_signature.go diff --git a/sdk/go/arvados/blob_signature.go b/sdk/go/arvados/blob_signature.go new file mode 100644 index 0000000000..4a936026fa --- /dev/null +++ b/sdk/go/arvados/blob_signature.go @@ -0,0 +1,127 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +// Generate and verify permission signatures for Keep locators. +// +// See https://dev.arvados.org/projects/arvados/wiki/Keep_locator_format + +package arvados + +import ( + "crypto/hmac" + "crypto/sha1" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var ( + // ErrSignatureExpired - a signature was rejected because the + // expiry time has passed. + ErrSignatureExpired = errors.New("Signature expired") + // ErrSignatureInvalid - a signature was rejected because it + // was badly formatted or did not match the given secret key. + ErrSignatureInvalid = errors.New("Invalid signature") + // ErrSignatureMissing - the given locator does not have a + // signature hint. + ErrSignatureMissing = errors.New("Missing signature") +) + +// makePermSignature generates a SHA-1 HMAC digest for the given blob, +// token, expiry, and site secret. +func makePermSignature(blobHash, apiToken, expiry, blobSignatureTTL 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)) + hmac.Write([]byte("@")) + hmac.Write([]byte(blobSignatureTTL)) + digest := hmac.Sum(nil) + return fmt.Sprintf("%x", digest) +} + +var ( + mBlkRe = regexp.MustCompile(`^[0-9a-f]{32}.*`) + mPermHintRe = regexp.MustCompile(`\+A[^+]*`) +) + +// SignManifest signs all locators in the given manifest, discarding +// any existing signatures. +func SignManifest(manifest string, apiToken string, ttl time.Duration, permissionSecret []byte) string { + expiry := time.Now().Add(ttl) + return regexp.MustCompile(`\S+`).ReplaceAllStringFunc(manifest, func(tok string) string { + if mBlkRe.MatchString(tok) { + return SignLocator(mPermHintRe.ReplaceAllString(tok, ""), apiToken, expiry, ttl, permissionSecret) + } else { + return tok + } + }) +} + +// SignLocator returns blobLocator with a permission signature +// added. If either permissionSecret or apiToken is empty, blobLocator +// is returned untouched. +// +// This function is intended to be used by system components and admin +// utilities: userland programs do not know the permissionSecret. +func SignLocator(blobLocator, apiToken string, expiry time.Time, blobSignatureTTL time.Duration, permissionSecret []byte) string { + if len(permissionSecret) == 0 || apiToken == "" { + return blobLocator + } + // Strip off all hints: only the hash is used to sign. + blobHash := strings.Split(blobLocator, "+")[0] + timestampHex := fmt.Sprintf("%08x", expiry.Unix()) + blobSignatureTTLHex := strconv.FormatInt(int64(blobSignatureTTL.Seconds()), 16) + return blobLocator + + "+A" + makePermSignature(blobHash, apiToken, timestampHex, blobSignatureTTLHex, permissionSecret) + + "@" + timestampHex +} + +var SignedLocatorRe = regexp.MustCompile( + //1 2 34 5 6 7 89 + `^([[:xdigit:]]{32})(\+[0-9]+)?((\+[B-Z][A-Za-z0-9@_-]*)*)(\+A([[:xdigit:]]{40})@([[:xdigit:]]{8}))((\+[B-Z][A-Za-z0-9@_-]*)*)$`) + +// VerifySignature returns nil if the signature on the signedLocator +// can be verified using the given apiToken. Otherwise it returns +// ErrSignatureExpired (if the signature's expiry time has passed, +// which is something the client could have figured out +// independently), ErrSignatureMissing (if there is no signature hint +// at all), or ErrSignatureInvalid (if the signature is present but +// badly formatted or incorrect). +// +// This function is intended to be used by system components and admin +// utilities: userland programs do not know the permissionSecret. +func VerifySignature(signedLocator, apiToken string, blobSignatureTTL time.Duration, permissionSecret []byte) error { + matches := SignedLocatorRe.FindStringSubmatch(signedLocator) + if matches == nil { + return ErrSignatureMissing + } + blobHash := matches[1] + signatureHex := matches[6] + expiryHex := matches[7] + if expiryTime, err := parseHexTimestamp(expiryHex); err != nil { + return ErrSignatureInvalid + } else if expiryTime.Before(time.Now()) { + return ErrSignatureExpired + } + blobSignatureTTLHex := strconv.FormatInt(int64(blobSignatureTTL.Seconds()), 16) + if signatureHex != makePermSignature(blobHash, apiToken, expiryHex, blobSignatureTTLHex, permissionSecret) { + return ErrSignatureInvalid + } + return nil +} + +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 +}