[recipe_wrapper] Actually invoke attestation_tool

This change adds actually invoking the attestation_tool. The test runs using
the `attestation_tool` when it can, or drops back to the fake when it can't.
Because the output of the tool is non-deterministic at runtime due to the fact
the tool appears to use clock time as a seed, what's tested is the output can
be parsed and look for the values known not to change.

As the attestation_tool on success doesn't actually return any output, the test
was able to be simplified.

I don't love how the I/O here is limited to communicating with files that are
written/read/cleaned up, but I don't think there's anything smarter to be done
here, just isolate the tool as best as possible to allow for idomatic data
passing internally.

The current input to the tool is fake and needs to actually be formulated to
something reasonable with CIPD and such, but that's for later.

Bug: b/297416582
Change-Id: Ied9fcfc8c36ee0bc525216e94397664e938d323c
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/909334
Reviewed-by: Anthony Fandrianto <atyfto@google.com>
Commit-Queue: Chris Lewis <cflewis@google.com>
diff --git a/cmd/recipe_wrapper/bcid/bcid.go b/cmd/recipe_wrapper/bcid/bcid.go
index 6fe58e3..f04fff4 100644
--- a/cmd/recipe_wrapper/bcid/bcid.go
+++ b/cmd/recipe_wrapper/bcid/bcid.go
@@ -5,9 +5,15 @@
 package bcid
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
+	"fmt"
 	"io"
+	"os"
 	"os/exec"
+
+	"go.chromium.org/luci/common/logging"
 )
 
 const ProdKeyID = "projects/fuchsia-bcid/locations/global/keyRings/bcid/cryptoKeys/attestation_signer"
@@ -16,6 +22,8 @@
 
 func (a *attestCommander) CombinedOutput(ctx context.Context, args ...string) ([]byte, error) {
 	cmd := exec.CommandContext(ctx, "attestation_tool", args...)
+
+	logging.Infof(ctx, "Executing %+v\n", cmd)
 	return cmd.CombinedOutput()
 }
 
@@ -28,5 +36,70 @@
 }
 
 func attestWithCommander(ctx context.Context, cmd CombinedOutputer, data io.Reader, keyID string) ([]byte, error) {
-	return cmd.CombinedOutput(ctx, "generate")
+	tempFiles := make(map[string]*os.File)
+	for _, name := range []string{"input", "output"} {
+		f, err := os.CreateTemp("", "bcid")
+		if err != nil {
+			return nil, err
+		}
+		tempFiles[name] = f
+		defer os.Remove(f.Name())
+	}
+	all, err := io.ReadAll(data)
+	if err != nil {
+		return nil, err
+	}
+	tempFiles["input"].Write(all)
+
+	out, err := cmd.CombinedOutput(ctx, "generate",
+		"--input", tempFiles["input"].Name(),
+		"--key_id", keyID,
+		"--output", tempFiles["output"].Name(),
+		"--generate_slsa_provenance")
+	if err != nil {
+		return nil, fmt.Errorf("exit status %v, got out %q", err, out)
+	}
+
+	return io.ReadAll(tempFiles["output"])
+}
+
+type JWT struct {
+	Token string `json:"jwt"`
+}
+
+type Signature struct {
+	KeyID string `json:"keyid"`
+	Sig   string `json:"sig"`
+}
+
+type SLSA struct {
+	ContentType string      `json:"payloadType"`
+	Payload     string      `json:"payload"`
+	Signatures  []Signature `json:"signatures"`
+}
+
+type AuthSLSA struct {
+	JWT  *JWT
+	SLSA *SLSA
+}
+
+func ReadSLSA(data io.Reader) (*AuthSLSA, error) {
+	all, err := io.ReadAll(data)
+	if err != nil {
+		return nil, fmt.Errorf("can't read input data: %v", err)
+	}
+
+	lines := bytes.Split(all, []byte("\n"))
+
+	var jwt JWT
+	if err := json.Unmarshal(lines[0], &jwt); err != nil {
+		return nil, fmt.Errorf("unable to parse JWT: %v", err)
+	}
+
+	var slsa SLSA
+	if err := json.Unmarshal(lines[1], &slsa); err != nil {
+		return nil, fmt.Errorf("unable to pass SLSA payload: %v", err)
+	}
+
+	return &AuthSLSA{JWT: &jwt, SLSA: &slsa}, nil
 }
