168 lines
5.2 KiB
Go
168 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The managed install signs the agent's manifest in Go (faSignManifest): the
|
|
// agent prints `payload`, the bastion signs it with the key it minted, the
|
|
// agent records it with `sign --value`. No openssl anywhere — Windows ships
|
|
// none, and every Windows agent used to stop at `missing-openssl`.
|
|
|
|
const fakeAgent = `import base64, sys
|
|
args = sys.argv[1:]
|
|
if args[0] == "payload":
|
|
sys.stdout.buffer.write(open(args[1], "rb").read())
|
|
elif args[0] == "sign":
|
|
path = args[1]
|
|
val = args[args.index("--value") + 1]
|
|
cert = args[args.index("--cert") + 1]
|
|
base64.b64decode(val, validate=True)
|
|
open(path + ".sig", "w").write(val + "\n" + cert)
|
|
else:
|
|
sys.exit(2)
|
|
`
|
|
|
|
func findPython(t *testing.T) string {
|
|
for _, n := range []string{"python3", "python"} {
|
|
if p, err := exec.LookPath(n); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
t.Skip("no python on PATH")
|
|
return ""
|
|
}
|
|
|
|
// pythonOnlyPath: a PATH holding a link to python and NOTHING else, so a step
|
|
// that reached for openssl would fail here the way it does on Windows.
|
|
func pythonOnlyPath(t *testing.T, py string) string {
|
|
d := t.TempDir()
|
|
if err := os.Symlink(py, filepath.Join(d, filepath.Base(py))); err != nil {
|
|
t.Skip("cannot link python: " + err.Error())
|
|
}
|
|
if _, err := exec.LookPath("openssl"); err == nil {
|
|
old := os.Getenv("PATH")
|
|
os.Setenv("PATH", d)
|
|
defer os.Setenv("PATH", old)
|
|
if _, err := exec.LookPath("openssl"); err == nil {
|
|
t.Fatal("openssl still resolvable on the python-only PATH")
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestFaSignManifestSignsPayloadWithMintedKey(t *testing.T) {
|
|
py := findPython(t)
|
|
dir := t.TempDir()
|
|
cert, key, _, err := faEnsureIdentity(dir, "desk-example")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
agent := filepath.Join(dir, "agent.py")
|
|
if err := os.WriteFile(agent, []byte(fakeAgent), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
staged := filepath.Join(dir, "manifest.yaml")
|
|
body := []byte("freeagent: 1\nid: t\n")
|
|
if err := os.WriteFile(staged, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// PATH without openssl: the step must not need it.
|
|
env := []string{"PATH=" + pythonOnlyPath(t, py)}
|
|
if err := faSignManifest(py, nil, agent, staged, key, cert, env); err != nil {
|
|
t.Fatalf("faSignManifest: %v", err)
|
|
}
|
|
rec, err := os.ReadFile(staged + ".sig")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parts := strings.SplitN(string(rec), "\n", 2)
|
|
if parts[1] != cert {
|
|
t.Fatalf("recorded cert %q, want %q", parts[1], cert)
|
|
}
|
|
sig, err := base64.StdEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cb, _ := os.ReadFile(cert)
|
|
blk, _ := pem.Decode(cb)
|
|
c, err := x509.ParseCertificate(blk.Bytes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(body)
|
|
if !ecdsa.VerifyASN1(c.PublicKey.(*ecdsa.PublicKey), sum[:], sig) {
|
|
t.Fatal("signature does not verify against the minted cert over the payload")
|
|
}
|
|
}
|
|
|
|
func TestFaSignManifestReportsAgentFailure(t *testing.T) {
|
|
py := findPython(t)
|
|
dir := t.TempDir()
|
|
cert, key, _, err := faEnsureIdentity(dir, "desk-example")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
agent := filepath.Join(dir, "agent.py")
|
|
_ = os.WriteFile(agent, []byte("import sys\nsys.stderr.write('bad manifest')\nsys.exit(1)\n"), 0o600)
|
|
err = faSignManifest(py, nil, agent, filepath.Join(dir, "m.yaml"), key, cert, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "bad manifest") {
|
|
t.Fatalf("want the agent's own error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// With FA_TEST_AGENT_PYZ pointing at a built agent (bastion_agent/build.py),
|
|
// the real `payload` / `sign --value` / gate round-trip runs, openssl-free.
|
|
func TestFaSignManifestWithRealAgent(t *testing.T) {
|
|
pyz := os.Getenv("FA_TEST_AGENT_PYZ")
|
|
if pyz == "" {
|
|
t.Skip("FA_TEST_AGENT_PYZ not set")
|
|
}
|
|
py := findPython(t)
|
|
dir := t.TempDir()
|
|
cert, key, _, err := faEnsureIdentity(dir, "desk-example")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root := filepath.Join(dir, "root")
|
|
_ = os.MkdirAll(root, 0o700)
|
|
staged := filepath.Join(dir, "manifest.yaml")
|
|
text := "freeagent: 1\nid: t\nowner:\n host: desk-example\nscope:\n roots:\n - " + root +
|
|
"\nmask:\n- read\n- index\nstages:\n- walk\nsweep: manual\nsink:\n catalog: local\n vectors: local\n plaintext: never\n"
|
|
_ = os.WriteFile(staged, []byte(text), 0o600)
|
|
env := []string{"PATH=" + pythonOnlyPath(t, py), "FREEAGENT_CA_ROOT=" + cert}
|
|
if err := faSignManifest(py, nil, pyz, staged, key, cert, env); err != nil {
|
|
t.Fatalf("faSignManifest: %v", err)
|
|
}
|
|
cmd := exec.Command(py, pyz, "sweep", staged, "--out", filepath.Join(dir, "out"), "--once")
|
|
cmd.Env = env
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("the real agent's gate refused the Go-signed manifest: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestFaVersionLess(t *testing.T) {
|
|
for _, c := range []struct {
|
|
a, b string
|
|
less bool
|
|
}{
|
|
{"0.1.10", "0.1.11", true}, {"0.1.9", "0.1.11", true}, {"0.1.11", "0.1.11", false},
|
|
{"0.1.12", "0.1.11", false}, {"0.2", "0.1.11", false}, {"1.0.0", "0.1.11", false},
|
|
{"", "0.1.11", true}, {"dev", "0.1.11", true},
|
|
} {
|
|
if got := faVersionLess(c.a, c.b); got != c.less {
|
|
t.Errorf("faVersionLess(%q, %q) = %v, want %v", c.a, c.b, got, c.less)
|
|
}
|
|
}
|
|
}
|