Skip to content

Commit

Permalink
Merge pull request #3 from tkhq/rno/x-stamp
Browse files Browse the repository at this point in the history
Change the CLI to produce valid X-Stamp headers

Former-commit-id: 33ad5b5
  • Loading branch information
r-n-o authored Dec 15, 2022
2 parents 8b9aada + 81e9d34 commit 114f2e5
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 99 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local-release:
test: build/turnkey
go test ./...

build/turnkey:
build/turnkey: main.go internal/
go build -o build/turnkey main.go

.PHONY: clean
Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ To release:
# Generate a Github token with "write:packages"
# ==> https://github.com/settings/tokens
$ export GITHUB_TOKEN=<your token>
$ git tag -a vx.y.z -m "New Release: x.y.z"
$ git tag -a vx.y.z -m "New release: x.y.z"
$ git push origin vx.y.z
$ goreleaser release --rm-dist
```
Expand All @@ -38,19 +38,20 @@ brew install turnkey
Create a new API key:

```sh
turnkey gen --name rno
Creating /Users/rno/.tk/rno.public
Creating /Users/rno/.tk/rno.private
$ turnkey gen --name my-test-key
{
"privateKeyFile": "/Users/rno/.config/turnkey/keys/my-test-key.private",
"publicKeyFile": "/Users/rno/.config/turnkey/keys/my-test-key.public"
}
```

Sign a request:

```sh
turnkey approve-request --method POST --path /api/v1/sign --body '{"payload": "hello from TKHQ"}' --key=rno
Raw signature: 3046022100a99781a6b1d7ff7c4ce3951ded09a7757c74f1c6d7c7e1a2e617ac2921d74674022100f75d167abe426eb8f89884afe5e864cb965c6370611566f50b46690209b3a95b
Approval header: X-Approved-By-035acbc8b7751b7703736ae16cb22112451372f7b77717bbecdfa8300d4038432: 3046022100a99781a6b1d7ff7c4ce3951ded09a7757c74f1c6d7c7e1a2e617ac2921d74674022100f75d167abe426eb8f89884afe5e864cb965c6370611566f50b46690209b3a95b
--------
To make this request with curl:
curl -X POST -d {"payload": "hello from TKHQ"} -H'X-Approved-By-035acbc8b7751b7703736ae16cb22112451372f7b77717bbecdfa8300d4038432: 3046022100a99781a6b1d7ff7c4ce3951ded09a7757c74f1c6d7c7e1a2e617ac2921d74674022100f75d167abe426eb8f89884afe5e864cb965c6370611566f50b46690209b3a95b' -v 'https://api.turnkey.io/api/v1/sign'
--------
$ turnkey approve-request --host api.turnkey.io --path /api/v1/sign --body '{"payload": "hello from TKHQ"}' --key=my-test-key
{
"curlCommand": "curl -X POST -d'{\"payload\": \"hello from TKHQ\"}' -H'X-Stamp: eyJwdWJsaWNLZXkiOiIwM2JmMTYyNTc2ZWI4ZGZlY2YzM2Q5Mjc1ZDA5NTk1Mjg0ZjZjNGRmMGRiNjE1NmMzYzU4Mjc3Nzg4NmEwZWUwYWMiLCJzaWduYXR1cmUiOiIzMDQ0MDIyMDZiMmRlYmIwYjA3YmYwMDJlMjI1ZmQ4NTgzZjZmNGUxNGE5YTUxYWRiYWJjNDAyYzY5YTZlN2Q4N2ViNWNjMDgwMjIwMjE0ZTdkMGJlODFjMGYyNDEyOWE0MmNkZGFlOTUxYTBmZTViMGM1Mzc3YjM2NzZiOTUyNDgyNmYwODdhMWU4ZiIsInNjaGVtZSI6IlNJR05BVFVSRV9TQ0hFTUVfVEtfQVBJX1AyNTYifQ' -v 'https://api.turnkey.io/api/v1/sign'",
"message": "{\"payload\": \"hello from TKHQ\"}",
"stamp": "eyJwdWJsaWNLZXkiOiIwM2JmMTYyNTc2ZWI4ZGZlY2YzM2Q5Mjc1ZDA5NTk1Mjg0ZjZjNGRmMGRiNjE1NmMzYzU4Mjc3Nzg4NmEwZWUwYWMiLCJzaWduYXR1cmUiOiIzMDQ0MDIyMDZiMmRlYmIwYjA3YmYwMDJlMjI1ZmQ4NTgzZjZmNGUxNGE5YTUxYWRiYWJjNDAyYzY5YTZlN2Q4N2ViNWNjMDgwMjIwMjE0ZTdkMGJlODFjMGYyNDEyOWE0MmNkZGFlOTUxYTBmZTViMGM1Mzc3YjM2NzZiOTUyNDgyNmYwODdhMWU4ZiIsInNjaGVtZSI6IlNJR05BVFVSRV9TQ0hFTUVfVEtfQVBJX1AyNTYifQ"
}
```
2 changes: 1 addition & 1 deletion fixtures/testkey.public
Original file line number Diff line number Diff line change
@@ -1 +1 @@
035acbc8b7751b7703736ae16cb22112451372f7b77717bbecdfa8300d4038432
0305acbc8b7751b7703736ae16cb22112451372f7b77717bbecdfa8300d4038432
33 changes: 31 additions & 2 deletions internal/apikey/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"

"github.com/pkg/errors"
)

// Struct to hold both serialized and ecdsa lib friendly version of a public/private key pair
Expand All @@ -19,6 +23,17 @@ type ApiKey struct {
publicKey *ecdsa.PublicKey
}

const TURNKEY_API_SIGNATURE_SCHEME = "SIGNATURE_SCHEME_TK_API_P256"

type ApiStamp struct {
// API public key, hex-encoded
PublicKey string `json:"publicKey"`
// P-256 signature bytes, hex-coded
Signature string `json:"signature"`
// Signature scheme. Must be set to "SIGNATURE_SCHEME_TK_API_P256"
Scheme string `json:"scheme"`
}

// Create a new Turnkey API key
func NewTkApiKey() (*ApiKey, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expand Down Expand Up @@ -126,12 +141,26 @@ func DecodeTKPublicKey(encodedPublicKey string) (*ecdsa.PublicKey, error) {
return publicKey, nil
}

func Sign(message string, apiKey *ApiKey) (string, error) {
// / Takes a message and returns the proper API stamp
// / This value should be inserted in a "X-Stamp" header
func Stamp(message string, apiKey *ApiKey) (string, error) {
hash := sha256.Sum256([]byte(message))

sigBytes, err := ecdsa.SignASN1(rand.Reader, apiKey.privateKey, hash[:])
if err != nil {
return "", err
}
return hex.EncodeToString(sigBytes), nil
sigHex := hex.EncodeToString(sigBytes)

stamp := ApiStamp{
PublicKey: apiKey.TkPublicKey,
Signature: sigHex,
Scheme: TURNKEY_API_SIGNATURE_SCHEME,
}
jsonStamp, err := json.Marshal(stamp)
if err != nil {
return "", errors.Wrap(err, "cannot marshall API stamp to JSON")
}
encodedStamp := base64.RawURLEncoding.EncodeToString(jsonStamp)
return encodedStamp, nil
}
19 changes: 15 additions & 4 deletions internal/apikey/apikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package apikey_test
import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"testing"

Expand Down Expand Up @@ -42,17 +44,26 @@ func Test_FromTkPrivateKey(t *testing.T) {

func Test_Sign(t *testing.T) {
tkPrivateKey := "487f361ddfd73440e707f4daa6775b376859e8a3c9f29b3bb694a12927c0213c"
tkPublicKey := "02f739f8c77b32f4d5f13265861febd76e7a9c61a1140d296b8c16302508870316"

apiKey, err := apikey.FromTkPrivateKey(tkPrivateKey)
assert.Nil(t, err)

sig, err := apikey.Sign("hello", apiKey)
stampHeader, err := apikey.Stamp("hello", apiKey)
assert.Nil(t, err)
sigBytes, err := hex.DecodeString(sig)

decodedHeaderBytes, err := base64.RawURLEncoding.DecodeString(stampHeader)
assert.Nil(t, err)

var stamp *apikey.ApiStamp
err = json.Unmarshal(decodedHeaderBytes, &stamp)
assert.Nil(t, err)
assert.Equal(t, stamp.PublicKey, "02f739f8c77b32f4d5f13265861febd76e7a9c61a1140d296b8c16302508870316")
assert.Equal(t, stamp.Scheme, "SIGNATURE_SCHEME_TK_API_P256")

sigBytes, err := hex.DecodeString(stamp.Signature)
assert.Nil(t, err)

publicKey, err := apikey.DecodeTKPublicKey(tkPublicKey)
publicKey, err := apikey.DecodeTKPublicKey(stamp.PublicKey)
assert.Nil(t, err)

// Verify the soundness of the hash:
Expand Down
5 changes: 0 additions & 5 deletions internal/apikey/serialize.go

This file was deleted.

8 changes: 0 additions & 8 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ func Host() *cli.StringFlag {
}
}

func Method() *cli.StringFlag {
return &cli.StringFlag{
Name: "method",
Usage: "HTTP Method. Should be \"GET\" or \"POST\"",
Required: true,
}
}

func Path() *cli.StringFlag {
return &cli.StringFlag{
Name: "path",
Expand Down
85 changes: 23 additions & 62 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,11 @@ func main() {
UsageText: "generate an approval and make an HTTP(s) request",
Flags: []cli.Flag{
flags.Host(),
flags.Method(),
flags.Path(),
flags.Body(),
flags.Key(),
},
Action: func(cCtx *cli.Context) error {
method := cCtx.String("method")
host := cCtx.String("host")
path := cCtx.String("path")
body := cCtx.String("body")
Expand All @@ -106,33 +104,21 @@ func main() {
protocol = "http"
}

signaturePayload := apikey.SerializeRequest(method, host, path, body)

key := cCtx.String("key")
apiKey, err := clifs.GetApiKey(key)
if err != nil {
log.Fatalf("Unable to retrieve API key: %v", err)
}

signature, err := apikey.Sign(signaturePayload, apiKey)
stamp, err := apikey.Stamp(body, apiKey)
if err != nil {
log.Fatalln(err)
return cli.Exit("Failed to produce a valid signature", 1)
return cli.Exit("Failed to produce a valid API stamp", 1)
}

var response *http.Response
if method == "GET" {
response, err = get(apiKey, protocol, host, path, signature)
if err != nil {
log.Fatalln(err)
}
} else if method == "POST" {
response, err = post(apiKey, protocol, host, path, body, signature)
if err != nil {
log.Fatalln(err)
}
} else {
return cli.Exit("Invalid method", 1)
response, err := post(protocol, host, path, body, stamp)
if err != nil {
log.Fatalln(err)
}

displayResponse, err := display.DisplayResponse(response)
Expand All @@ -150,37 +136,32 @@ func main() {
Usage: "approve a request",
UsageText: "generate an approval over an HTTP request",
Flags: []cli.Flag{
flags.Key(),
flags.Host(),
flags.Method(),
flags.Path(),
flags.Body(),
flags.KeysFolder(),
},
Action: func(cCtx *cli.Context) error {
method := cCtx.String("method")
host := cCtx.String("host")
path := cCtx.String("path")
body := cCtx.String("body")

signaturePayload := apikey.SerializeRequest(method, host, path, body)

key := cCtx.String("key")
apiKey, err := clifs.GetApiKey(key)
if err != nil {
log.Fatalf("Unable to retrieve API key: %v", err)
}

signature, err := apikey.Sign(signaturePayload, apiKey)
stamp, err := apikey.Stamp(body, apiKey)
if err != nil {
log.Fatalln(err)
return cli.Exit("Failed to produce a valid signature", 1)
return cli.Exit("Failed to produce a valid stamp", 1)
}

jsonBytes, err := json.MarshalIndent(map[string]interface{}{
"message": fmt.Sprintf("%q", signaturePayload),
"signature": signature,
"approvalHeader": approvalHeader(apiKey, signature),
"curlCommand": generateCurlCommand(apiKey, method, host, path, body, signature),
"message": body,
"stamp": stamp,
"curlCommand": generateCurlCommand(host, path, body, stamp),
}, "", " ")
if err != nil {
log.Fatalf("Unable to serialize output to JSON: %v", err)
Expand All @@ -191,9 +172,9 @@ func main() {
},
},
{
Name: "sign",
Name: "stamp",
Aliases: []string{"s"},
Usage: "sign an arbitrary message",
Usage: "sign an arbitrary message and produce a valid API Stamp",
Flags: []cli.Flag{
flags.Message(),
flags.Key(),
Expand Down Expand Up @@ -226,15 +207,15 @@ func main() {
log.Fatalln(err)
return cli.Exit("Could recover API key from private key file content", 1)
}
signature, err := apikey.Sign(message, apiKey)
stamp, err := apikey.Stamp(message, apiKey)
if err != nil {
log.Fatalln(err)
return cli.Exit("Failed to produce a valid signature", 1)
return cli.Exit("Failed to produce a valid stamp", 1)
}

jsonBytes, err := json.MarshalIndent(map[string]interface{}{
"message": fmt.Sprintf("%q", message),
"signature": signature,
"message": fmt.Sprintf("%q", message),
"stamp": stamp,
}, "", " ")
if err != nil {
log.Fatalf("Unable to serialize output to JSON: %v", err)
Expand All @@ -252,42 +233,22 @@ func main() {
}
}

func generateCurlCommand(apiKey *apikey.ApiKey, method, host, path, body, signature string) string {
if method == "POST" {
return fmt.Sprintf("curl -X POST -d'%s' -H'%s' -v 'https://%s%s'", body, approvalHeader(apiKey, signature), host, path)
} else {
return fmt.Sprintf("curl -H'%s' -v 'https://%s%s'", approvalHeader(apiKey, signature), host, path)
}
}

func approvalHeader(apiKey *apikey.ApiKey, signature string) string {
return fmt.Sprintf("X-Approved-By-%s: %s", apiKey.TkPublicKey, signature)
func generateCurlCommand(host, path, body, stamp string) string {
return fmt.Sprintf("curl -X POST -d'%s' -H'%s' -v 'https://%s%s'", body, stampHeader(stamp), host, path)
}

func get(key *apikey.ApiKey, protocol string, host string, path string, signature string) (*http.Response, error) {
url := fmt.Sprintf("%s://%s%s", protocol, host, path)
req, _ := http.NewRequest("GET", url, nil)

headerName := fmt.Sprintf("X-Approved-By-%s", key.TkPublicKey)
req.Header.Set(headerName, signature)

client := http.Client{}
response, err := client.Do(req)
if err != nil {
return nil, err
}
return response, nil
func stampHeader(stamp string) string {
return fmt.Sprintf("X-Stamp: %s", stamp)
}

func post(key *apikey.ApiKey, protocol string, host string, path string, body string, signature string) (*http.Response, error) {
func post(protocol string, host string, path string, body string, stamp string) (*http.Response, error) {
url := fmt.Sprintf("%s://%s%s", protocol, host, path)
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil {
return nil, errors.Wrap(err, "error while creating HTTP POST request")
}

headerName := fmt.Sprintf("X-Approved-By-%s", key.TkPublicKey)
req.Header.Set(headerName, signature)
req.Header.Set("X-Stamp", stamp)
client := http.Client{}
response, err := client.Do(req)
if err != nil {
Expand Down
Loading

0 comments on commit 114f2e5

Please sign in to comment.