diff --git a/cmd/recipe_wrapper/bcid/bcid_test.go b/cmd/recipe_wrapper/bcid/bcid_test.go
index f25cd64..6e62f30 100644
--- a/cmd/recipe_wrapper/bcid/bcid_test.go
+++ b/cmd/recipe_wrapper/bcid/bcid_test.go
@@ -11,41 +11,77 @@
 // https://npf.io/2015/06/testing-exec-command/
 
 import (
+	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"os"
 	"os/exec"
 	"strings"
 	"testing"
 
-	"github.com/google/go-cmp/cmp"
+	"go.chromium.org/luci/common/logging/gologger"
 )
 
+const testKeyID = "projects/fuchsia-bcid-dev/locations/global/keyRings/tests/cryptoKeys/recipe_wrapper_attestation_signer/cryptoKeyVersions/1"
+
+// This is read from a test data file because the line length is enormous.
+var fakeSLSA []byte
+
+// TODO(b/298073152): Make this test data look like what is intended for Fuchsia.
+const fakeBCIDConfig = `{
+  "type": "//bcid.corp.google.com/attestations/core-provenance/v1",
+  "subject": {
+    "sha256": "2e290049efd49402c246e07667350c5907f17e85a0bd314312f2df83a601354a"
+  },
+  "payload": {
+    "builder": {
+      "id": "//bcid.corp.google.com/builders/bcid-test"
+    },
+    "top_level_source": {
+      "git_repo": {
+        "uri": "https://chromium.googlesource.com/chromium/src.git",
+        "branch": "refs/heads/main",
+        "commit": "mda39a3ee5e6b4b0d3255bfef95601890afd80709"
+      }
+    },
+    "build_entry_point": {
+      "type": "//bcid.corp.google.com/build_entry_point/luci/v1",
+      "value": "official/release/diffs_mac"
+    },
+    "source_complete": false
+  }
+}`
+
+func ctx() context.Context {
+	ctx := context.Background()
+
+	if testing.Verbose() {
+		return gologger.StdConfig.Use(ctx)
+	}
+	return ctx
+}
+
 type testAttestCommander struct{}
 
-const generateOutput = `error in cmd validation: input filename is required
-
-synopsis: generate a BCID provenance attestation (output) using the provided input, kms type, and key path.
-usage: generate --input=<input_filename:required>
-	--output=<output_filename:required>
-	--key_id=<key_id:required>
-	--artifact_dir=<artifact_dir:optional>
-	--generate_core_provenance=<true|false:optional>
-	--generate_slsa_provenance=<true|false:optional>
-`
-
 func (c *testAttestCommander) CombinedOutput(ctx context.Context, args ...string) ([]byte, error) {
 	if args[0] != "generate" {
 		panic("only generate test is supported")
 	}
 
-	cs := []string{"-test.run=TestAttestOutput", "--"}
-	cs = append(cs, args...)
-	cmd := exec.CommandContext(ctx, os.Args[0], cs...)
-	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1", "GOCOVERDIR=" + os.Getenv("GOCOVERDIR")}
-	out, err := cmd.CombinedOutput()
-	return out, err
+	var filename string
+	for i, arg := range args {
+		if arg == "--output" {
+			filename = args[i+1]
+			break
+		}
+	}
+	if err := os.WriteFile(filename, []byte(fakeSLSA), 0); err != nil {
+		panic(fmt.Sprintf("can't write to %v: %v", filename, err))
+	}
+
+	return nil, nil
 }
 
 // commander chooses a real version of `attestation_tool` if it is available and the test
@@ -67,32 +103,41 @@
 	return &testAttestCommander{}
 }
 
