xref: /freebsd/crypto/openssh/misc-agent.c (revision 2574974648c68c738aec3ff96644d888d7913a37)
1 /* $OpenBSD: misc-agent.c,v 1.7 2026/02/11 17:05:32 dtucker Exp $ */
2 /*
3  * Copyright (c) 2025 Damien Miller <djm@mindrot.org>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include "includes.h"
19 
20 #include <sys/types.h>
21 #include <sys/socket.h>
22 #include <sys/stat.h>
23 #include <sys/un.h>
24 
25 #include <dirent.h>
26 #include <errno.h>
27 #include <fcntl.h>
28 #include <netdb.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <time.h>
32 #include <unistd.h>
33 
34 #include "digest.h"
35 #include "log.h"
36 #include "misc.h"
37 #include "pathnames.h"
38 #include "ssh.h"
39 #include "xmalloc.h"
40 
41 /* stuff shared by agent listeners (ssh-agent and sshd agent forwarding) */
42 
43 #define SOCKET_HOSTNAME_HASHLEN 10 /* length of hostname hash in socket path */
44 
45 /* used for presenting random strings in unix_listener_tmp and hostname_hash */
46 static const char presentation_chars[] =
47     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
48 
49 /* returns a text-encoded hash of the hostname of specified length (max 64) */
50 static char *
hostname_hash(size_t len)51 hostname_hash(size_t len)
52 {
53 	char hostname[NI_MAXHOST], p[65];
54 	u_char hash[64];
55 	int r;
56 	size_t l, i;
57 
58 	l = ssh_digest_bytes(SSH_DIGEST_SHA512);
59 	if (len > 64) {
60 		error_f("bad length %zu >= max %zd", len, l);
61 		return NULL;
62 	}
63 	if (gethostname(hostname, sizeof(hostname)) == -1) {
64 		error_f("gethostname: %s", strerror(errno));
65 		return NULL;
66 	}
67 	if ((r = ssh_digest_memory(SSH_DIGEST_SHA512,
68 	    hostname, strlen(hostname), hash, sizeof(hash))) != 0) {
69 		error_fr(r, "ssh_digest_memory");
70 		return NULL;
71 	}
72 	memset(p, '\0', sizeof(p));
73 	for (i = 0; i < l; i++)
74 		p[i] = presentation_chars[
75 		    hash[i] % (sizeof(presentation_chars) - 1)];
76 	/* debug3_f("hostname \"%s\" => hash \"%s\"", hostname, p); */
77 	p[len] = '\0';
78 	return xstrdup(p);
79 }
80 
81 char *
agent_hostname_hash(void)82 agent_hostname_hash(void)
83 {
84 	return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
85 }
86 
87 /*
88  * Creates a unix listener at a mkstemp(3)-style path, e.g. "/dir/sock.XXXXXX"
89  * Supplied path is modified to the actual one used.
90  */
91 static int
unix_listener_tmp(char * path,int backlog)92 unix_listener_tmp(char *path, int backlog)
93 {
94 	struct sockaddr_un sunaddr;
95 	int good, sock = -1;
96 	size_t i, xstart;
97 	mode_t prev_mask;
98 
99 	/* Find first 'X' template character back from end of string */
100 	xstart = strlen(path);
101 	while (xstart > 0 && path[xstart - 1] == 'X')
102 		xstart--;
103 
104 	memset(&sunaddr, 0, sizeof(sunaddr));
105 	sunaddr.sun_family = AF_UNIX;
106 	prev_mask = umask(0177);
107 	for (good = 0; !good;) {
108 		sock = -1;
109 		/* Randomise path suffix */
110 		for (i = xstart; path[i] != '\0'; i++) {
111 			path[i] = presentation_chars[
112 			    arc4random_uniform(sizeof(presentation_chars)-1)];
113 		}
114 		debug_f("trying path \"%s\"", path);
115 
116 		if (strlcpy(sunaddr.sun_path, path,
117 		    sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
118 			error_f("path \"%s\" too long for Unix domain socket",
119 			    path);
120 			break;
121 		}
122 
123 		if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
124 			error_f("socket: %.100s", strerror(errno));
125 			break;
126 		}
127 		if (bind(sock, (struct sockaddr *)&sunaddr,
128 		    sizeof(sunaddr)) == -1) {
129 			if (errno == EADDRINUSE) {
130 				error_f("bind \"%s\": %.100s",
131 				    path, strerror(errno));
132 				close(sock);
133 				sock = -1;
134 				continue;
135 			}
136 			error_f("bind \"%s\": %.100s", path, strerror(errno));
137 			break;
138 		}
139 		if (listen(sock, backlog) == -1) {
140 			error_f("listen \"%s\": %s", path, strerror(errno));
141 			break;
142 		}
143 		good = 1;
144 	}
145 	umask(prev_mask);
146 	if (good) {
147 		debug3_f("listening on unix socket \"%s\" as fd=%d",
148 		    path, sock);
149 	} else if (sock != -1) {
150 		close(sock);
151 		sock = -1;
152 	}
153 	return sock;
154 }
155 
156 /*
157  * Create a subdirectory under the supplied home directory if it
158  * doesn't already exist
159  */
160 static int
ensure_mkdir(const char * homedir,const char * subdir)161 ensure_mkdir(const char *homedir, const char *subdir)
162 {
163 	char *path;
164 
165 	xasprintf(&path, "%s/%s", homedir, subdir);
166 	if (mkdir(path, 0700) == 0)
167 		debug("created directory %s", path);
168 	else if (errno != EEXIST) {
169 		error_f("mkdir %s: %s", path, strerror(errno));
170 		free(path);
171 		return -1;
172 	}
173 	free(path);
174 	return 0;
175 }
176 
177 static int
agent_prepare_sockdir(const char * homedir)178 agent_prepare_sockdir(const char *homedir)
179 {
180 	if (homedir == NULL || *homedir == '\0' ||
181 	    ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 ||
182 	    ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0)
183 		return -1;
184 	return 0;
185 }
186 
187 
188 /* Get a path template for an agent socket in the user's homedir */
189 static char *
agent_socket_template(const char * homedir,const char * tag)190 agent_socket_template(const char *homedir, const char *tag)
191 {
192 	char *hostnamehash, *ret;
193 
194 	if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL)
195 		return NULL;
196 	xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX",
197 	    homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag);
198 	free(hostnamehash);
199 	return ret;
200 }
201 
202 int
agent_listener(const char * homedir,const char * tag,int * sockp,char ** pathp)203 agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp)
204 {
205 	int sock;
206 	char *path;
207 
208 	*sockp = -1;
209 	*pathp = NULL;
210 
211 	if (agent_prepare_sockdir(homedir) != 0)
212 		return -1; /* error already logged */
213 	if ((path = agent_socket_template(homedir, tag)) == NULL)
214 		return -1; /* error already logged */
215 	if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
216 		free(path);
217 		return -1; /* error already logged */
218 	}
219 	/* success */
220 	*sockp = sock;
221 	*pathp = path;
222 	return 0;
223 }
224 
225 static int
socket_is_stale(const char * path)226 socket_is_stale(const char *path)
227 {
228 	int fd, r;
229 	struct sockaddr_un sunaddr;
230 	socklen_t l = sizeof(r);
231 
232 	/* attempt non-blocking connect on socket */
233 	memset(&sunaddr, '\0', sizeof(sunaddr));
234 	sunaddr.sun_family = AF_UNIX;
235 	if (strlcpy(sunaddr.sun_path, path,
236 	    sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
237 		debug_f("path for \"%s\" too long for sockaddr_un", path);
238 		return 0;
239 	}
240 	if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
241 		error_f("socket: %s", strerror(errno));
242 		return 0;
243 	}
244 	set_nonblock(fd);
245 	/* a socket without a listener should yield an error immediately */
246 	if (connect(fd, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
247 		debug_f("connect \"%s\": %s", path, strerror(errno));
248 		close(fd);
249 		return 1;
250 	}
251 	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &r, &l) == -1) {
252 		debug_f("getsockopt: %s", strerror(errno));
253 		close(fd);
254 		return 0;
255 	}
256 	if (r != 0) {
257 		debug_f("socket error on %s: %s", path, strerror(errno));
258 		close(fd);
259 		return 1;
260 	}
261 	close(fd);
262 	debug_f("socket %s seems still active", path);
263 	return 0;
264 }
265 
266 #ifndef HAVE_FSTATAT
267 # define fstatat(x, y, buf, z) lstat(path, buf)
268 #endif
269 #ifndef HAVE_UNLINKAT
270 # define unlinkat(x, y, z) unlink(path)
271 #endif
272 
273 void
agent_cleanup_stale(const char * homedir,int ignore_hosthash)274 agent_cleanup_stale(const char *homedir, int ignore_hosthash)
275 {
276 	DIR *d = NULL;
277 	struct dirent *dp;
278 	struct stat sb;
279 	char *prefix = NULL, *dirpath = NULL, *path = NULL;
280 	struct timespec now, sub, *mtimp = NULL;
281 
282 	/* Only consider sockets last modified > 1 hour ago */
283 	if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
284 		error_f("clock_gettime: %s", strerror(errno));
285 		return;
286 	}
287 	sub.tv_sec = 60 * 60;
288 	sub.tv_nsec = 0;
289 	timespecsub(&now, &sub, &now);
290 
291 	/* Only consider sockets from the same hostname */
292 	if (!ignore_hosthash) {
293 		if ((path = agent_hostname_hash()) == NULL) {
294 			error_f("couldn't get hostname hash");
295 			return;
296 		}
297 		xasprintf(&prefix, "s.%s.", path);
298 		free(path);
299 		path = NULL;
300 	}
301 
302 	xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR);
303 	if ((d = opendir(dirpath)) == NULL) {
304 		if (errno != ENOENT)
305 			error_f("opendir \"%s\": %s", dirpath, strerror(errno));
306 		goto out;
307 	}
308 
309 	path = NULL;
310 	while ((dp = readdir(d)) != NULL) {
311 		free(path);
312 		xasprintf(&path, "%s/%s", dirpath, dp->d_name);
313 #ifdef HAVE_DIRENT_D_TYPE
314 		if (dp->d_type != DT_SOCK && dp->d_type != DT_UNKNOWN)
315 			continue;
316 #endif
317 		if (fstatat(dirfd(d), dp->d_name,
318 		    &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) {
319 			error_f("stat \"%s/%s\": %s",
320 			    dirpath, dp->d_name, strerror(errno));
321 			continue;
322 		}
323 		if (!S_ISSOCK(sb.st_mode))
324 			continue;
325 #ifdef HAVE_STRUCT_STAT_ST_MTIM
326 		mtimp = &sb.st_mtim;
327 #else
328 		sub.tv_sec = sb.st_mtime;
329 		sub.tv_nsec = 0;
330 		mtimp = &sub;
331 #endif
332 		if (timespeccmp(mtimp, &now, >)) {
333 			debug3_f("Ignoring recent socket \"%s/%s\"",
334 			    dirpath, dp->d_name);
335 			continue;
336 		}
337 		if (!ignore_hosthash &&
338 		    strncmp(dp->d_name, prefix, strlen(prefix)) != 0) {
339 			debug3_f("Ignoring socket \"%s/%s\" "
340 			    "from different host", dirpath, dp->d_name);
341 			continue;
342 		}
343 		if (socket_is_stale(path)) {
344 			debug_f("cleanup stale socket %s", path);
345 			unlinkat(dirfd(d), dp->d_name, 0);
346 		}
347 	}
348  out:
349 	if (d != NULL)
350 		closedir(d);
351 	free(path);
352 	free(dirpath);
353 	free(prefix);
354 }
355 
356 #undef unlinkat
357 #undef fstatat
358