Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/tcnksm/go-gitconfig v0.1.2
github.com/theckman/yacspin v0.13.12
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
)

require (
Expand All @@ -57,7 +58,6 @@ require (
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/interface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"crypto/ed25519"
"net/http"

"github.com/fastly/go-fastly/v7/fastly"
Expand Down Expand Up @@ -345,6 +346,8 @@ type Interface interface {
GetSecret(i *fastly.GetSecretInput) (*fastly.Secret, error)
DeleteSecret(i *fastly.DeleteSecretInput) error
ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error)
CreateClientKey() (*fastly.ClientKey, error)
GetSigningKey() (ed25519.PublicKey, error)

CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error)
}
Expand Down
53 changes: 52 additions & 1 deletion pkg/commands/secretstoreentry/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package secretstoreentry

import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
Expand All @@ -21,6 +22,24 @@ const (
maxSecretLen = maxSecretKiB * 1024
)

// The signing key is a public key that is used to sign client keys.
// It's meant to be a long-lived key and infrequently (if ever) rotated.
// Hardcoding it in the CLI gives us the benefit of distributing it via
// a different channel from the client keys it's signing.
//
// When we do rotate it, we will need to update this value and release a
// new version of the CLI. However, users can also override this with
// the FASTLY_USE_API_SIGNING_KEY environment variable.
var signingKey []byte = mustDecode("CrO/A92vkxEZjtTW7D/Sr+1EMf/q9BahC0sfLkWa+0k=")

func mustDecode(s string) []byte {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
panic(err)
}
return b
}

