Skip to content

Commit c7a0ce9

Browse files
committed
agent: show requesting process in confirm prompt
Append "Requested by ssh (1234) ← git ← zsh ← kitty" to the askpass prompt for keys added with -c, so users can tell what is asking. The peer pid comes from SO_PEERCRED on the accepted socket; the chain is read from /proc and omitted on failure. A connAgent wrapper carries the peer string into Sign since agent.ServeAgent hides the net.Conn. Allow RO /proc under landlock so the lookup works when sandboxed.
1 parent e8cd630 commit c7a0ce9

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

agent/agent.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ func (a *Agent) List() ([]*agent.Key, error) {
164164
}
165165

166166
func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
167+
return a.signWithFlags(key, data, flags, "")
168+
}
169+
170+
func (a *Agent) signWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags, peer string) (*ssh.Signature, error) {
167171
slog.Debug("called signwithflags")
168172
a.mu.Lock()
169173
defer a.mu.Unlock()
@@ -200,7 +204,7 @@ func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.Signat
200204
if !bytes.Equal(s.PublicKey().Marshal(), wantKey) {
201205
continue
202206
}
203-
if err := a.confirmKeyUse(wantKey); err != nil {
207+
if err := a.confirmKeyUse(wantKey, peer); err != nil {
204208
return nil, err
205209
}
206210
return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg)
@@ -233,7 +237,8 @@ func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
233237
// the confirm constraint (ssh-tpm-add -c). Mirrors ssh-agent(1) behaviour.
234238
// wantKey must be the marshalled public key with any certificate wrapper
235239
// already stripped, matching the comparison done in SignWithFlags.
236-
func (a *Agent) confirmKeyUse(wantKey []byte) error {
240+
// peer (from peerChain) is appended to the prompt when non-empty.
241+
func (a *Agent) confirmKeyUse(wantKey []byte, peer string) error {
237242
for _, k := range a.keys {
238243
// AgentKey() may return a certificate; unwrap it so that a key
239244
// added both plain and as a cert is covered by either entry's
@@ -252,6 +257,9 @@ func (a *Agent) confirmKeyUse(wantKey []byte) error {
252257
}
253258
prompt := fmt.Sprintf("Allow use of key %s?\nKey fingerprint %s.",
254259
k.GetDescription(), k.Fingerprint())
260+
if peer != "" {
261+
prompt += "\nRequested by " + peer
262+
}
255263
ok, err := askpass.AskPermission(prompt)
256264
if err != nil {
257265
slog.Info("askpass confirmation failed", slog.String("error", err.Error()))
@@ -265,8 +273,24 @@ func (a *Agent) confirmKeyUse(wantKey []byte) error {
265273
return nil
266274
}
267275

268-
func (a *Agent) serveConn(c net.Conn) {
269-
if err := agent.ServeAgent(a, c); err != io.EOF {
276+
// connAgent threads the per-connection peer description into Sign,
277+
// since agent.ServeAgent does not expose the net.Conn to handlers.
278+
type connAgent struct {
279+
*Agent
280+
peer string
281+
}
282+
283+
func (c *connAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
284+
return c.Agent.signWithFlags(key, data, 0, c.peer)
285+
}
286+
287+
func (c *connAgent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
288+
return c.Agent.signWithFlags(key, data, flags, c.peer)
289+
}
290+
291+
func (a *Agent) serveConn(c *net.UnixConn) {
292+
ca := &connAgent{Agent: a, peer: peerChain(c)}
293+
if err := agent.ServeAgent(ca, c); err != io.EOF {
270294
slog.Info("Agent client connection ended unsuccessfully", slog.String("error", err.Error()))
271295
}
272296
}

agent/peer.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package agent
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os"
7+
"strconv"
8+
"strings"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
const maxPeerChain = 4
14+
15+
// peerChain describes the process at the other end of c and a few of its
16+
// ancestors, e.g. "ssh (1234) ← git ← zsh ← kitty". Best effort; returns ""
17+
// if SO_PEERCRED or /proc are unavailable.
18+
func peerChain(c *net.UnixConn) string {
19+
rc, err := c.SyscallConn()
20+
if err != nil {
21+
return ""
22+
}
23+
var ucred *unix.Ucred
24+
if cerr := rc.Control(func(fd uintptr) {
25+
ucred, err = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
26+
}); cerr != nil || err != nil {
27+
return ""
28+
}
29+
30+
var parts []string
31+
for i, pid := 0, int(ucred.Pid); i < maxPeerChain && pid > 1; i++ {
32+
name, ppid := procStatus(pid)
33+
if name == "" {
34+
break
35+
}
36+
if i == 0 {
37+
name = fmt.Sprintf("%s (%d)", name, pid)
38+
}
39+
parts = append(parts, name)
40+
pid = ppid
41+
}
42+
return strings.Join(parts, " ← ")
43+
}
44+
45+
// procStatus returns Name and PPid from /proc/<pid>/status.
46+
func procStatus(pid int) (name string, ppid int) {
47+
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/status")
48+
if err != nil {
49+
return
50+
}
51+
for _, line := range strings.Split(string(b), "\n") {
52+
if v, ok := strings.CutPrefix(line, "Name:"); ok {
53+
name = strings.TrimSpace(v)
54+
} else if v, ok := strings.CutPrefix(line, "PPid:"); ok {
55+
ppid, _ = strconv.Atoi(strings.TrimSpace(v))
56+
return
57+
}
58+
}
59+
return
60+
}

cmd/ssh-tpm-agent/testdata/script/agent_confirm.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ exec ssh-tpm-add -c
1111
# Approve: askpass exits 0, signing succeeds, prompt+hint are recorded
1212
exec ssh-keygen -Y sign -n file -f .ssh/id_ecdsa.pub file_to_sign.txt
1313
grep '^confirm Allow use of key' askpass.log
14+
grep 'Requested by ssh-keygen' askpass.log
1415
rm askpass.log file_to_sign.txt.sig
1516

1617
# Deny: sentinel file makes askpass exit 1, signing fails

internal/lsm/lsm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func RestrictAgentFiles() {
3131
"/etc/localtime",
3232
"/dev/null",
3333
),
34+
// Peer process lookup for the confirm prompt
35+
landlock.RODirs("/proc"),
3436
// We almost always want to read the TPM
3537
landlock.RWFiles(
3638
"/dev/tpm0",

0 commit comments

Comments
 (0)