├── .gitignore ├── README.md ├── doc.go ├── hash_func.go ├── hash_func_test.go ├── keys.go ├── keys_test.go ├── settings.go ├── siggen └── main.go ├── signing.go ├── signing_test.go ├── test └── webserver │ ├── README.md │ └── main.go ├── tracing.go ├── url.go └── url_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | test/webserver/main 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Signature 2 | ========= 3 | 4 | URL signing package for Go. 5 | 6 | * You can jump straight into the [API documentation](http://godoc.org/github.com/stretchr/signature). 7 | 8 | ## What does it do? 9 | 10 | Signature secures web calls by generating a security hash on the client (using a private key shared with the server), to ensure that the request is geniune. Only a client who knows the private key will be able to generate the same security hash. 11 | 12 | Since the private key is only used to generate the security hash and not transmitted with the request (only some kind of public key is), the server and client must agree on the private key in order for the hash to be verified. 13 | 14 | ## Request signing 15 | 16 | Request signing entails generating a hash based on the details of the request, PLUS a private key - and having the remote server try to generate the same hash, assuming you both agree on the private key. 17 | 18 | ### Encoding Process 19 | 20 | Before signing the request, you must: 21 | 22 | * Generate the original request URL 23 | * example: http://something.com?key=value&key2=value2 24 | * Add the public key value to the request URL 25 | * example: http://something.com?key=value&key2=value2?key=85Bad53987b3d851 26 | 27 | SignatureKey parameter generation: 28 | 29 | To generate a signature for the request, the Signature package does the following: 30 | 31 | * Create a copy of the request URL 32 | * Add `PrivateKeyKey` key parameter 33 | * Add `BodyHashKey` value containing an SHA-1 hash of the body contents if there is a body - otherwise, skip this step (and do not add a `BodyHashKey` parameter at all) 34 | * Order parameters alphabetically. Order first by keys, and if there are multiple values for one key (i.e. ?color=red&color=blue) then order the values alphabetically afterwards. 35 | * Prefix it with the HTTP method (in uppercase) followed by an ampersand (i.e. `GET&http://...`) 36 | * Hash it (using SHA-1) 37 | * Add the hash as `SignatureKey` to the _end_ of the original URL 38 | 39 | ### Decoding 40 | 41 | To verify a signed request, the Signature package does the following: 42 | 43 | * Strip off the `SignatureKey` parameter (and keep it) 44 | * Lookup the account (using the public key) and get the `PrivateKeyKey` parameter, and add it to the URL 45 | * Hash it 46 | * Compare the generated hash with the `SignatureKey` value to decide if it the request is valid or not 47 | 48 | ### Settings 49 | 50 | The `signature` package provides some settings to allow you to use non-default field names in your code. Remember that the client needs to use the same fields in order for the security hashes to match. 51 | 52 | // PrivateKeyKey is the key (URL field) for the private key. 53 | signature.PrivateKeyKey string = "~private" 54 | 55 | // BodyHashKey is the key (URL field) for the body hash used for signing requests. 56 | signature.BodyHashKey string = "~bodyhash" 57 | 58 | // SignatureKey is the key (URL field) for the signature of requests. 59 | signature.SignatureKey string = "~sign" 60 | 61 | ### Validating request signature 62 | 63 | To validate your code is generating the correct hash, ensure it generates the following output. 64 | 65 | GET http://test.stretchr.com/api/v2?:name=!Mat&:name=!Laurie&:age=>20 66 | 67 | Public key: ABC123 68 | Private key: ABC123-private 69 | Body: body 70 | 71 | #### Step 1: Get the original request URL 72 | 73 | http://test.stretchr.com/api/v2?:name=!Mat&:name=!Laurie&:age=>20 74 | 75 | #### Step 2: Add the public key value to the request URL 76 | 77 | http://test.stretchr.com/api/v2?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~key=ABC123 78 | 79 | #### Step 3: Add the private key to a *copy* of this URL 80 | 81 | http://test.stretchr.com/api/v2?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~key=ABC123&~private=ABC123-private 82 | 83 | #### Step 4: Add the body hash 84 | 85 | Add the body hash containing an SHA-1 hash of the body contents if there is a body - otherwise, skip this step (and do not add a `BodyHashKey` parameter at all) 86 | 87 | In this case, since `"body"` is the body, it will be hashed as `02083f4579e08a612425c0c1a17ee47add783b94`. 88 | 89 | http://test.stretchr.com/api/v2?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~key=ABC123&~private=ABC123-private&bodyhash=02083f4579e08a612425c0c1a17ee47add783b94 90 | 91 | #### Step 5: Order parameters alphabetically. 92 | 93 | Order first by keys, and if there are multiple values for one key (i.e. ?color=red&color=blue) then order the values alphabetically afterwards. 94 | 95 | http://test.stretchr.com/api/v2?:age=>20&:name=!Laurie&:name=!Mat&bodyhash=02083f4579e08a612425c0c1a17ee47add783b94&~key=ABC123&~private=ABC123-private 96 | 97 | #### Step 6: Prefix the HTTP method 98 | 99 | Append the HTTP method in uppercase, followed by an ampersand `&`: 100 | 101 | GET&http://test.stretchr.com/api/v2?:age=>20&:name=!Laurie&:name=!Mat&bodyhash=02083f4579e08a612425c0c1a17ee47add783b94&~key=ABC123&~private=ABC123-private 102 | 103 | #### Step 7: Hash it (using the SHA-1 hash algorithm) 104 | 105 | 6c3dc03b3f85c9eb80ed9e4bd21e82f1bbda5b8d 106 | 107 | #### Step 8: Append the `signature.SignatureKey` to the *end* of the URL from step 2 108 | 109 | http://test.stretchr.com/api/v2?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~key=ABC123&~private=ABC123-private&~sign=6c3dc03b3f85c9eb80ed9e4bd21e82f1bbda5b8d 110 | 111 | ## Response signing 112 | 113 | Response signing refers to generating a hash based on the response, to validate that the remote server indeed was responsible for generating the response. This prevents clients of the service from being tricked into accepting a response from an unreliable source. 114 | 115 | ### The `HashWithKeys` method 116 | 117 | Signature provides the `HashWithKeys` method that allows you to hash a series of bytes (along with the public and private keys). When this value is transmitted to the client, they can attempt to generate the same hash. If the hashes match, then the response was geniune. 118 | 119 | 120 | ------ 121 | 122 | Contributing 123 | ============ 124 | 125 | Please feel free to submit issues, fork the repository and send pull requests! 126 | 127 | When submitting an issue, we ask that you please include steps to reproduce the issue so we can see it on our end also! 128 | 129 | 130 | Licence 131 | ======= 132 | Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell 133 | 134 | Please consider promoting this project if you find it useful. 135 | 136 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 139 | 140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 141 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Signature provides URL signing capabilities for Go. 3 | 4 | Encoding 5 | 6 | * Generate the original request URL 7 | * Add ~key public key value 8 | 9 | To generate hash: 10 | 11 | * Create a copy of the request URL 12 | * Add ~private key parameter 13 | * Add ~bodyhash value containing an SHA1 hash of the body contents if there is a body - otherwise, skip this step 14 | * Order parameters alphabetically 15 | * Prefix it with the HTTP method (in uppercase) followed by an ampersand (i.e. "GET&http://...") 16 | * Hash it (using SHA-1) 17 | * Add the hash as ~sign to the END of the original URL 18 | 19 | Decoding 20 | 21 | * Strip off the ~sign parameter (and keep it) 22 | * Lookup the account (using ~key) and get the ~private parameter, and add it to the URL 23 | * Hash it 24 | * Compare the generated hash with the ~sign value to decide if it the request is valid or not 25 | */ 26 | package signature 27 | -------------------------------------------------------------------------------- /hash_func.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "fmt" 7 | "github.com/stretchr/stew/strings" 8 | "github.com/stretchr/tracer" 9 | "io" 10 | ) 11 | 12 | var HashWithKeysSeparator string = ":" 13 | 14 | // HashFunc represents funcs that can hash a string. 15 | type HashFunc func(s string) string 16 | 17 | // Hash hashes a string using the current HashFunc. 18 | // 19 | // To tell Signature to use a different hashing algorithm, you 20 | // just need to assign a different HashFunc to the Hash variable. 21 | // 22 | // To use the MD5 hash: 23 | // 24 | // signature.Hash = signature.MD5Hash 25 | // 26 | // Or you can write your own hashing function: 27 | // 28 | // signature.Hash = func(s string) string { 29 | // // TODO: do your own hashing here 30 | // } 31 | var Hash HashFunc = SHA1Hash 32 | 33 | // SHA1Hash hashes a string using the SHA-1 hash algorithm as defined in RFC 3174. 34 | var SHA1Hash HashFunc = func(s string) string { 35 | hash := sha1.New() 36 | hash.Write([]byte(s)) 37 | return fmt.Sprintf("%x", hash.Sum(nil)) 38 | } 39 | 40 | // MD5Hash hashes a string using the MD5 hash algorithm as defined in RFC 1321. 41 | var MD5Hash HashFunc = func(s string) string { 42 | md5 := md5.New() 43 | io.WriteString(md5, s) 44 | return fmt.Sprintf("%x", md5.Sum(nil)) 45 | } 46 | 47 | // HashWithKeys generates a hash of the specified bytes by first merging them with 48 | // the specified private key. 49 | // 50 | // For format is: 51 | // 52 | // BODY:PUBLIC:PRIVATE 53 | // 54 | // The keywords should be replaced with the actual bytes, but the colons are literals as the 55 | // HashWithKeysSeparator value is used to separate the values. 56 | // 57 | // Useful for hashing non URLs (such as response bodies etc.) 58 | func HashWithKeys(body, publicKey, privateKey []byte) string { 59 | return HashWithKeysWithTrace(body, publicKey, privateKey, nil) 60 | } 61 | 62 | // HashWithKey does the same as HashWithKey, but uses a single key. 63 | func HashWithKey(body, key []byte) string { 64 | return HashWithKeyWithTrace(body, key, nil) 65 | } 66 | 67 | func HashWithKeysWithTrace(body, publicKey, privateKey []byte, t *tracer.Tracer) string { 68 | 69 | if t.Should(tracer.LevelDebug) { 70 | t.Trace(tracer.LevelDebug, "HashWithKeys: body=", body) 71 | t.Trace(tracer.LevelDebug, "HashWithKeys: publicKey=", publicKey) 72 | t.Trace(tracer.LevelDebug, "HashWithKeys: privateKey=", privateKey) 73 | } 74 | 75 | hash := Hash(string(strings.JoinBytes([]byte(HashWithKeysSeparator), body, publicKey, privateKey))) 76 | 77 | if t.Should(tracer.LevelDebug) { 78 | t.Trace(tracer.LevelDebug, "HashWithKeys: Output: %s", hash) 79 | } 80 | 81 | return hash 82 | 83 | } 84 | 85 | func HashWithKeyWithTrace(body, key []byte, t *tracer.Tracer) string { 86 | 87 | if t.Should(tracer.LevelDebug) { 88 | t.Trace(tracer.LevelDebug, "HashWithKeys: body=", body) 89 | t.Trace(tracer.LevelDebug, "HashWithKeys: key=", key) 90 | } 91 | 92 | hash := Hash(string(strings.JoinBytes([]byte(HashWithKeysSeparator), body, key))) 93 | 94 | if t.Should(tracer.LevelDebug) { 95 | t.Trace(tracer.LevelDebug, "HashWithKeys: Output: %s", hash) 96 | } 97 | 98 | return hash 99 | 100 | } 101 | -------------------------------------------------------------------------------- /hash_func_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | /* 9 | func TestHash(t *testing.T) { 10 | assert.Equal(t, SHA1Hash, Hash) 11 | } 12 | */ 13 | 14 | func TestSHA1Hash(t *testing.T) { 15 | 16 | assert.Equal(t, SHA1Hash("abc"), "a9993e364706816aba3e25717850c26c9cd0d89d") 17 | 18 | } 19 | 20 | func TestMD5Hash(t *testing.T) { 21 | 22 | assert.Equal(t, MD5Hash("abc"), "900150983cd24fb0d6963f7d28e17f72") 23 | 24 | } 25 | 26 | func TestHashWithPrivateKey(t *testing.T) { 27 | 28 | assert.Equal(t, HashWithKeys([]byte("some bytes to hash"), []byte("public key"), []byte("private key")), "4c9ac9d6594e19c0bcbfcc261f82e190cf222526") 29 | assert.NotEmpty(t, HashWithKeys([]byte("some bytes to hash"), []byte("public key"), []byte("private key")), HashWithKeys([]byte("some bytes to hash"), []byte("public key"), []byte("different private key"))) 30 | assert.NotEmpty(t, HashWithKeys([]byte("some bytes to hash"), []byte("public key"), []byte("private key")), HashWithKeys([]byte("some bytes to hash"), []byte("different public key"), []byte("private key"))) 31 | assert.NotEmpty(t, HashWithKeys([]byte("some bytes to hash"), []byte("public key"), []byte("private key")), HashWithKeys([]byte("different bytes to hash"), []byte("public key"), []byte("private key"))) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // RandomKeyCharacters is a []byte of the characters to choose from when generating 9 | // random keys. 10 | var RandomKeyCharacters []byte = []byte("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") 11 | 12 | // randomised gets whether the rand.Seed has been set or not. 13 | var randomised bool 14 | 15 | // RandomKey generates a random key at the given length. 16 | // 17 | // The first time this is called, the rand.Seed will be set 18 | // to the current time. 19 | func RandomKey(length int) string { 20 | 21 | // randomise the seed 22 | if !randomised { 23 | rand.Seed(time.Now().UTC().UnixNano()) 24 | randomised = true 25 | } 26 | 27 | // Credit: http://stackoverflow.com/questions/12321133/golang-random-number-generator-how-to-seed-properly 28 | 29 | bytes := make([]byte, length) 30 | for i := 0; i < length; i++ { 31 | randInt := randInt(0, len(RandomKeyCharacters)) 32 | bytes[i] = RandomKeyCharacters[randInt] 33 | } 34 | return string(bytes) 35 | 36 | } 37 | 38 | // randInt generates a random integer between min and max. 39 | func randInt(min int, max int) int { 40 | return min + rand.Intn(max-min) 41 | } 42 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestKeys_RandomKey(t *testing.T) { 9 | 10 | var key string 11 | 12 | key = RandomKey(20) 13 | assert.Equal(t, 20, len(key)) 14 | 15 | key = RandomKey(50) 16 | assert.Equal(t, 50, len(key)) 17 | 18 | key = RandomKey(2) 19 | assert.Equal(t, 2, len(key)) 20 | 21 | key = RandomKey(1) 22 | assert.Equal(t, 1, len(key)) 23 | 24 | key = RandomKey(0) 25 | assert.Equal(t, 0, len(key)) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | var ( 4 | // PrivateKeyKey is the key (URL field) for the private key. 5 | PrivateKeyKey string = "private" 6 | 7 | // BodyHashKey is the key (URL field) for the body hash used for signing requests. 8 | BodyHashKey string = "bodyhash" 9 | 10 | // SignatureKey is the key (URL field) for the signature of requests. 11 | SignatureKey string = "sign" 12 | ) 13 | -------------------------------------------------------------------------------- /siggen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/commander" 6 | "github.com/stretchr/objx" 7 | "github.com/stretchr/signature" 8 | ) 9 | 10 | // Generates a 32 character random signature 11 | func main() { 12 | commander.Go(func() { 13 | 14 | commander.Map(commander.DefaultCommand, "", "", 15 | func(args objx.Map) { 16 | fmt.Println(signature.RandomKey(32)) 17 | }) 18 | 19 | commander.Map("len length=(int)", "Key length", 20 | "Specify the length of the generated key", 21 | func(args objx.Map) { 22 | length := args.Get("length").Int() 23 | fmt.Println(signature.RandomKey(length)) 24 | }) 25 | 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /signing.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | stewstrings "github.com/stretchr/stew/strings" 7 | "github.com/stretchr/tracer" 8 | "net/url" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // FailedSignature is the string that will be used if signing fails. 14 | const FailedSignature string = ":-(" 15 | 16 | // ErrNoSignatureFound is the error that is thrown when no signature could be found. 17 | var ErrNoSignatureFound = errors.New("No signature was found.") 18 | 19 | // SignatureRegex is the regex used to remove the signature from the URL string 20 | var SignatureRegex = regexp.MustCompile("(.*)[&?](~|%7E)?sign=([0-9a-zA-Z]+)(.*)") 21 | 22 | // trace writes some trace (if there is a Tracer set). 23 | func trace(t *tracer.Tracer, format string, args ...interface{}) { 24 | 25 | if t.Should(tracer.LevelDebug) { 26 | 27 | // add the 'signature' prefix to trace 28 | if len(format) > 0 { 29 | format = stewstrings.MergeStrings("signature: ", format) 30 | } 31 | 32 | // trace this 33 | t.Trace(tracer.LevelDebug, format, args...) 34 | } 35 | 36 | } 37 | 38 | // GetSignature gets the signature of a request based on the given parameters. 39 | func GetSignature(method, requestUrl, body, privateKey string) (string, error) { 40 | return GetSignatureWithTrace(method, requestUrl, body, privateKey, Tracer) 41 | } 42 | 43 | // GetSignatureWithTrace gets the signature of a request based on the given parameters. 44 | func GetSignatureWithTrace(method, requestUrl, body, privateKey string, tracer *tracer.Tracer) (string, error) { 45 | 46 | trace(tracer, "GetSignature: method=%s", method) 47 | trace(tracer, "GetSignature: requestUrl=%s", requestUrl) 48 | trace(tracer, "GetSignature: body=%s", body) 49 | trace(tracer, "GetSignature: privateKey=%s", privateKey) 50 | 51 | // parse the URL 52 | u, parseErr := url.ParseRequestURI(requestUrl) 53 | 54 | if parseErr != nil { 55 | trace(tracer, "GetSignature: FAILED to parse the URL: %s", parseErr) 56 | return FailedSignature, parseErr 57 | } 58 | 59 | trace(tracer, "GetSignature: Parsed the URL as: %s", u.String()) 60 | 61 | // get the query values 62 | values := u.Query() 63 | 64 | // add the private key parameter 65 | values.Set(PrivateKeyKey, privateKey) 66 | 67 | trace(tracer, "GetSignature: Set the private key (%s): %s", PrivateKeyKey, privateKey) 68 | 69 | if len(body) > 0 { 70 | bodyHash := Hash(body) 71 | trace(tracer, "GetSignature: Set the body hash (%s): %s", BodyHashKey, bodyHash) 72 | values.Set(BodyHashKey, bodyHash) 73 | } else { 74 | trace(tracer, "GetSignature: Skipping body hash as there's no body (%s).", BodyHashKey) 75 | } 76 | 77 | // get the ordered params 78 | orderedParams := OrderParams(values) 79 | 80 | trace(tracer, "GetSignature: Ordered parameters: %s", orderedParams) 81 | 82 | base := strings.Split(u.String(), "?")[0] 83 | combined := stewstrings.MergeStrings(strings.ToUpper(method), "&", base, "?", orderedParams) 84 | 85 | trace(tracer, "GetSignature: Base : %s", base) 86 | trace(tracer, "GetSignature: Combined: %s", combined) 87 | 88 | theHash := Hash(combined) 89 | 90 | trace(tracer, "GetSignature: Output: %s", theHash) 91 | 92 | return theHash, nil 93 | 94 | } 95 | 96 | // GetSignedURL gets the URL with the sign parameter added based on the given parameters. 97 | func GetSignedURL(method, requestUrl, body, privateKey string) (string, error) { 98 | return GetSignedURLWithTrace(method, requestUrl, body, privateKey, Tracer) 99 | } 100 | 101 | // GetSignedURL gets the URL with the sign parameter added based on the given parameters. 102 | func GetSignedURLWithTrace(method, requestUrl, body, privateKey string, tracer *tracer.Tracer) (string, error) { 103 | 104 | trace(tracer, "GetSignedURL: method=%s", method) 105 | trace(tracer, "GetSignedURL: requestUrl=%s", requestUrl) 106 | trace(tracer, "GetSignedURL: body=%s", body) 107 | trace(tracer, "GetSignedURL: privateKey=%s", privateKey) 108 | 109 | hash, hashErr := GetSignatureWithTrace(method, requestUrl, body, privateKey, tracer) 110 | 111 | if hashErr != nil { 112 | trace(tracer, "GetSignedURL: FAILED to get the signature: %s", hashErr) 113 | return FailedSignature, hashErr 114 | } 115 | 116 | var signedUrl string 117 | if strings.Contains(requestUrl, "?") { 118 | signedUrl = stewstrings.MergeStrings(requestUrl, "&", url.QueryEscape(SignatureKey), "=", url.QueryEscape(hash)) 119 | } else { 120 | signedUrl = stewstrings.MergeStrings(requestUrl, "?", url.QueryEscape(SignatureKey), "=", url.QueryEscape(hash)) 121 | } 122 | 123 | trace(tracer, "GetSignedURL: Output: %s", signedUrl) 124 | 125 | return signedUrl, nil 126 | 127 | } 128 | 129 | // ValidateSignature validates the signature in a URL to ensure it is correct based on 130 | // the specified parameters. 131 | func ValidateSignature(method, requestUrl, body, privateKey string) (bool, error) { 132 | return ValidateSignatureWithTrace(method, requestUrl, body, privateKey, Tracer) 133 | } 134 | 135 | // ValidateSignature validates the signature in a URL to ensure it is correct based on 136 | // the specified parameters. 137 | func ValidateSignatureWithTrace(method, requestUrl, body, privateKey string, tracer *tracer.Tracer) (bool, error) { 138 | 139 | trace(tracer, "ValidateSignature: method=%s", method) 140 | trace(tracer, "ValidateSignature: requestUrl=%s", requestUrl) 141 | trace(tracer, "ValidateSignature: body=%s", body) 142 | trace(tracer, "ValidateSignature: privateKey=%s", privateKey) 143 | 144 | if !strings.Contains(requestUrl, "?") { 145 | trace(tracer, "ValidateSignature: FAILED because there was no signature found.") 146 | return false, ErrNoSignatureFound 147 | } 148 | 149 | segments := strings.Split(requestUrl, SignatureKey) 150 | 151 | if len(segments) < 2 { 152 | trace(tracer, "ValidateSignature: Failed to get signature: %s", ErrNoSignatureFound) 153 | return false, ErrNoSignatureFound 154 | } else { 155 | trace(tracer, "Segments: %s", segments) 156 | } 157 | 158 | modifiedURL := strings.TrimRight(segments[0], "&") 159 | signature := strings.TrimLeft(segments[1], "=") 160 | 161 | trace(tracer, "ValidateSignature: Modified URL (without signature): %s", modifiedURL) 162 | 163 | expectedSignature, signErr := GetSignatureWithTrace(method, modifiedURL, body, privateKey, tracer) 164 | 165 | if signErr != nil { 166 | trace(tracer, "ValidateSignature: FAILED to GetSignature: %s", signErr) 167 | return false, signErr 168 | } 169 | 170 | if signature != expectedSignature { 171 | err := errors.New(fmt.Sprintf("Signature \"%s\" is incorrect when \"%s\" is expected.", signature, expectedSignature)) 172 | trace(tracer, "ValidateSignature: Signatures do not match: %s", err) 173 | return false, err 174 | } 175 | 176 | trace(tracer, "ValidateSignature: Happy because the signatures match: %s", signature) 177 | 178 | return true, nil 179 | 180 | } 181 | -------------------------------------------------------------------------------- /signing_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "log" 7 | "testing" 8 | ) 9 | 10 | func TestGetSignature(t *testing.T) { 11 | 12 | var signed string 13 | 14 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "body", "ABC123-private") 15 | assert.Equal(t, "0bd60ce074a9a3ecda66a438f04a6cf779ab60d3", signed) 16 | 17 | signed, _ = GetSignature("get", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "body", "ABC123-private") 18 | assert.Equal(t, "0bd60ce074a9a3ecda66a438f04a6cf779ab60d3", signed, "Lower case method shouldn't affect GetSignature") 19 | 20 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "body", "DIFFERENT-PRIVATE") 21 | assert.Equal(t, "be7348dd329e0791b3c082a9044e55bc16779587", signed) 22 | 23 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?:name=!Laurie&~key=ABC123&:age=>20&:name=!Mat", "body", "DIFFERENT-PRIVATE") 24 | assert.Equal(t, "be7348dd329e0791b3c082a9044e55bc16779587", signed, "Different order of args shouldn't matter") 25 | 26 | } 27 | 28 | func TestGetSignedURL(t *testing.T) { 29 | 30 | var signed string 31 | 32 | signed, _ = GetSignedURL("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "body", "ABC123-private") 33 | assert.Equal(t, "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=0bd60ce074a9a3ecda66a438f04a6cf779ab60d3", signed) 34 | 35 | signed, _ = GetSignedURL("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "body", "DIFFERENT-PRIVATE") 36 | assert.Equal(t, "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=be7348dd329e0791b3c082a9044e55bc16779587", signed) 37 | 38 | } 39 | 40 | func TestValidateSignature(t *testing.T) { 41 | 42 | var valid bool 43 | 44 | signed, _ := GetSignature("GET", "http://test.stretchr.com/api/v1?key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 45 | 46 | valid, _ = ValidateSignature("GET", fmt.Sprintf("http://test.stretchr.com/api/v1?key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 47 | assert.Equal(t, true, valid, "1") 48 | 49 | valid, _ = ValidateSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~sign=qJWro1ZxLeToLjNr5Znfi2ZbD+o=", "ABC123", "ABC123-private-wrong") 50 | assert.Equal(t, false, valid, "2") 51 | 52 | signed, _ = GetSignature("get", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 53 | valid, _ = ValidateSignature("GET", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 54 | assert.Equal(t, true, valid, "3") 55 | 56 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 57 | valid, _ = ValidateSignature("get", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 58 | assert.Equal(t, true, valid, "4") 59 | 60 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:text=/test+plus", "ABC123", "ABC123-private") 61 | valid, _ = ValidateSignature("get", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:text=/test+plus&sign=%s", signed), "ABC123", "ABC123-private") 62 | assert.Equal(t, true, valid, "5") 63 | 64 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:text=/test%2bplus", "ABC123", "ABC123-private") 65 | valid, _ = ValidateSignature("get", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:text=/test%%2bplus&sign=%s", signed), "ABC123", "ABC123-private") 66 | assert.Equal(t, true, valid, "6") 67 | 68 | valid, _ = ValidateSignature("get", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&", "ABC123", "ABC123-private") 69 | assert.Equal(t, false, valid, "7") 70 | 71 | } 72 | 73 | func TestValidateSignature_NoTilde(t *testing.T) { 74 | 75 | var valid bool 76 | 77 | signed, _ := GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 78 | 79 | valid, _ = ValidateSignature("GET", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 80 | assert.Equal(t, true, valid, "1") 81 | 82 | valid, _ = ValidateSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=qJWro1ZxLeToLjNr5Znfi2ZbD+o=", "ABC123", "ABC123-private-wrong") 83 | assert.Equal(t, false, valid, "2") 84 | 85 | signed, _ = GetSignature("get", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 86 | valid, _ = ValidateSignature("GET", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 87 | assert.Equal(t, true, valid, "3") 88 | 89 | signed, _ = GetSignature("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "ABC123", "ABC123-private") 90 | valid, _ = ValidateSignature("get", fmt.Sprintf("http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=%s", signed), "ABC123", "ABC123-private") 91 | assert.Equal(t, true, valid, "4") 92 | 93 | valid, _ = ValidateSignature("get", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&", "ABC123", "ABC123-private") 94 | assert.Equal(t, false, valid, "5") 95 | 96 | } 97 | 98 | func TestNoBodyHashWhenNoBody(t *testing.T) { 99 | 100 | signed, _ := GetSignedURL("GET", "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20", "", "ABC123-private") 101 | assert.Equal(t, "http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&sign=0e1e85ebdf8c4bcebdfc9033b6590c6c4a13a78f", signed) 102 | 103 | } 104 | 105 | func TestSigning_BodyInURL(t *testing.T) { 106 | 107 | valid, _ := ValidateSignature("GET", `http://test.stretchr.com/api/v1?~key=ABC123&:name=!Mat&:name=!Laurie&:age=>20&~body={"question":"Is this OK & working?"}&sign=a1f72a10e882ac64236c43dca381981c77aa8a48`, "", "ABC123-Private") 108 | 109 | assert.Equal(t, true, valid, "1") 110 | 111 | s, _ := GetSignature("GET", `http://test.stretchr.com/api/v2/test?always200=1&body=%7B%22question%22%3A%22Is%20this%20OK%20%26%20working%3F%22%7D&callback=Stretchr.callback&context=1&key=ABC123&method=POST`, "", "PRIVATE") 112 | log.Printf(s) 113 | 114 | // The tests below represent real requests via JSONP 115 | valid, _ = ValidateSignature("GET", `http://test.stretchr.com/api/v2/test?always200=1&body=%7B%22question%22%3A%22Is%20this%20OK%20%26%20working%3F%22%7D&callback=Stretchr.callback&context=1&key=ABC123&method=POST&sign=6078d0be451bb783d31addb85db654a7759a8792`, "", "PRIVATE") 116 | 117 | assert.Equal(t, true, valid, "2") 118 | 119 | } 120 | -------------------------------------------------------------------------------- /test/webserver/README.md: -------------------------------------------------------------------------------- 1 | # Signature package - web server test 2 | 3 | This package is a simple web server that implements the `signature` package and allows you to test clients that are trying to generate the same signature. 4 | 5 | ## Usage 6 | 7 | * Start the web server 8 | 9 | In Terminal: 10 | 11 | cd signature/test/webserver 12 | go run main.go 13 | 14 | * Make a request to the web server like `http://localhost:8080/some/path?~sign=ABC123&` 15 | 16 | * Use the private key `PRIVATE` 17 | -------------------------------------------------------------------------------- /test/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/goweb" 6 | "github.com/stretchr/goweb/context" 7 | "github.com/stretchr/signature" 8 | "github.com/stretchr/tracer" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "net/http" 13 | "os" 14 | "os/signal" 15 | "time" 16 | ) 17 | 18 | const Address string = ":80" 19 | 20 | func absoluteUrlForRequest(request *http.Request) string { 21 | return fmt.Sprintf("http://%s%s", request.Host, request.RequestURI) 22 | } 23 | 24 | func main() { 25 | 26 | // handle every path 27 | goweb.Map("***", func(c context.Context) error { 28 | 29 | method := c.HttpRequest().Method 30 | requestUrl := absoluteUrlForRequest(c.HttpRequest()) 31 | privateKey := "PRIVATE" 32 | body, bodyErr := ioutil.ReadAll(c.HttpRequest().Body) 33 | 34 | fmt.Printf("URL: %s", requestUrl) 35 | 36 | if bodyErr != nil { 37 | goweb.Respond.With(c, 500, []byte(fmt.Sprintf("Balls! Couldn't read the body because %s", bodyErr))) 38 | return nil 39 | } 40 | 41 | t := tracer.New(tracer.LevelEverything) 42 | 43 | valid, _ := signature.ValidateSignatureWithTrace(method, requestUrl, string(body), privateKey, t) 44 | 45 | // return the tracing 46 | c.HttpResponseWriter().Header().Set("Content Type", "text/html") 47 | c.HttpResponseWriter().Write([]byte("")) 48 | c.HttpResponseWriter().Write([]byte("")) 49 | c.HttpResponseWriter().Write([]byte("Signature")) 50 | 51 | c.HttpResponseWriter().Write([]byte("")) 52 | 53 | c.HttpResponseWriter().Write([]byte("")) 63 | 64 | c.HttpResponseWriter().Write([]byte("")) 65 | c.HttpResponseWriter().Write([]byte("")) 66 | c.HttpResponseWriter().Write([]byte("
")) 67 | c.HttpResponseWriter().Write([]byte("
")) 68 | c.HttpResponseWriter().Write([]byte("
"))
 69 | 
 70 | 		for _, trace := range t.Data() {
 71 | 			c.HttpResponseWriter().Write([]byte(trace.Data))
 72 | 			c.HttpResponseWriter().Write([]byte("\n"))
 73 | 		}
 74 | 
 75 | 		c.HttpResponseWriter().Write([]byte("
")) 76 | c.HttpResponseWriter().Write([]byte("
")) 77 | 78 | c.HttpResponseWriter().Write([]byte("")) 79 | c.HttpResponseWriter().Write([]byte("")) 80 | 81 | return nil 82 | 83 | }) 84 | 85 | fmt.Println("Signature test webserver") 86 | fmt.Println("by Mat Ryer and Tyler Bunnell") 87 | fmt.Println(" ") 88 | fmt.Println("Start making signed request to: http://localhost:8080/") 89 | 90 | /* 91 | 92 | START OF WEB SERVER CODE 93 | 94 | */ 95 | 96 | log.Print("Goweb 2") 97 | log.Print("by Mat Ryer and Tyler Bunnell") 98 | log.Print(" ") 99 | log.Print("Starting Goweb powered server...") 100 | 101 | // make a http server using the goweb.DefaultHttpHandler() 102 | s := &http.Server{ 103 | Addr: "Address", 104 | Handler: goweb.DefaultHttpHandler(), 105 | ReadTimeout: 10 * time.Second, 106 | WriteTimeout: 10 * time.Second, 107 | MaxHeaderBytes: 1 << 20, 108 | } 109 | 110 | c := make(chan os.Signal, 1) 111 | signal.Notify(c, os.Interrupt) 112 | listener, listenErr := net.Listen("tcp", Address) 113 | 114 | log.Printf(" visit: %s", Address) 115 | 116 | if listenErr != nil { 117 | log.Fatalf("Could not listen: %s", listenErr) 118 | } 119 | 120 | log.Println("Also try some of these routes:") 121 | log.Printf("%s", goweb.DefaultHttpHandler()) 122 | 123 | go func() { 124 | for _ = range c { 125 | 126 | // sig is a ^C, handle it 127 | 128 | // stop the HTTP server 129 | log.Print("Stopping the server...") 130 | listener.Close() 131 | 132 | /* 133 | Tidy up and tear down 134 | */ 135 | log.Print("Tearing down...") 136 | 137 | // TODO: tidy code up here 138 | 139 | log.Fatal("Finished - bye bye. ;-)") 140 | 141 | } 142 | }() 143 | 144 | // begin the server 145 | log.Fatalf("Error in Serve: %s", s.Serve(listener)) 146 | 147 | /* 148 | 149 | END OF WEB SERVER CODE 150 | 151 | */ 152 | 153 | } 154 | -------------------------------------------------------------------------------- /tracing.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "github.com/stretchr/tracer" 5 | ) 6 | 7 | // Tracer (by default nil) is the object that the signature package 8 | // should trace to. 9 | var Tracer *tracer.Tracer = nil 10 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | stewstrings "github.com/stretchr/stew/strings" 5 | "net/url" 6 | "sort" 7 | ) 8 | 9 | // OrderParams gets the parameters ordered by key, then by values as a URL string. 10 | func OrderParams(values url.Values) string { 11 | 12 | // get the keys 13 | var keys []string 14 | for k, _ := range values { 15 | keys = append(keys, k) 16 | } 17 | 18 | // sort the keys 19 | sort.Strings(keys) 20 | 21 | // ordered items 22 | var ordered []string 23 | 24 | // sort the values 25 | for _, key := range keys { 26 | sort.Strings(values[key]) 27 | for _, val := range values[key] { 28 | ordered = append(ordered, stewstrings.MergeStrings(key, "=", val)) 29 | } 30 | } 31 | 32 | joined := stewstrings.JoinStrings("&", ordered...) 33 | return joined 34 | 35 | } 36 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestOrderParams(t *testing.T) { 10 | 11 | values := make(url.Values) 12 | values.Add("~key", "ABC123") 13 | values.Add(":name", "!Mat") 14 | values.Add(":name", "!Laurie") 15 | values.Add(":age", ">20") 16 | values.Add(":something", ">2 0") 17 | 18 | ordered := OrderParams(values) 19 | 20 | //TODO: @matryer does this make sense? We no longer encode right? 21 | //assert.Equal(t, "%3Aage=%3E20&%3Aname=%21Laurie&%3Aname=%21Mat&%3Asomething=%3E2+0&~key=ABC123", ordered) 22 | assert.Equal(t, ":age=>20&:name=!Laurie&:name=!Mat&:something=>2 0&~key=ABC123", ordered) 23 | 24 | } 25 | --------------------------------------------------------------------------------