// NewCreateCommand returns a usable command registered under the parent.
func NewCreateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *CreateCommand {
c := CreateCommand{
Expand Down Expand Up @@ -94,7 +113,6 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error {

case c.secretFile != "":
var err error
// nosemgrep: trailofbits.go.questionable-assignment.questionable-assignment
if c.Input.Secret, err = os.ReadFile(c.secretFile); err != nil {
return err
}
Expand All @@ -111,6 +129,39 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) error {
return errMaxSecretLength
}

ck, err := c.Globals.APIClient.CreateClientKey()
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

sk, err := c.Globals.APIClient.GetSigningKey()
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

if !bytes.Equal(sk, signingKey) && os.Getenv("FASTLY_USE_API_SIGNING_KEY") == "" {
err := fmt.Errorf("API signing key does not match expected value")
c.Globals.ErrLog.Add(err)
return err
}

if !ck.ValidateSignature(sk) {
err := fmt.Errorf("unable to validate signature of client key")
c.Globals.ErrLog.Add(err)
return err
}

wrapped, err := ck.Encrypt(c.Input.Secret)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

c.Input.Secret = wrapped
c.Input.ClientKey = ck.PublicKey

o, err := c.Globals.APIClient.CreateSecret(&c.Input)
if err != nil {
c.Globals.ErrLog.Add(err)
Expand Down
60 changes: 54 additions & 6 deletions pkg/commands/secretstoreentry/secretstoreentry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ package secretstoreentry_test

import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"os"
"path"
"runtime"
"testing"
"time"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/commands/secretstoreentry"
fstfmt "github.com/fastly/cli/pkg/fmt"
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
"github.com/fastly/go-fastly/v7/fastly"
"golang.org/x/crypto/nacl/box"
)

func TestCreateSecretCommand(t *testing.T) {
Expand All @@ -33,6 +37,33 @@ func TestCreateSecretCommand(t *testing.T) {
}
doesNotExistFile := path.Join(tmpDir, "DOES-NOT-EXIST")

ckPub, ckPriv, err := box.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}

skPub, skPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}

ck := &fastly.ClientKey{
PublicKey: ckPub[:],
Signature: ed25519.Sign(skPriv, ckPub[:]),
ExpiresAt: time.Now().Add(time.Hour),
}

mockCreateClientKey := func() (*fastly.ClientKey, error) { return ck, nil }
mockGetSigningKey := func() (ed25519.PublicKey, error) { return skPub, nil }

decrypt := func(ciphertext []byte) (string, error) {
plaintext, ok := box.OpenAnonymous(nil, ciphertext, ckPub, ckPriv)
if !ok {
return "", errors.New("failed to decrypt")
}
return string(plaintext), nil
}

scenarios := []struct {
args string
stdin string
Expand Down Expand Up @@ -67,9 +98,13 @@ func TestCreateSecretCommand(t *testing.T) {
args: fmt.Sprintf("create --store-id %s --name %s --stdin", storeID, secretName),
stdin: secretValue,
api: mock.API{
CreateClientKeyFn: mockCreateClientKey,
GetSigningKeyFn: mockGetSigningKey,
CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) {
if secret := string(i.Secret); secret != secretValue {
return nil, fmt.Errorf("invalid secret: %s", secret)
if got, err := decrypt(i.Secret); err != nil {
return nil, err
} else if got != secretValue {
return nil, fmt.Errorf("invalid secret: %s", got)
}
return &fastly.Secret{
Name: i.Name,
Expand All @@ -84,9 +119,13 @@ func TestCreateSecretCommand(t *testing.T) {
{
args: fmt.Sprintf("create --store-id %s --name %s --file %s", storeID, secretName, secretFile),
api: mock.API{
CreateClientKeyFn: mockCreateClientKey,
GetSigningKeyFn: mockGetSigningKey,
CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) {
if secret := string(i.Secret); secret != secretValue {
return nil, fmt.Errorf("invalid secret: %s", secret)
if got, err := decrypt(i.Secret); err != nil {
return nil, err
} else if got != secretValue {
return nil, fmt.Errorf("invalid secret: %s", got)
}
return &fastly.Secret{
Name: i.Name,
Expand All @@ -100,9 +139,13 @@ func TestCreateSecretCommand(t *testing.T) {
{
args: fmt.Sprintf("create --store-id %s --name %s --file %s --json", storeID, secretName, secretFile),
api: mock.API{
CreateClientKeyFn: mockCreateClientKey,
GetSigningKeyFn: mockGetSigningKey,
CreateSecretFn: func(i *fastly.CreateSecretInput) (*fastly.Secret, error) {
if secret := string(i.Secret); secret != secretValue {
return nil, fmt.Errorf("invalid secret: %s", secret)
if got, err := decrypt(i.Secret); err != nil {
return nil, err
} else if got != secretValue {
return nil, fmt.Errorf("invalid secret: %s", got)
}
return &fastly.Secret{
Name: i.Name,
Expand Down Expand Up @@ -138,6 +181,11 @@ func TestCreateSecretCommand(t *testing.T) {

opts.APIClient = mock.APIClient(testcase.api)

// Tests generate their own signing keys, which won't match
// the hardcoded value. Disable the check against the
// hardcoded value.
t.Setenv("FASTLY_USE_API_SIGNING_KEY", "1")

err := app.Run(opts)

testutil.AssertErrorContains(t, err, testcase.wantError)
Expand Down
14 changes: 14 additions & 0 deletions pkg/mock/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mock

import (
"crypto/ed25519"

"github.com/fastly/go-fastly/v7/fastly"
)

Expand Down Expand Up @@ -336,6 +338,8 @@ type API struct {
GetSecretFn func(i *fastly.GetSecretInput) (*fastly.Secret, error)
DeleteSecretFn func(i *fastly.DeleteSecretInput) error
ListSecretsFn func(i *fastly.ListSecretsInput) (*fastly.Secrets, error)
CreateClientKeyFn func() (*fastly.ClientKey, error)
GetSigningKeyFn func() (ed25519.PublicKey, error)

CreateResourceFn func(i *fastly.CreateResourceInput) (*fastly.Resource, error)
}
Expand Down Expand Up @@ -1710,6 +1714,16 @@ func (m API) ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error) {
return m.ListSecretsFn(i)
}

// CreateClientKey implements Interface.
func (m API) CreateClientKey() (*fastly.ClientKey, error) {
return m.CreateClientKeyFn()
}

// GetSigningKey implements Interface.
func (m API) GetSigningKey() (ed25519.PublicKey, error) {
return m.GetSigningKeyFn()
}

// CreateResource implements Interface.
func (m API) CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error) {
return m.CreateResourceFn(i)
Expand Down