diff --git a/go.mod b/go.mod index 2063f00..db07b9e 100644 --- a/go.mod +++ b/go.mod @@ -15,19 +15,23 @@ require ( ) require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect github.com/cloudflare/circl v1.5.0 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/zalando/go-keyring v0.2.6 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect golang.org/x/net v0.34.0 // indirect diff --git a/go.sum b/go.sum index ae648ef..cecdaef 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -12,6 +14,8 @@ github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+py github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,6 +39,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -70,6 +76,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= diff --git a/internal/keyring/backend/upstream.go b/internal/keyring/backend/upstream.go new file mode 100644 index 0000000..b53758b --- /dev/null +++ b/internal/keyring/backend/upstream.go @@ -0,0 +1,22 @@ +package backend + +import "github.com/zalando/go-keyring" + +func New() *backend { + return &backend{} +} + +type backend struct { +} + +func (backend) Get(service, user string) (string, error) { + return keyring.Get(service, user) +} + +func (backend) Set(service, user, value string) error { + return keyring.Set(service, user, value) +} + +func (backend) Delete(service, user string) error { + return keyring.Delete(service, user) +} diff --git a/internal/keyring/keyring.go b/internal/keyring/keyring.go new file mode 100644 index 0000000..c5aa395 --- /dev/null +++ b/internal/keyring/keyring.go @@ -0,0 +1,85 @@ +package keyring + +import ( + "errors" + "fmt" + "os/user" +) + +var ErrBackEndCannotBeNil = errors.New("backend cannot be nil") + +// New returns a keyring frontend to manage qubesome secrets. +func New(profile string, backend Backend) *frontend { + return &frontend{ + profile, + backend, + } +} + +type Backend interface { + // Set stores the value in a keyring service for user. + Set(service, user, password string) error + // Get returns the value stored in a keyring service for user. + Get(service, user string) (string, error) + // Delete removes any stored values in a keyring service. + Delete(service, user string) error +} + +type frontend struct { + profile string + backend Backend +} + +func (k *frontend) Get(name SecretName) (string, error) { + if k.backend == nil { + return "", ErrBackEndCannotBeNil + } + + u, err := user.Current() + if err != nil { + return "", err + } + + svc := fmt.Sprintf("qubesome:%s:%s", k.profile, name) + val, err := k.backend.Get(svc, u.Name) + if err != nil { + return "", fmt.Errorf("cannot get value for %q: %w", svc, err) + } + return val, nil +} + +func (k *frontend) Set(name SecretName, value string) error { + if k.backend == nil { + return ErrBackEndCannotBeNil + } + + u, err := user.Current() + if err != nil { + return err + } + + svc := fmt.Sprintf("qubesome:%s:%s", k.profile, name) + err = k.backend.Set(svc, u.Name, value) + if err != nil { + return fmt.Errorf("cannot set value for %q: %w", svc, err) + } + return nil +} + +func (k *frontend) Delete(name SecretName) error { + if k.backend == nil { + return ErrBackEndCannotBeNil + } + + u, err := user.Current() + if err != nil { + return err + } + + svc := fmt.Sprintf("qubesome:%s:%s", k.profile, name) + err = k.backend.Delete(svc, u.Name) + if err != nil { + return fmt.Errorf("cannot delete value for %q: %w", svc, err) + } + return nil +} diff --git a/internal/keyring/keyring_test.go b/internal/keyring/keyring_test.go new file mode 100644 index 0000000..4b453ea --- /dev/null +++ b/internal/keyring/keyring_test.go @@ -0,0 +1,106 @@ +package keyring_test + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "testing" + + "github.com/qubesome/cli/internal/keyring" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetSet(t *testing.T) { + tests := []struct { + name string + profile string + data map[string]string + key keyring.SecretName + want string + wantErr bool + }{ + { + name: "existing key", + profile: "foo", + data: map[string]string{ + "qubesome:foo:mtls-ca": "bar", + }, + key: keyring.MtlsCA, + want: "bar", + }, + { + name: "other profiles and keys", + profile: "bar", + data: map[string]string{ + "qubesome:bar:mtls-client-key": "foo", + }, + key: keyring.MtlsClientKey, + want: "foo", + }, + { + name: "key not found", + profile: "foo", + data: map[string]string{ + "qubesome:bar:mtls-ca": "bar", + }, + key: keyring.MtlsCA, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ks := keyring.New(tc.profile, newBackend(tc.data)) + + got, err := ks.Get(tc.key) + if tc.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.want, got) + } + + newValue := randValue() + err = ks.Set(tc.key, newValue) + require.NoError(t, err) + + val, err := ks.Get(tc.key) + require.NoError(t, err) + assert.Equal(t, newValue, val) + }) + } +} + +func randValue() string { + d := make([]byte, 16) + if _, err := rand.Read(d); err != nil { + panic(err) + } + return base64.RawURLEncoding.EncodeToString(d) +} + +func newBackend(data map[string]string) *mockBackend { + return &mockBackend{data} +} + +type mockBackend struct { + data map[string]string +} + +func (m *mockBackend) Get(service, user string) (string, error) { + if val, ok := m.data[service]; ok { + return val, nil + } + return "", fmt.Errorf("not found") +} + +func (m *mockBackend) Set(service, user, value string) error { + m.data[service] = value + return nil +} + +func (m *mockBackend) Delete(service, user string) error { + delete(m.data, service) + return nil +} diff --git a/internal/keyring/values.go b/internal/keyring/values.go new file mode 100644 index 0000000..1325cf7 --- /dev/null +++ b/internal/keyring/values.go @@ -0,0 +1,9 @@ +package keyring + +type SecretName string + +const ( + MtlsCA SecretName = "mtls-ca" + MtlsClientCert SecretName = "mtls-client-cert" + MtlsClientKey SecretName = "mtls-client-key" +) diff --git a/internal/profiles/profiles.go b/internal/profiles/profiles.go index f2b9c12..fd1307f 100644 --- a/internal/profiles/profiles.go +++ b/internal/profiles/profiles.go @@ -18,6 +18,8 @@ import ( "github.com/qubesome/cli/internal/command" "github.com/qubesome/cli/internal/files" "github.com/qubesome/cli/internal/images" + "github.com/qubesome/cli/internal/keyring" + "github.com/qubesome/cli/internal/keyring/backend" "github.com/qubesome/cli/internal/runners/util/container" "github.com/qubesome/cli/internal/types" "github.com/qubesome/cli/internal/util/dbus" @@ -293,6 +295,13 @@ func Start(runner string, profile *types.Profile, cfg *types.Config) (err error) } }() + defer func() { + err := deleteMtlsData(profile.Name) + if err != nil { + slog.Warn("failed to delete mTLS data", "error", err) + } + }() + err = createNewDisplay(binary, creds.CA, creds.ClientPEM, creds.ClientKeyPEM, profile, strconv.Itoa(int(profile.Display))) @@ -581,6 +590,11 @@ func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile, cmd.Env = append(cmd.Env, "Q_MTLS_CERT="+string(cert)) cmd.Env = append(cmd.Env, "Q_MTLS_KEY="+string(key)) + err = storeMtlsData(profile.Name, string(ca), string(cert), string(key)) + if err != nil { + return err + } + output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("%s: %w", output, err) @@ -588,6 +602,36 @@ func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile, return nil } +func storeMtlsData(profile, ca, cert, key string) error { + ks := keyring.New(profile, backend.New()) + if err := ks.Set(keyring.MtlsCA, ca); err != nil { + return err + } + + if err := ks.Set(keyring.MtlsClientCert, cert); err != nil { + return err + } + + if err := ks.Set(keyring.MtlsClientKey, key); err != nil { + return err + } + return nil +} + +func deleteMtlsData(profile string) error { + ks := keyring.New(profile, backend.New()) + if err := ks.Delete(keyring.MtlsCA); err != nil { + return err + } + if err := ks.Delete(keyring.MtlsClientCert); err != nil { + return err + } + if err := ks.Delete(keyring.MtlsClientKey); err != nil { + return err + } + return nil +} + func grabberShortcut() string { if strings.EqualFold(os.Getenv("XDG_SESSION_TYPE"), "wayland") { return " + " diff --git a/internal/runners/docker/run.go b/internal/runners/docker/run.go index 75985b0..9cd8911 100644 --- a/internal/runners/docker/run.go +++ b/internal/runners/docker/run.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/qubesome/cli/internal/files" + "github.com/qubesome/cli/internal/keyring" + "github.com/qubesome/cli/internal/keyring/backend" "github.com/qubesome/cli/internal/runners/util/container" "github.com/qubesome/cli/internal/runners/util/mime" "github.com/qubesome/cli/internal/runners/util/usb" @@ -194,6 +196,9 @@ func Run(ew types.EffectiveWorkload) error { // Mount qube socket so that it can send commands from container to host. args = append(args, fmt.Sprintf("-v=%s:/tmp/qube.sock:ro", socket)) + args = append(args, "-e=Q_MTLS_CA") + args = append(args, "-e=Q_MTLS_CERT") + args = append(args, "-e=Q_MTLS_KEY") } if ew.Profile.DNS != "" { @@ -247,6 +252,33 @@ func Run(ew types.EffectiveWorkload) error { slog.Debug(fmt.Sprintf("exec: %s", runnerBinary), "args", args) cmd := execabs.Command(runnerBinary, args...) + if ew.Workload.HostAccess.Mime { + // Since the implementation of mTLS, workloads granted mime handling + // need the mTLS creds so that they can communicate with the inception + // server. + ks := keyring.New(ew.Profile.Name, backend.New()) + ca, err := ks.Get(keyring.MtlsCA) + if err != nil { + return err + } + + cert, err := ks.Get(keyring.MtlsClientCert) + if err != nil { + return err + } + + key, err := ks.Get(keyring.MtlsClientKey) + if err != nil { + return err + } + + slog.Debug("enabling mime access") + + cmd.Env = append(os.Environ(), "Q_MTLS_CA="+ca) + cmd.Env = append(cmd.Env, "Q_MTLS_CERT="+cert) + cmd.Env = append(cmd.Env, "Q_MTLS_KEY="+key) + } + cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/internal/runners/podman/run.go b/internal/runners/podman/run.go index c4c3b25..46acacf 100644 --- a/internal/runners/podman/run.go +++ b/internal/runners/podman/run.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/qubesome/cli/internal/files" + "github.com/qubesome/cli/internal/keyring" + "github.com/qubesome/cli/internal/keyring/backend" "github.com/qubesome/cli/internal/runners/util/container" "github.com/qubesome/cli/internal/runners/util/mime" "github.com/qubesome/cli/internal/runners/util/usb" @@ -197,6 +199,9 @@ func Run(ew types.EffectiveWorkload) error { // Mount qube socket so that it can send commands from container to host. args = append(args, fmt.Sprintf("-v=%s:/tmp/qube.sock:ro", socket)) + args = append(args, "-e=Q_MTLS_CA") + args = append(args, "-e=Q_MTLS_CERT") + args = append(args, "-e=Q_MTLS_KEY") } if ew.Profile.DNS != "" { @@ -250,6 +255,33 @@ func Run(ew types.EffectiveWorkload) error { slog.Debug(fmt.Sprintf("exec: %s", runnerBinary), "args", args) cmd := execabs.Command(runnerBinary, args...) + if ew.Workload.HostAccess.Mime { + // Since the implementation of mTLS, workloads granted mime handling + // need the mTLS creds so that they can communicate with the inception + // server. + ks := keyring.New(ew.Profile.Name, backend.New()) + ca, err := ks.Get(keyring.MtlsCA) + if err != nil { + return err + } + + cert, err := ks.Get(keyring.MtlsClientCert) + if err != nil { + return err + } + + key, err := ks.Get(keyring.MtlsClientKey) + if err != nil { + return err + } + + slog.Debug("enabling mime access") + + cmd.Env = append(os.Environ(), "Q_MTLS_CA="+ca) + cmd.Env = append(cmd.Env, "Q_MTLS_CERT="+cert) + cmd.Env = append(cmd.Env, "Q_MTLS_KEY="+key) + } + cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout