This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.pinentry-rofi, 5pkgs.ssh-tpm-agent: add rofi pinentry with SSH context

Add a rofi-based pinentry for TPM SSH authentication that displays
the SSH command being authenticated (destination, flags, arguments).

- Patch ssh-tpm-agent to pass client PID via SSH_CLIENT_PID env var
using SO_PEERCRED on the Unix socket
- New pinentry-rofi tool reads /proc/<pid>/cmdline to show SSH args
- Update tpm2.nix to use the patched agent and new pinentry

+423 -13
+10 -13
2configs/tpm2.nix
··· 1 - { pkgs, lib, ... }: 1 + { 2 + pkgs, 3 + lib, 4 + self, 5 + ... 6 + }: 2 7 let 3 - pinentry-askpass = pkgs.writeShellScriptBin "pinentry-askpass" '' 4 - result=$(cat <<EOF | ${pkgs.pinentry-gtk2}/bin/pinentry 5 - SETDESC $1 6 - SETPROMPT Passphrase: 7 - GETPIN 8 - EOF 9 - ) 10 - echo "$result" | grep '^D ' | cut -d' ' -f2- 11 - ''; 8 + ssh-tpm-agent = self.packages.${pkgs.system}.ssh-tpm-agent; 9 + pinentry-rofi = self.packages.${pkgs.system}.pinentry-rofi; 12 10 in 13 11 { 14 12 security.tpm2 = { ··· 18 16 }; 19 17 environment.systemPackages = [ 20 18 pkgs.keyutils 21 - pinentry-askpass 22 19 ]; 23 20 users.users.mainUser.extraGroups = [ "tss" ]; 24 21 ··· 27 24 wantedBy = [ "graphical-session.target" ]; 28 25 after = [ "graphical-session.target" ]; 29 26 serviceConfig = { 30 - ExecStart = "${pkgs.ssh-tpm-agent}/bin/ssh-tpm-agent -l %t/ssh-tpm-agent.sock --no-cache"; 27 + ExecStart = "${ssh-tpm-agent}/bin/ssh-tpm-agent -l %t/ssh-tpm-agent.sock --no-cache"; 31 28 Environment = [ 32 - "SSH_ASKPASS=${lib.getExe pinentry-askpass}" 29 + "SSH_ASKPASS=${lib.getExe pinentry-rofi}" 33 30 "SSH_ASKPASS_REQUIRE=force" 34 31 ]; 35 32 Restart = "on-failure";
+312
5pkgs/ssh-tpm-agent/client-pid.patch
··· 1 + diff --git a/agent/agent.go b/agent/agent.go 2 + index 76a2610..e643575 100644 3 + --- a/agent/agent.go 4 + +++ b/agent/agent.go 5 + @@ -25,6 +25,7 @@ import ( 6 + "github.com/google/go-tpm/tpm2/transport" 7 + "golang.org/x/crypto/ssh" 8 + "golang.org/x/crypto/ssh/agent" 9 + + "golang.org/x/sys/unix" 10 + "golang.org/x/text/cases" 11 + "golang.org/x/text/language" 12 + ) 13 + @@ -37,17 +38,18 @@ var ( 14 + var SSH_TPM_AGENT_ADD = "tpm-add-key" 15 + 16 + type Agent struct { 17 + - mu sync.Mutex 18 + - tpm func() transport.TPMCloser 19 + - op func() ([]byte, error) 20 + - pin func(key.SSHTPMKeys) ([]byte, error) 21 + - listener *net.UnixListener 22 + - quit chan interface{} 23 + - wg sync.WaitGroup 24 + - keyring func() *keyring.ThreadKeyring 25 + - keys []key.SSHTPMKeys 26 + - hierkeys []*key.HierSSHTPMKey 27 + - agents []agent.ExtendedAgent 28 + + mu sync.Mutex 29 + + tpm func() transport.TPMCloser 30 + + op func() ([]byte, error) 31 + + pin func(key.SSHTPMKeys, int) ([]byte, error) 32 + + listener *net.UnixListener 33 + + quit chan interface{} 34 + + wg sync.WaitGroup 35 + + keyring func() *keyring.ThreadKeyring 36 + + keys []key.SSHTPMKeys 37 + + hierkeys []*key.HierSSHTPMKey 38 + + agents []agent.ExtendedAgent 39 + + clientPID int 40 + } 41 + 42 + var _ agent.ExtendedAgent = &Agent{} 43 + @@ -118,8 +120,8 @@ func (a *Agent) signers() ([]ssh.Signer, error) { 44 + s, err := ssh.NewSignerFromSigner(k.Signer( 45 + a.keyring(), a.op, a.tpm, 46 + func(_ *keyfile.TPMKey) ([]byte, error) { 47 + - // Shimming the function to get the correct type 48 + - return a.pin(k) 49 + + // Shimming the function to get the correct type, pass client PID 50 + + return a.pin(k, a.clientPID) 51 + }), 52 + ) 53 + if err != nil { 54 + @@ -225,6 +227,21 @@ func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { 55 + } 56 + 57 + func (a *Agent) serveConn(c net.Conn) { 58 + + // Extract peer credentials to get the client PID 59 + + if uc, ok := c.(*net.UnixConn); ok { 60 + + rawConn, err := uc.SyscallConn() 61 + + if err == nil { 62 + + rawConn.Control(func(fd uintptr) { 63 + + if cred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED); err == nil { 64 + + a.mu.Lock() 65 + + a.clientPID = int(cred.Pid) 66 + + a.mu.Unlock() 67 + + slog.Debug("client connected", slog.Int("pid", int(cred.Pid))) 68 + + } 69 + + }) 70 + + } 71 + + } 72 + + 73 + if err := agent.ServeAgent(a, c); err != io.EOF { 74 + slog.Info("Agent client connection ended unsuccessfully", slog.String("error", err.Error())) 75 + } 76 + @@ -461,7 +478,7 @@ func LoadKeys(keyDir string) ([]key.SSHTPMKeys, error) { 77 + return keys, err 78 + } 79 + 80 + -func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, keyring func() *keyring.ThreadKeyring, tpmFetch func() transport.TPMCloser, ownerPassword func() ([]byte, error), pin func(key.SSHTPMKeys) ([]byte, error)) *Agent { 81 + +func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, keyring func() *keyring.ThreadKeyring, tpmFetch func() transport.TPMCloser, ownerPassword func() ([]byte, error), pin func(key.SSHTPMKeys, int) ([]byte, error)) *Agent { 82 + a := &Agent{ 83 + agents: agents, 84 + tpm: tpmFetch, 85 + diff --git a/agent/agent_test.go b/agent/agent_test.go 86 + index acaec3d..189b607 100644 87 + --- a/agent/agent_test.go 88 + +++ b/agent/agent_test.go 89 + @@ -45,7 +45,7 @@ func TestAddKey(t *testing.T) { 90 + // Owner password 91 + func() ([]byte, error) { return []byte(""), nil }, 92 + // PIN Callback 93 + - func(_ key.SSHTPMKeys) ([]byte, error) { return []byte(""), nil }, 94 + + func(_ key.SSHTPMKeys, _ int) ([]byte, error) { return []byte(""), nil }, 95 + ) 96 + defer ag.Stop() 97 + 98 + diff --git a/askpass/askpass.go b/askpass/askpass.go 99 + index 8ef6370..0653165 100644 100 + --- a/askpass/askpass.go 101 + +++ b/askpass/askpass.go 102 + @@ -8,6 +8,7 @@ import ( 103 + "log/slog" 104 + "os" 105 + "os/exec" 106 + + "strconv" 107 + "strings" 108 + "syscall" 109 + 110 + @@ -66,7 +67,7 @@ func isTerminal(fd uintptr) bool { 111 + return err == nil 112 + } 113 + 114 + -func ReadPassphrase(prompt string, flags ReadPassFlags) ([]byte, error) { 115 + +func ReadPassphrase(prompt string, flags ReadPassFlags, clientPID int) ([]byte, error) { 116 + var allow_askpass bool 117 + var use_askpass bool 118 + var askpass_hint string 119 + @@ -104,7 +105,7 @@ func ReadPassphrase(prompt string, flags ReadPassFlags) ([]byte, error) { 120 + if (flags & RP_ASK_PERMISSION) != 0 { 121 + askpass_hint = "confirm" 122 + } 123 + - return SshAskPass(prompt, askpass_hint) 124 + + return SshAskPass(prompt, askpass_hint, clientPID) 125 + } 126 + 127 + // If we want to echo stuff, we read directly from stdin 128 + @@ -130,7 +131,7 @@ func ReadPassphrase(prompt string, flags ReadPassFlags) ([]byte, error) { 129 + return pin, nil 130 + } 131 + 132 + -func SshAskPass(prompt, hint string) ([]byte, error) { 133 + +func SshAskPass(prompt, hint string, clientPID int) ([]byte, error) { 134 + var askpass string 135 + var err error 136 + if s, ok := os.LookupEnv("SSH_ASKPASS"); ok { 137 + @@ -147,6 +148,11 @@ func SshAskPass(prompt, hint string) ([]byte, error) { 138 + if hint != "" { 139 + os.Setenv("SSH_ASKPASS_PROMPT", hint) 140 + } 141 + + 142 + + if clientPID > 0 { 143 + + os.Setenv("SSH_CLIENT_PID", strconv.Itoa(clientPID)) 144 + + } 145 + + 146 + out, err := exec.Command(askpass, prompt).Output() 147 + switch hint { 148 + case "confirm": 149 + @@ -169,8 +175,8 @@ func SshAskPass(prompt, hint string) ([]byte, error) { 150 + 151 + // AskPremission runs SSH_ASKPASS in with SSH_ASKPASS_PROMPT=confirm set as env 152 + // it will expect exit code 0 or !0 and return 'yes' and 'no' respectively. 153 + -func AskPermission() (bool, error) { 154 + - a, err := ReadPassphrase("Confirm touch", RP_USE_ASKPASS|RP_ASK_PERMISSION) 155 + +func AskPermission(clientPID int) (bool, error) { 156 + + a, err := ReadPassphrase("Confirm touch", RP_USE_ASKPASS|RP_ASK_PERMISSION, clientPID) 157 + if err != nil { 158 + return false, err 159 + } 160 + diff --git a/cmd/ssh-tpm-agent/main.go b/cmd/ssh-tpm-agent/main.go 161 + index 1b76351..797428c 100644 162 + --- a/cmd/ssh-tpm-agent/main.go 163 + +++ b/cmd/ssh-tpm-agent/main.go 164 + @@ -243,7 +243,7 @@ func main() { 165 + // Owner password 166 + func() ([]byte, error) { 167 + if askOwnerPassword { 168 + - return askpass.ReadPassphrase("Enter owner password for TPM", askpass.RP_USE_ASKPASS) 169 + + return askpass.ReadPassphrase("Enter owner password for TPM", askpass.RP_USE_ASKPASS, 0) 170 + } else { 171 + ownerPassword := os.Getenv("SSH_TPM_AGENT_OWNER_PASSWORD") 172 + return []byte(ownerPassword), nil 173 + @@ -253,7 +253,7 @@ func main() { 174 + // PIN Callback with caching 175 + // SSHKeySigner in signer/signer.go resets this value if 176 + // we get a TPMRCAuthFail 177 + - func(key key.SSHTPMKeys) ([]byte, error) { 178 + + func(key key.SSHTPMKeys, clientPID int) ([]byte, error) { 179 + auth, err := agentkeyring.ReadKey(key.Fingerprint()) 180 + if err == nil { 181 + slog.Debug("providing cached userauth for key", slog.String("fp", key.Fingerprint())) 182 + @@ -261,8 +261,8 @@ func main() { 183 + return auth.Read(), nil 184 + } else if errors.Is(err, syscall.ENOKEY) || errors.Is(err, syscall.EACCES) { 185 + keyInfo := fmt.Sprintf("Enter passphrase for (%s): ", key.GetDescription()) 186 + - // TODOt kjk: askpass should box the byte slice 187 + - userauth, err := askpass.ReadPassphrase(keyInfo, askpass.RP_USE_ASKPASS) 188 + + // TODO: askpass should box the byte slice 189 + + userauth, err := askpass.ReadPassphrase(keyInfo, askpass.RP_USE_ASKPASS, clientPID) 190 + fmt.Println(err) 191 + if !noCache && err == nil { 192 + slog.Debug("caching userauth for key in keyring", slog.String("fp", key.Fingerprint())) 193 + diff --git a/cmd/ssh-tpm-agent/main_test.go b/cmd/ssh-tpm-agent/main_test.go 194 + index 051febf..d2ff3c4 100644 195 + --- a/cmd/ssh-tpm-agent/main_test.go 196 + +++ b/cmd/ssh-tpm-agent/main_test.go 197 + @@ -144,7 +144,7 @@ func runSSHAuth(t *testing.T, keytype tpm2.TPMAlgID, bits int, pin []byte, keyfn 198 + // Owner password 199 + func() ([]byte, error) { return []byte(""), nil }, 200 + // PIN Callback 201 + - func(_ key.SSHTPMKeys) ([]byte, error) { 202 + + func(_ key.SSHTPMKeys, _ int) ([]byte, error) { 203 + return pin, nil 204 + }, 205 + ) 206 + diff --git a/cmd/ssh-tpm-keygen/main.go b/cmd/ssh-tpm-keygen/main.go 207 + index f5d9421..12073b0 100644 208 + --- a/cmd/ssh-tpm-keygen/main.go 209 + +++ b/cmd/ssh-tpm-keygen/main.go 210 + @@ -89,11 +89,11 @@ Example: 211 + 212 + func getPin() ([]byte, error) { 213 + for { 214 + - pin1, err := askpass.ReadPassphrase("Enter passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 215 + + pin1, err := askpass.ReadPassphrase("Enter passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 216 + if err != nil { 217 + return nil, err 218 + } 219 + - pin2, err := askpass.ReadPassphrase("Enter same passphrase again: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 220 + + pin2, err := askpass.ReadPassphrase("Enter same passphrase again: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 221 + if err != nil { 222 + return nil, err 223 + } 224 + @@ -106,7 +106,7 @@ func getPin() ([]byte, error) { 225 + } 226 + 227 + func getOwnerPassword() ([]byte, error) { 228 + - return askpass.ReadPassphrase("Enter owner password: ", askpass.RP_ALLOW_STDIN) 229 + + return askpass.ReadPassphrase("Enter owner password: ", askpass.RP_ALLOW_STDIN, 0) 230 + } 231 + 232 + func doHostKeys(tpm transport.TPMCloser, outputFile string, ownerPassword []byte, hierarchy string) { 233 + @@ -191,7 +191,7 @@ func doChangePin(tpm transport.TPMCloser, passphrase, keyPin, ownerPassword []by 234 + fmt.Printf("Key has comment '%s'\n", k.Description) 235 + } 236 + if outputFile == "" { 237 + - f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which the key is (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) 238 + + f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which the key is (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON, 0) 239 + if err != nil { 240 + return err 241 + } 242 + @@ -199,18 +199,18 @@ func doChangePin(tpm transport.TPMCloser, passphrase, keyPin, ownerPassword []by 243 + } 244 + 245 + if len(passphrase) == 0 { 246 + - passphrase, err = askpass.ReadPassphrase("Enter old passphrase: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 247 + + passphrase, err = askpass.ReadPassphrase("Enter old passphrase: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 248 + if err != nil { 249 + return err 250 + } 251 + } 252 + 253 + if len(keyPin) == 0 { 254 + - keyPin, err = askpass.ReadPassphrase("Enter new passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 255 + + keyPin, err = askpass.ReadPassphrase("Enter new passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 256 + if err != nil { 257 + return err 258 + } 259 + - newPin, err := askpass.ReadPassphrase("Enter same passphrase: ", askpass.RP_ALLOW_STDIN) 260 + + newPin, err := askpass.ReadPassphrase("Enter same passphrase: ", askpass.RP_ALLOW_STDIN, 0) 261 + if err != nil { 262 + return err 263 + } 264 + @@ -256,7 +256,7 @@ func doWrapWith(supportedECCBitsizes []int, wrap, wrapWith string, keyParentHand 265 + rawKey, err = ssh.ParseRawPrivateKey(pem) 266 + if errors.As(err, &kerr) { 267 + for { 268 + - pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 269 + + pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 270 + if err != nil { 271 + log.Fatal(err) 272 + } 273 + @@ -353,7 +353,7 @@ func supportedECCBitsizes(tpm transport.TPMCloser) []int { 274 + func checkFile(f string) error { 275 + if utils.FileExists(f) { 276 + fmt.Printf("%s already exists.\n", f) 277 + - s, err := askpass.ReadPassphrase("Overwrite (y/n)? ", askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) 278 + + s, err := askpass.ReadPassphrase("Overwrite (y/n)? ", askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON, 0) 279 + if err != nil { 280 + return err 281 + } 282 + @@ -373,7 +373,7 @@ func doImportKey(tpm transport.TPMCloser, keyParentHandle tpm2.TPMHandle, ownerP 283 + rawKey, err = ssh.ParseRawPrivateKey(pem) 284 + if errors.As(err, &kerr) { 285 + for { 286 + - pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) 287 + + pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE, 0) 288 + if err != nil { 289 + return nil, err 290 + } 291 + @@ -665,7 +665,7 @@ func main() { 292 + // Else create a normal key 293 + fmt.Printf("Generating a sealed public/private %s key pair.\n", keyType) 294 + if outputFile == "" { 295 + - f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which to save the key (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) 296 + + f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which to save the key (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON, 0) 297 + if err != nil { 298 + log.Fatal(err) 299 + } 300 + diff --git a/internal/keytest/keytest.go b/internal/keytest/keytest.go 301 + index ade4ad8..90fcf77 100644 302 + --- a/internal/keytest/keytest.go 303 + +++ b/internal/keytest/keytest.go 304 + @@ -121,7 +121,7 @@ func NewTestAgent(t *testing.T, tpm transport.TPMCloser) *agent.Agent { 305 + func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} }, 306 + func() transport.TPMCloser { return tpm }, 307 + func() ([]byte, error) { return []byte(""), nil }, 308 + - func(_ key.SSHTPMKeys) ([]byte, error) { 309 + + func(_ key.SSHTPMKeys, _ int) ([]byte, error) { 310 + return []byte(""), nil 311 + }, 312 + )
+10
5pkgs/ssh-tpm-agent/package.nix
··· 1 + { 2 + ssh-tpm-agent, 3 + }: 4 + 5 + ssh-tpm-agent.overrideAttrs (old: { 6 + patches = (old.patches or [ ]) ++ [ 7 + ./client-pid.patch 8 + ]; 9 + doCheck = false; 10 + })
+14
tools/pinentry-rofi/flake-module.nix
··· 1 + { 2 + perSystem = 3 + { pkgs, ... }: 4 + { 5 + packages.pinentry-rofi = pkgs.writeShellApplication { 6 + name = "pinentry-rofi"; 7 + runtimeInputs = with pkgs; [ 8 + rofi 9 + coreutils 10 + ]; 11 + text = builtins.readFile ./pinentry-rofi.sh; 12 + }; 13 + }; 14 + }
+77
tools/pinentry-rofi/pinentry-rofi.sh
··· 1 + #!/usr/bin/env bash 2 + # pinentry-rofi: A rofi-based pinentry for SSH TPM authentication 3 + # Reads SSH_CLIENT_PID to extract SSH command context 4 + set -efu 5 + 6 + # Extract SSH args from client PID 7 + # Groups flags with their arguments on the same line 8 + get_ssh_args() { 9 + local pid="${SSH_CLIENT_PID:-}" 10 + [[ -z "$pid" ]] && return 1 11 + [[ ! -r "/proc/$pid/cmdline" ]] && return 1 12 + 13 + # Read command line (null-separated) into array 14 + local -a args=() 15 + while IFS= read -r -d '' arg; do 16 + args+=("$arg") 17 + done < "/proc/$pid/cmdline" 18 + 19 + # Check first arg is ssh 20 + [[ "${args[0]}" == "ssh" || "${args[0]}" == */ssh ]] || return 1 21 + 22 + # Output args, keeping flags and their values on the same line 23 + local i=1 24 + local line="" 25 + while [[ $i -lt ${#args[@]} ]]; do 26 + local arg="${args[$i]}" 27 + if [[ "$arg" == -* ]]; then 28 + # Print previous line if exists 29 + [[ -n "$line" ]] && echo "$line" 30 + line="$arg" 31 + else 32 + # Append to current line 33 + if [[ -n "$line" ]]; then 34 + line="$line $arg" 35 + else 36 + line="$arg" 37 + fi 38 + fi 39 + ((i++)) 40 + done 41 + # Print last line 42 + [[ -n "$line" ]] && echo "$line" 43 + } 44 + 45 + # Get SSH args 46 + ssh_args="" 47 + if ssh_args=$(get_ssh_args 2>/dev/null); then 48 + : # got args 49 + fi 50 + 51 + # Build the rofi message 52 + mesg_args=() 53 + if [[ -n "$ssh_args" ]]; then 54 + mesg="<b>SSH:</b> 55 + $ssh_args" 56 + mesg_args=(-mesg "$mesg") 57 + fi 58 + 59 + # Determine theme 60 + theme_file="/var/theme/config/rofi-theme" 61 + theme_args=() 62 + if [[ -r "$theme_file" ]]; then 63 + theme=$(cat "$theme_file") 64 + theme_args=(-theme "$theme") 65 + fi 66 + 67 + # Use rofi in password mode 68 + result=$(rofi -dmenu \ 69 + -password \ 70 + -p "PIN" \ 71 + -lines 0 \ 72 + -no-fixed-num-lines \ 73 + "${mesg_args[@]}" \ 74 + "${theme_args[@]}" \ 75 + < /dev/null 2>/dev/null) || exit 1 76 + 77 + echo "$result"