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 }