-func TestAttestOutput(t *testing.T) {
-	t.Helper()
-	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
-		return
+func TestAttest(t *testing.T) {
+	ctx := ctx()
+
+	out, err := attestWithCommander(ctx, commander(t, ctx), strings.NewReader(fakeBCIDConfig), testKeyID)
+	if err != nil {
+		t.Fatalf("attestWithCommander() error %v, want nil error", err)
+	}
+	slsa, err := ReadSLSA(bytes.NewReader(out))
+	if err != nil {
+		t.Fatalf("ReadSLSA() error %v, want nil error", err)
 	}
 
-	// dir, err := os.MkdirTemp("", "")
-	// if err != nil {
-	// 	panic(err)
-	// }
-	// defer os.RemoveAll(dir)
-	// // If GOCOVERDIR is not set on the LUCI builder (which executes Go using -cover),
-	// // it complains and stdout is polluted with the error. This then fails out the test.
-	// // Coverage isn't really important here, so just set the envvar to a sink.
-	// os.Setenv("GOCOVERDIR", dir)
-	defer os.Exit(0)
-	fmt.Printf("%v", generateOutput)
+	// A real invocation of the attestation_tool is non-deterministic because it appears
+	// to rely on the clock time as an input.
+	// Instead of relying on the data itself, just check that the data was formatted as
+	// expected. This validates that attestation_tool did work.
+	if slsa.JWT.Token == "" || slsa.SLSA.ContentType != "application/vnd.in-toto+json" ||
+		slsa.SLSA.Signatures[0].KeyID != "gcpkms://"+testKeyID {
+		t.Errorf("SLSA struct did not follow expected format\nJWT = %q\nContentType = %q\nKey ID = %q\n",
+			slsa.JWT.Token, slsa.SLSA.ContentType, slsa.SLSA.Signatures[0].KeyID)
+	}
 }
 
-func TestAttest(t *testing.T) {
-	ctx := context.Background()
-
-	want := strings.TrimSpace(generateOutput)
-	out, _ := attestWithCommander(ctx, commander(t, ctx), strings.NewReader("foo"), "testKey")
-	trimOut := strings.TrimSpace(string(out))
-	if !cmp.Equal(trimOut, want) {
-		t.Errorf("got %q, want %q", trimOut, want)
+func TestMain(m *testing.M) {
+	f, err := os.Open("testdata/slsa.txt")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "unable to open slsa testdata: %v", err)
+		os.Exit(1)
 	}
+	slsa, err := io.ReadAll(f)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "unable to read slsa testdata: %v", err)
+		os.Exit(1)
+	}
+	fakeSLSA = slsa
+
+	os.Exit(m.Run())
 }
diff --git a/cmd/recipe_wrapper/bcid/testdata/slsa.txt b/cmd/recipe_wrapper/bcid/testdata/slsa.txt
new file mode 100644
index 0000000..5a0e3da
--- /dev/null
+++ b/cmd/recipe_wrapper/bcid/testdata/slsa.txt
@@ -0,0 +1,2 @@
+{"jwt":"eyJ0eXAiOiJqd3QiLCAiYWxnIjoiRVMyNTYiLCAia2lkIjoiZ2Nwa21zOi8vcHJvamVjdHMvZnVjaHNpYS1iY2lkLWRldi9sb2NhdGlvbnMvZ2xvYmFsL2tleVJpbmdzL3Rlc3RzL2NyeXB0b0tleXMvcmVjaXBlX3dyYXBwZXJfYXR0ZXN0YXRpb25fc2lnbmVyL2NyeXB0b0tleVZlcnNpb25zLzEifQ.eyJhdWQiOiIvL2JpbmFyeWF1dGhvcml6YXRpb24uZ29vZ2xlYXBpcy5jb20vQXR0ZXN0YXRpb24vdjEiLCAiaWF0IjoxNjkzMzQwNjEwLCAiZXhwIjoxNzA4ODkyNjEwLCAibmJmIjoxNjkzMzQwNjEwLCAiYXR0ZXN0ZWRDbGFpbSI6eyJ0eXBlIjoiLy9iY2lkLmNvcnAuZ29vZ2xlLmNvbS9hdHRlc3RhdGlvbnMvY29yZS1wcm92ZW5hbmNlL3YxIiwgInN1YmplY3QiOnsic2hhLTI1NiI6IjJlMjkwMDQ5ZWZkNDk0MDJjMjQ2ZTA3NjY3MzUwYzU5MDdmMTdlODVhMGJkMzE0MzEyZjJkZjgzYTYwMTM1NGEifSwgInBheWxvYWQiOnsiYnVpbGRlciI6eyJpZCI6Ii8vYmNpZC5jb3JwLmdvb2dsZS5jb20vYnVpbGRlcnMvYmNpZC10ZXN0In0sICJ0b3BMZXZlbFNvdXJjZSI6eyJnaXRfcmVwbyI6eyJ1cmkiOiJodHRwczovL2Nocm9taXVtLmdvb2dsZXNvdXJjZS5jb20vY2hyb21pdW0vc3JjLmdpdCIsICJicmFuY2giOiJyZWZzL2hlYWRzL21hc3RlciIsICJjb21taXQiOiJtZGEzOWEzZWU1ZTZiNGIwZDMyNTViZmVmOTU2MDE4OTBhZmQ4MDcwOSJ9fSwgImJ1aWxkRW50cnlQb2ludCI6eyJ0eXBlIjoiLy9iY2lkLmNvcnAuZ29vZ2xlLmNvbS9idWlsZF9lbnRyeV9wb2ludC9sdWNpL3YxIiwgInZhbHVlIjoib2ZmaWNpYWwvcmVsZWFzZS9kaWZmc19tYWMifX19fQ.xM7i1yT7cUBUvfwKfR7xY4PA62LXZGFc6DmuCvrXXKYXuRpupEqinGUa66Bq-AvtH1RP2bp_Ovyljbx7o3bK7g"}
+{"payloadType":"application/vnd.in-toto+json", "payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsICJzdWJqZWN0IjpbeyJuYW1lIjoiXyIsICJkaWdlc3QiOnsic2hhMjU2IjoiMmUyOTAwNDllZmQ0OTQwMmMyNDZlMDc2NjczNTBjNTkwN2YxN2U4NWEwYmQzMTQzMTJmMmRmODNhNjAxMzU0YSJ9fV0sICJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YwLjIiLCAicHJlZGljYXRlIjp7ImJ1aWxkVHlwZSI6Ii8vYmNpZC5jb3JwLmdvb2dsZS5jb20vYnVpbGRfZW50cnlfcG9pbnQvbHVjaS92MSIsICJidWlsZGVyIjp7ImlkIjoiLy9iY2lkLmNvcnAuZ29vZ2xlLmNvbS9idWlsZGVycy9iY2lkLXRlc3QifSwgImludm9jYXRpb24iOnsiY29uZmlnU291cmNlIjp7ImRpZ2VzdCI6eyJzaGExIjoibWRhMzlhM2VlNWU2YjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkODA3MDkifSwgImVudHJ5UG9pbnQiOiJvZmZpY2lhbC9yZWxlYXNlL2RpZmZzX21hYyIsICJ1cmkiOiJnaXQraHR0cHM6Ly9jaHJvbWl1bS5nb29nbGVzb3VyY2UuY29tL2Nocm9taXVtL3NyYy5naXRAcmVmcy9oZWFkcy9tYXN0ZXIifX0sICJtZXRhZGF0YSI6eyJjb21wbGV0ZW5lc3MiOnt9fX19", "signatures":[{"keyid":"gcpkms://projects/fuchsia-bcid-dev/locations/global/keyRings/tests/cryptoKeys/recipe_wrapper_attestation_signer/cryptoKeyVersions/1", "sig":"MEUCIQDgSW25cBl4zo4QvPvJvwSfQD1u6ocNmdN8E5YfwiwbmQIgUS784Tw2VActIcFAMc60y0Kv1ORSNpV3quPS0qAHEWQ="}]}
\ No newline at end of file