PAM lines written into login and passwd as blocks, so login unlocks the keyring on both workstations; no daemon of its own; gcr's ssh agent named for the session until the environment can say a runtime-directory path. Go tools unlocked, lock, collections and ssh-keys, never reading a secret.
132 lines
4.9 KiB
Go
132 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const nobody = 4194400
|
|
|
|
// secretService fakes busctl answering as gnome-keyring did on 2026-10-04: a session, a login and a
|
|
// default keyring, the login one unlocked; Lock locks it.
|
|
func secretService(t *testing.T) string {
|
|
t.Helper()
|
|
fakeMachine(t)
|
|
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
|
|
if err := os.MkdirAll(runtime, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(runtime, "bus"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := fakeBinaries(t, map[string]string{"busctl": `echo "$*" >> "$LOG"
|
|
locked=false; [ -f "$LOG.locked" ] && locked=true
|
|
case "$*" in
|
|
*"get-property org.freedesktop.secrets /org/freedesktop/secrets org.freedesktop.Secret.Service Collections")
|
|
echo '{"type":"ao","data":["/org/freedesktop/secrets/collection/session","/org/freedesktop/secrets/collection/login","/org/freedesktop/secrets/collection/Default_5fkeyring"]}' ;;
|
|
*"ReadAlias s default") echo '{"type":"o","data":["/org/freedesktop/secrets/collection/Default_5fkeyring"]}' ;;
|
|
*"/collection/login org.freedesktop.DBus.Properties GetAll"*)
|
|
echo '{"type":"a{sv}","data":[{"Items":{"type":"ao","data":["/x/1","/x/2"]},"Label":{"type":"s","data":"Login"},"Locked":{"type":"b","data":'$locked'},"Created":{"type":"t","data":1752488320},"Modified":{"type":"t","data":0}}]}' ;;
|
|
*"/collection/"*"GetAll"*)
|
|
echo '{"type":"a{sv}","data":[{"Items":{"type":"ao","data":[]},"Label":{"type":"s","data":"Other"},"Locked":{"type":"b","data":true},"Created":{"type":"t","data":0},"Modified":{"type":"t","data":0}}]}' ;;
|
|
*"Lock ao 1 /org/freedesktop/secrets/collection/login") touch "$LOG.locked"; echo '{"type":"aoo","data":[["/org/freedesktop/secrets/collection/login"],"/"]}' ;;
|
|
*) echo "no such call: $*" >&2; exit 1 ;;
|
|
esac`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
return bin
|
|
}
|
|
|
|
func TestCollectionsAreNamesCountsAndLocksNeverItems(t *testing.T) {
|
|
bin := secretService(t)
|
|
got, err := Collections()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Collections) != 3 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
var login, def Collection
|
|
for _, c := range got.Collections {
|
|
switch c.ID {
|
|
case "login":
|
|
login = c
|
|
case "Default_5fkeyring":
|
|
def = c
|
|
}
|
|
}
|
|
if login.Label != "Login" || login.Locked || login.Items != 2 || login.Created == "" || login.Modified != "" || login.Default {
|
|
t.Fatalf("login: %+v", login)
|
|
}
|
|
if !def.Default || !def.Locked {
|
|
t.Fatalf("default: %+v", def)
|
|
}
|
|
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
if strings.Contains(string(asked), "GetSecret") || strings.Contains(string(asked), "Item") && strings.Contains(string(asked), "Secret.Item") {
|
|
t.Fatalf("a secret was asked for:\n%s", asked)
|
|
}
|
|
}
|
|
|
|
func TestUnlockedAnswersTheLoginAndTheDefaultKeyring(t *testing.T) {
|
|
secretService(t)
|
|
fakeProcess(t, nobody, "gnome-keyring-d")
|
|
got, err := Unlocked()
|
|
if err != nil || !got.Daemon || got.Login == nil || got.Login.Locked || got.Default == nil || !got.Default.Default {
|
|
t.Fatalf("%+v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestLockLocksTheLoginKeyringAndRefusesAPathForAnId(t *testing.T) {
|
|
secretService(t)
|
|
got, err := Lock("")
|
|
if err != nil || got.Collection != "login" || !got.Locked {
|
|
t.Fatalf("%+v, %v", got, err)
|
|
}
|
|
for _, bad := range []string{"../service", "login /org/x", "a b"} {
|
|
if _, err := Lock(bad); err == nil {
|
|
t.Errorf("%q was accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheAgentsKeysAreFingerprints(t *testing.T) {
|
|
keys := parseKeys("256 SHA256:x+LmFabc op@laptop (ED25519)\n3072 SHA256:yyy a comment with spaces (RSA)\nThe agent has no identities.\n")
|
|
if len(keys) != 2 || keys[0] != (Key{Bits: 256, Fingerprint: "SHA256:x+LmFabc", Comment: "op@laptop", Type: "ED25519"}) ||
|
|
keys[1].Comment != "a comment with spaces" || keys[1].Type != "RSA" {
|
|
t.Fatalf("%+v", keys)
|
|
}
|
|
}
|
|
|
|
func TestSSHKeysAsksGcrsAgentAndSaysWhenItDoesNotAnswer(t *testing.T) {
|
|
secretService(t)
|
|
bin := fakeBinaries(t, map[string]string{"ssh-add": `echo "$SSH_AUTH_SOCK $*" > "$LOG.ssh"; [ -f "$NOAGENT" ] && { echo "Could not open a connection" >&2; exit 2; }; echo "256 SHA256:abc op (ED25519)"`})
|
|
t.Setenv("NOAGENT", filepath.Join(bin, "noagent"))
|
|
got, err := SSHKeys()
|
|
if err != nil || len(got.Keys) != 1 || !strings.HasSuffix(got.Agent, "/gcr/ssh") {
|
|
t.Fatalf("%+v, %v", got, err)
|
|
}
|
|
asked, _ := os.ReadFile(os.Getenv("LOG") + ".ssh")
|
|
if !strings.HasSuffix(strings.TrimSpace(string(asked)), "/gcr/ssh -l -E sha256") {
|
|
t.Fatalf("asked: %s", asked)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(bin, "noagent"), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := SSHKeys(); err == nil || !strings.Contains(err.Error(), "gcr-ssh-agent.socket") {
|
|
t.Fatalf("no agent: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWithoutABusTheToolsSaySo(t *testing.T) {
|
|
fakeMachine(t)
|
|
if _, err := Collections(); !errors.Is(err, ErrNoBus) {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := SSHKeys(); !errors.Is(err, ErrNoBus) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|