github.com/sentienttechnologies/studio-go-runner@v0.0.0-20201118202441-6d21f2ced8ee/internal/runner/signing_store_test.go (about)

     1  // Copyright 2020 (c) Cognizant Digital Business, Evolutionary AI. All rights reserved. Issued under the Apache 2.0 License.
     2  
     3  package runner
     4  
     5  // This file contains the unit tests for the message signing
     6  // features of the runner
     7  
     8  import (
     9  	"context"
    10  	"crypto/rand"
    11  	"fmt"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/go-stack/stack"
    20  	"github.com/go-test/deep"
    21  	"github.com/jjeffery/kv"
    22  	"github.com/karlmutch/k8s"
    23  	core "github.com/karlmutch/k8s/apis/core/v1"
    24  	"github.com/rs/xid"
    25  
    26  	"golang.org/x/crypto/ed25519"
    27  	"golang.org/x/crypto/ssh"
    28  )
    29  
    30  var (
    31  	sigWatchDone = context.Background()
    32  	initSigWatch sync.Once
    33  )
    34  
    35  func InitSigWatch() {
    36  	StartSigWatch(sigWatchDone, "/runner/certs/queues/signing")
    37  }
    38  
    39  func StartSigWatch(ctx context.Context, sigDir string) {
    40  
    41  	errorC := make(chan kv.Error)
    42  	defer close(errorC)
    43  
    44  	go func() {
    45  		for {
    46  			select {
    47  			case err := <-errorC:
    48  				if err == nil {
    49  					return
    50  				}
    51  				fmt.Println(err.Error())
    52  			case <-ctx.Done():
    53  				return
    54  			}
    55  		}
    56  	}()
    57  
    58  	// The directory location is the standard one for our containers inside Kubernetes
    59  	// for mounting signatures from the signature 'secret' resource
    60  	go InitSignatures(ctx, sigDir, errorC)
    61  }
    62  
    63  // TestFingerprint does an expected value test for the SHA256 fingerprint
    64  // generation facilities in Go for our purposes.
    65  //
    66  func TestSignatureFingerprint(t *testing.T) {
    67  	pKey := []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFITo06Pk8sqCMoMHPaQiQ7BY3pjf7OE8BDcsnYozmIG kmutch@awsdev")
    68  
    69  	expected := "SHA256:rM9uPGQWiB8BrF542H5tJdVQoWU2+jw00w1KnXjywTY"
    70  
    71  	// Create a new TMPDIR so that we can cleanup
    72  	tmpDir, errGo := ioutil.TempDir("", "")
    73  	if errGo != nil {
    74  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
    75  	}
    76  	defer func() {
    77  		os.RemoveAll(tmpDir)
    78  	}()
    79  
    80  	testFN := filepath.Join(tmpDir, "public_key.pub")
    81  	if errGo := ioutil.WriteFile(testFN, pKey, 0600); errGo != nil {
    82  		t.Fatal(kv.Wrap(errGo).With("filename", testFN).With("stack", stack.Trace().TrimRuntime()))
    83  	}
    84  	fp, err := getFingerprint(testFN)
    85  	if err != nil {
    86  		t.Fatal(err)
    87  	}
    88  	if diff := deep.Equal(expected, fp); diff != nil {
    89  		t.Fatal(diff)
    90  	}
    91  }
    92  
    93  func generateTestKey() (publicKey ssh.PublicKey, fp string, err kv.Error) {
    94  	pubKey, _, errGo := ed25519.GenerateKey(rand.Reader)
    95  	if errGo != nil {
    96  		return nil, "", kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime())
    97  	}
    98  	sshKey, errGo := ssh.NewPublicKey(pubKey)
    99  	if errGo != nil {
   100  		return nil, "", kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime())
   101  	}
   102  	return sshKey, ssh.FingerprintSHA256(sshKey), nil
   103  }
   104  
   105  // TestSignatureBase is used to exercise a simple text signature use case
   106  //
   107  func TestSignatureBase(t *testing.T) {
   108  	pubKey, prvKey, errGo := ed25519.GenerateKey(rand.Reader)
   109  	if errGo != nil {
   110  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   111  	}
   112  	sshKey, errGo := ssh.NewPublicKey(pubKey)
   113  	if errGo != nil {
   114  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   115  	}
   116  	signer, errGo := ssh.NewSignerFromKey(prvKey)
   117  	if errGo != nil {
   118  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   119  	}
   120  	txt := []byte("Hello World")
   121  	sig, errGo := signer.Sign(rand.Reader, txt)
   122  	if errGo != nil {
   123  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   124  	}
   125  	if errGo = sshKey.Verify(txt, sig); errGo != nil {
   126  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   127  	}
   128  }
   129  
   130  // TestSignatureCascade will add signatures to the signature config map and will
   131  // then run a series of queries against the runners internal record of signatures
   132  // and queues and will validate the correct selection of partial queue names that
   133  // were selected.  For this test we will use a temporary directory to populate
   134  // signatures.
   135  //
   136  func TestSignatureCascade(t *testing.T) {
   137  
   138  	// Create a directory to be used with signatures
   139  	dir, errGo := ioutil.TempDir("", xid.New().String())
   140  	if errGo != nil {
   141  		t.Fatal(kv.Wrap(errGo).With("stack", stack.Trace().TrimRuntime()))
   142  	}
   143  	defer os.RemoveAll(dir)
   144  
   145  	// Start a signature watching function as the default wont be running
   146  	// inside tests without the production main
   147  	watchDone, cancelWatch := context.WithCancel(context.Background())
   148  	defer cancelWatch()
   149  
   150  	StartSigWatch(watchDone, dir)
   151  
   152  	// Contains all of the matches to be attempted that are not exact matches
   153  	attemptMatches := map[string]string{
   154  		"r":                 "",
   155  		"rmq_z":             "rmq_",
   156  		"rmq_donn_":         "rmq_donn",
   157  		"rmq_andrei_andrei": "rmq_andrei",
   158  		"rmq_karlx":         "rmq_karl",
   159  	}
   160  
   161  	// Queue names against which we are going to add public keys
   162  	queues := []string{"rmq_", "rmq_karl", "rmq_andrei", "rmq_k", "rmq_ka", "rmq_kar", "rmq_donn", "rmq_do"}
   163  
   164  	type keyTracker struct {
   165  		q   string
   166  		fp  string
   167  		key ssh.PublicKey
   168  	}
   169  	keys := map[string]keyTracker{}
   170  
   171  	for _, q := range queues {
   172  		// Now write a set of test files to be used for selecting signatures, and record
   173  		// the data we have written to exercise the signatures implementation
   174  		pubKey, fp, err := generateTestKey()
   175  		if err != nil {
   176  			t.Fatal(err)
   177  		}
   178  		keys[q] = keyTracker{
   179  			q:   q,
   180  			fp:  fp,
   181  			key: pubKey,
   182  		}
   183  
   184  		// Write the secrets to files
   185  		fn := filepath.Join(dir, q)
   186  		if errGo = ioutil.WriteFile(fn, ssh.MarshalAuthorizedKey(pubKey), 0600); errGo != nil {
   187  			t.Fatal(kv.Wrap(errGo).With("file", fn).With("stack", stack.Trace().TrimRuntime()))
   188  		}
   189  		//signatures.Data[newKey] = []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFITo06Pk8sqCMoMHPaQiQ7BY3pjf7OE8BDcsnYozmIG kmutch@awsdev")
   190  		//expectedFingerprint := "SHA256:rM9uPGQWiB8BrF542H5tJdVQoWU2+jw00w1KnXjywTY"
   191  		// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDA/bv8Usu/5rqUk6mJnYMD0gXgXn/8gQpcnVR4dt4tm
   192  
   193  		//SHA256:VV6NzLszADZ+PHkzK0k3TntaksOmiv4o9rJ3s0mrJ6U
   194  
   195  	}
   196  
   197  	// Get access to the signature store
   198  	sigs := GetSignatures()
   199  
   200  	// Wait for the signature store to refresh itself with our new files
   201  	<-GetSignaturesRefresh().Done()
   202  
   203  	// Go through the queue names looking for matches
   204  	for _, aCase := range keys {
   205  		key, fp, err := sigs.Get(aCase.q)
   206  		if err != nil {
   207  			t.Fatal(err)
   208  		}
   209  		if diff := deep.Equal(aCase.fp, fp); diff != nil {
   210  			t.Fatal(diff)
   211  		}
   212  		if diff := deep.Equal(aCase.key, key); diff != nil {
   213  			t.Fatal(diff)
   214  		}
   215  		key, fp, err = sigs.Select(aCase.q)
   216  		if err != nil {
   217  			t.Fatal(err)
   218  		}
   219  		if diff := deep.Equal(aCase.fp, fp); diff != nil {
   220  			t.Fatal(diff)
   221  		}
   222  		if diff := deep.Equal(aCase.key, key); diff != nil {
   223  			t.Fatal(diff)
   224  		}
   225  	}
   226  
   227  	// Go through the queue names looking for prefixes
   228  	for prefix, qExpect := range attemptMatches {
   229  		key, _, err := sigs.Get(prefix)
   230  		if err == nil {
   231  			t.Fatal(kv.NewError("expected error, error not returned").With("prefix", prefix, "queueExpected", qExpect).With("stack", stack.Trace().TrimRuntime()))
   232  		}
   233  		if key != nil {
   234  			t.Fatal(kv.NewError("key found, expected error").With("prefix", prefix, "queueExpected", qExpect).With("stack", stack.Trace().TrimRuntime()))
   235  		}
   236  
   237  		key, fp, err := sigs.Select(prefix)
   238  		if key == nil && err != nil && len(qExpect) == 0 {
   239  			continue
   240  		}
   241  		if len(qExpect) == 0 && key == nil {
   242  			if err == nil {
   243  				t.Fatal(kv.NewError("expected error, error not returned").With("prefix", prefix, "queueExpected", qExpect).With("stack", stack.Trace().TrimRuntime()))
   244  			}
   245  			continue
   246  		}
   247  
   248  		expectedKey := keys[qExpect].key
   249  		if diff := deep.Equal(key, expectedKey); diff != nil {
   250  			fmt.Println("Test case", "prefix", prefix, "queueExpected", qExpect)
   251  			t.Fatal(diff)
   252  		}
   253  		if diff := deep.Equal(fp, keys[qExpect].fp); diff != nil {
   254  			t.Fatal(diff)
   255  		}
   256  		if diff := deep.Equal(qExpect, keys[qExpect].q); diff != nil {
   257  			t.Fatal(diff)
   258  		}
   259  	}
   260  }
   261  
   262  // TestSignatureWatch exercises the signature file event watching feature.  This
   263  // feature monitors a directory for signature files appearing and disappearing
   264  // as an administrator manipulates the message signature public keys that will
   265  // be used to authenticate that messages for the runner are genuine.
   266  //
   267  func TestSignatureWatch(t *testing.T) {
   268  	if !*useK8s {
   269  		t.Skip("kubernetes specific testing disabled")
   270  	}
   271  
   272  	if err := IsAliveK8s(); err != nil {
   273  		t.Fatal(err)
   274  	}
   275  
   276  	// Start a signature watcher that will output any errors or failures
   277  	// in the background
   278  	initSigWatch.Do(InitSigWatch)
   279  
   280  	// The downward API within K8s is configured within the build YAML
   281  	// to pass the pods namespace into the pods environment table.
   282  	namespace, isPresent := os.LookupEnv("K8S_NAMESPACE")
   283  	if !isPresent {
   284  		t.Fatal(kv.NewError("K8S_NAMESPACE missing").With("stack", stack.Trace().TrimRuntime()))
   285  	}
   286  
   287  	// Get access to the signature store
   288  	sigs := GetSignatures()
   289  
   290  	// Start a ticker that will be used throughout this test
   291  	tick := time.NewTicker(time.Second)
   292  	defer tick.Stop()
   293  
   294  	// Use the kubernetes client to modify the config map and then
   295  	// check the signature store
   296  	// K8s API receiver to be used to manipulate the config maps we are testing
   297  	client, errGo := k8s.NewInClusterClient()
   298  	if errGo != nil {
   299  		t.Fatal(errGo)
   300  	}
   301  
   302  	signatures := &core.Secret{}
   303  	if errGo = client.Get(context.Background(), namespace, "studioml-signing", signatures); errGo != nil {
   304  		t.Fatal(errGo)
   305  	}
   306  
   307  	// Add a key
   308  	newKey := xid.New().String()
   309  	signatures.Data[newKey] = []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFITo06Pk8sqCMoMHPaQiQ7BY3pjf7OE8BDcsnYozmIG kmutch@awsdev")
   310  	expectedFingerprint := "SHA256:rM9uPGQWiB8BrF542H5tJdVQoWU2+jw00w1KnXjywTY"
   311  
   312  	if errGo := client.Update(context.Background(), signatures); errGo != nil {
   313  		t.Fatal(errGo)
   314  	}
   315  	// Wait for the key to appear in the signatures collection
   316  	func() {
   317  		for {
   318  			select {
   319  			case <-tick.C:
   320  				_, fp, err := sigs.Get(newKey)
   321  				if err != nil {
   322  					continue
   323  				}
   324  				if diff := deep.Equal(expectedFingerprint, fp); diff != nil {
   325  					t.Fatal(diff)
   326  				}
   327  				return
   328  			}
   329  		}
   330  	}()
   331  
   332  	// Change a key
   333  	signatures.Data[newKey] = []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKohNVg9rRRrUlOSdksrXczWzuR9jN+NE2ZpX2Myw+k9 kmutch@awsdev")
   334  	expectedFingerprint = "SHA256:0Q8tSkwT/m8p4eAsUIFDUfonQZyleEla5nFQCvWE5lk"
   335  
   336  	if errGo := client.Update(context.Background(), signatures); errGo != nil {
   337  		t.Fatal(errGo)
   338  	}
   339  	// Wait for the key to change its value in the signatures collection
   340  	func() {
   341  		for {
   342  			select {
   343  			case <-tick.C:
   344  				_, fp, err := sigs.Get(newKey)
   345  				if err != nil {
   346  					t.Fatal(err)
   347  				}
   348  				if diff := deep.Equal(expectedFingerprint, fp); diff == nil {
   349  					return
   350  				}
   351  			}
   352  		}
   353  	}()
   354  
   355  	// Delete a Key
   356  	delete(signatures.Data, newKey)
   357  	if errGo := client.Update(context.Background(), signatures); errGo != nil {
   358  		t.Fatal(errGo)
   359  	}
   360  	// Wait for the key to disappear from the signatures collection
   361  	func() {
   362  		for {
   363  			select {
   364  			case <-tick.C:
   365  				_, _, err := sigs.Get(newKey)
   366  				if err != nil {
   367  					return
   368  				}
   369  			}
   370  		}
   371  	}()
   372  
   373  	// Add a key
   374  	signatures.Data[newKey] = []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFITo06Pk8sqCMoMHPaQiQ7BY3pjf7OE8BDcsnYozmIG kmutch@awsdev")
   375  	expectedFingerprint = "SHA256:rM9uPGQWiB8BrF542H5tJdVQoWU2+jw00w1KnXjywTY"
   376  
   377  	if errGo := client.Update(context.Background(), signatures); errGo != nil {
   378  		t.Fatal(errGo)
   379  	}
   380  	// Wait for the key to appear a second time in the signatures collection
   381  	func() {
   382  		for {
   383  			select {
   384  			case <-tick.C:
   385  				_, fp, err := sigs.Get(newKey)
   386  				if err != nil {
   387  					continue
   388  				}
   389  				if diff := deep.Equal(expectedFingerprint, fp); diff != nil {
   390  					t.Fatal(diff)
   391  				}
   392  				return
   393  			}
   394  		}
   395  	}()
   396  
   397  	// Purge the data we used from the signatures
   398  	delete(signatures.Data, newKey)
   399  	if errGo := client.Update(context.Background(), signatures); errGo != nil {
   400  		t.Fatal(errGo)
   401  	}
   402  }