diff options
Diffstat (limited to 'ssh-sk-client.c')
-rw-r--r-- | ssh-sk-client.c | 449 |
1 files changed, 449 insertions, 0 deletions
diff --git a/ssh-sk-client.c b/ssh-sk-client.c new file mode 100644 index 000000000..8d7e6c305 --- /dev/null +++ b/ssh-sk-client.c | |||
@@ -0,0 +1,449 @@ | |||
1 | /* $OpenBSD: ssh-sk-client.c,v 1.7 2020/01/23 07:10:22 dtucker Exp $ */ | ||
2 | /* | ||
3 | * Copyright (c) 2019 Google LLC | ||
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/wait.h> | ||
23 | |||
24 | #include <fcntl.h> | ||
25 | #include <limits.h> | ||
26 | #include <errno.h> | ||
27 | #include <signal.h> | ||
28 | #include <stdarg.h> | ||
29 | #include <stdio.h> | ||
30 | #include <stdlib.h> | ||
31 | #include <string.h> | ||
32 | #include <unistd.h> | ||
33 | |||
34 | #include "log.h" | ||
35 | #include "ssherr.h" | ||
36 | #include "sshbuf.h" | ||
37 | #include "sshkey.h" | ||
38 | #include "msg.h" | ||
39 | #include "digest.h" | ||
40 | #include "pathnames.h" | ||
41 | #include "ssh-sk.h" | ||
42 | #include "misc.h" | ||
43 | |||
44 | /* #define DEBUG_SK 1 */ | ||
45 | |||
46 | static int | ||
47 | start_helper(int *fdp, pid_t *pidp, void (**osigchldp)(int)) | ||
48 | { | ||
49 | void (*osigchld)(int); | ||
50 | int oerrno, pair[2], r = SSH_ERR_INTERNAL_ERROR; | ||
51 | pid_t pid; | ||
52 | char *helper, *verbosity = NULL; | ||
53 | |||
54 | *fdp = -1; | ||
55 | *pidp = 0; | ||
56 | *osigchldp = SIG_DFL; | ||
57 | |||
58 | helper = getenv("SSH_SK_HELPER"); | ||
59 | if (helper == NULL || strlen(helper) == 0) | ||
60 | helper = _PATH_SSH_SK_HELPER; | ||
61 | if (access(helper, X_OK) != 0) { | ||
62 | oerrno = errno; | ||
63 | error("%s: helper \"%s\" unusable: %s", __func__, helper, | ||
64 | strerror(errno)); | ||
65 | errno = oerrno; | ||
66 | return SSH_ERR_SYSTEM_ERROR; | ||
67 | } | ||
68 | #ifdef DEBUG_SK | ||
69 | verbosity = "-vvv"; | ||
70 | #endif | ||
71 | |||
72 | /* Start helper */ | ||
73 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == -1) { | ||
74 | error("socketpair: %s", strerror(errno)); | ||
75 | return SSH_ERR_SYSTEM_ERROR; | ||
76 | } | ||
77 | osigchld = ssh_signal(SIGCHLD, SIG_DFL); | ||
78 | if ((pid = fork()) == -1) { | ||
79 | oerrno = errno; | ||
80 | error("fork: %s", strerror(errno)); | ||
81 | close(pair[0]); | ||
82 | close(pair[1]); | ||
83 | ssh_signal(SIGCHLD, osigchld); | ||
84 | errno = oerrno; | ||
85 | return SSH_ERR_SYSTEM_ERROR; | ||
86 | } | ||
87 | if (pid == 0) { | ||
88 | if ((dup2(pair[1], STDIN_FILENO) == -1) || | ||
89 | (dup2(pair[1], STDOUT_FILENO) == -1)) { | ||
90 | error("%s: dup2: %s", __func__, ssh_err(r)); | ||
91 | _exit(1); | ||
92 | } | ||
93 | close(pair[0]); | ||
94 | close(pair[1]); | ||
95 | closefrom(STDERR_FILENO + 1); | ||
96 | debug("%s: starting %s %s", __func__, helper, | ||
97 | verbosity == NULL ? "" : verbosity); | ||
98 | execlp(helper, helper, verbosity, (char *)NULL); | ||
99 | error("%s: execlp: %s", __func__, strerror(errno)); | ||
100 | _exit(1); | ||
101 | } | ||
102 | close(pair[1]); | ||
103 | |||
104 | /* success */ | ||
105 | debug3("%s: started pid=%ld", __func__, (long)pid); | ||
106 | *fdp = pair[0]; | ||
107 | *pidp = pid; | ||
108 | *osigchldp = osigchld; | ||
109 | return 0; | ||
110 | } | ||
111 | |||
112 | static int | ||
113 | reap_helper(pid_t pid) | ||
114 | { | ||
115 | int status, oerrno; | ||
116 | |||
117 | debug3("%s: pid=%ld", __func__, (long)pid); | ||
118 | |||
119 | errno = 0; | ||
120 | while (waitpid(pid, &status, 0) == -1) { | ||
121 | if (errno == EINTR) { | ||
122 | errno = 0; | ||
123 | continue; | ||
124 | } | ||
125 | oerrno = errno; | ||
126 | error("%s: waitpid: %s", __func__, strerror(errno)); | ||
127 | errno = oerrno; | ||
128 | return SSH_ERR_SYSTEM_ERROR; | ||
129 | } | ||
130 | if (!WIFEXITED(status)) { | ||
131 | error("%s: helper exited abnormally", __func__); | ||
132 | return SSH_ERR_AGENT_FAILURE; | ||
133 | } else if (WEXITSTATUS(status) != 0) { | ||
134 | error("%s: helper exited with non-zero exit status", __func__); | ||
135 | return SSH_ERR_AGENT_FAILURE; | ||
136 | } | ||
137 | return 0; | ||
138 | } | ||
139 | |||
140 | static int | ||
141 | client_converse(struct sshbuf *msg, struct sshbuf **respp, u_int type) | ||
142 | { | ||
143 | int oerrno, fd, r2, ll, r = SSH_ERR_INTERNAL_ERROR; | ||
144 | u_int rtype, rerr; | ||
145 | pid_t pid; | ||
146 | u_char version; | ||
147 | void (*osigchld)(int); | ||
148 | struct sshbuf *req = NULL, *resp = NULL; | ||
149 | *respp = NULL; | ||
150 | |||
151 | if ((r = start_helper(&fd, &pid, &osigchld)) != 0) | ||
152 | return r; | ||
153 | |||
154 | if ((req = sshbuf_new()) == NULL || (resp = sshbuf_new()) == NULL) { | ||
155 | r = SSH_ERR_ALLOC_FAIL; | ||
156 | goto out; | ||
157 | } | ||
158 | /* Request preamble: type, log_on_stderr, log_level */ | ||
159 | ll = log_level_get(); | ||
160 | if ((r = sshbuf_put_u32(req, type)) != 0 || | ||
161 | (r = sshbuf_put_u8(req, log_is_on_stderr() != 0)) != 0 || | ||
162 | (r = sshbuf_put_u32(req, ll < 0 ? 0 : ll)) != 0 || | ||
163 | (r = sshbuf_putb(req, msg)) != 0) { | ||
164 | error("%s: build: %s", __func__, ssh_err(r)); | ||
165 | goto out; | ||
166 | } | ||
167 | if ((r = ssh_msg_send(fd, SSH_SK_HELPER_VERSION, req)) != 0) { | ||
168 | error("%s: send: %s", __func__, ssh_err(r)); | ||
169 | goto out; | ||
170 | } | ||
171 | if ((r = ssh_msg_recv(fd, resp)) != 0) { | ||
172 | error("%s: receive: %s", __func__, ssh_err(r)); | ||
173 | goto out; | ||
174 | } | ||
175 | if ((r = sshbuf_get_u8(resp, &version)) != 0) { | ||
176 | error("%s: parse version: %s", __func__, ssh_err(r)); | ||
177 | goto out; | ||
178 | } | ||
179 | if (version != SSH_SK_HELPER_VERSION) { | ||
180 | error("%s: unsupported version: got %u, expected %u", | ||
181 | __func__, version, SSH_SK_HELPER_VERSION); | ||
182 | r = SSH_ERR_INVALID_FORMAT; | ||
183 | goto out; | ||
184 | } | ||
185 | if ((r = sshbuf_get_u32(resp, &rtype)) != 0) { | ||
186 | error("%s: parse message type: %s", __func__, ssh_err(r)); | ||
187 | goto out; | ||
188 | } | ||
189 | if (rtype == SSH_SK_HELPER_ERROR) { | ||
190 | if ((r = sshbuf_get_u32(resp, &rerr)) != 0) { | ||
191 | error("%s: parse error: %s", __func__, ssh_err(r)); | ||
192 | goto out; | ||
193 | } | ||
194 | debug("%s: helper returned error -%u", __func__, rerr); | ||
195 | /* OpenSSH error values are negative; encoded as -err on wire */ | ||
196 | if (rerr == 0 || rerr >= INT_MAX) | ||
197 | r = SSH_ERR_INTERNAL_ERROR; | ||
198 | else | ||
199 | r = -(int)rerr; | ||
200 | goto out; | ||
201 | } else if (rtype != type) { | ||
202 | error("%s: helper returned incorrect message type %u, " | ||
203 | "expecting %u", __func__, rtype, type); | ||
204 | r = SSH_ERR_INTERNAL_ERROR; | ||
205 | goto out; | ||
206 | } | ||
207 | /* success */ | ||
208 | r = 0; | ||
209 | out: | ||
210 | oerrno = errno; | ||
211 | close(fd); | ||
212 | if ((r2 = reap_helper(pid)) != 0) { | ||
213 | if (r == 0) { | ||
214 | r = r2; | ||
215 | oerrno = errno; | ||
216 | } | ||
217 | } | ||
218 | if (r == 0) { | ||
219 | *respp = resp; | ||
220 | resp = NULL; | ||
221 | } | ||
222 | sshbuf_free(req); | ||
223 | sshbuf_free(resp); | ||
224 | ssh_signal(SIGCHLD, osigchld); | ||
225 | errno = oerrno; | ||
226 | return r; | ||
227 | |||
228 | } | ||
229 | |||
230 | int | ||
231 | sshsk_sign(const char *provider, struct sshkey *key, | ||
232 | u_char **sigp, size_t *lenp, const u_char *data, size_t datalen, | ||
233 | u_int compat, const char *pin) | ||
234 | { | ||
235 | int oerrno, r = SSH_ERR_INTERNAL_ERROR; | ||
236 | char *fp = NULL; | ||
237 | struct sshbuf *kbuf = NULL, *req = NULL, *resp = NULL; | ||
238 | |||
239 | *sigp = NULL; | ||
240 | *lenp = 0; | ||
241 | |||
242 | #ifndef ENABLE_SK | ||
243 | return SSH_ERR_KEY_TYPE_UNKNOWN; | ||
244 | #endif | ||
245 | |||
246 | if ((kbuf = sshbuf_new()) == NULL || | ||
247 | (req = sshbuf_new()) == NULL) { | ||
248 | r = SSH_ERR_ALLOC_FAIL; | ||
249 | goto out; | ||
250 | } | ||
251 | |||
252 | if ((r = sshkey_private_serialize(key, kbuf)) != 0) { | ||
253 | error("%s: serialize private key: %s", __func__, ssh_err(r)); | ||
254 | goto out; | ||
255 | } | ||
256 | if ((r = sshbuf_put_stringb(req, kbuf)) != 0 || | ||
257 | (r = sshbuf_put_cstring(req, provider)) != 0 || | ||
258 | (r = sshbuf_put_string(req, data, datalen)) != 0 || | ||
259 | (r = sshbuf_put_cstring(req, NULL)) != 0 || /* alg */ | ||
260 | (r = sshbuf_put_u32(req, compat)) != 0 || | ||
261 | (r = sshbuf_put_cstring(req, pin)) != 0) { | ||
262 | error("%s: compose: %s", __func__, ssh_err(r)); | ||
263 | goto out; | ||
264 | } | ||
265 | |||
266 | if ((fp = sshkey_fingerprint(key, SSH_FP_HASH_DEFAULT, | ||
267 | SSH_FP_DEFAULT)) == NULL) { | ||
268 | error("%s: sshkey_fingerprint failed", __func__); | ||
269 | r = SSH_ERR_ALLOC_FAIL; | ||
270 | goto out; | ||
271 | } | ||
272 | if ((r = client_converse(req, &resp, SSH_SK_HELPER_SIGN)) != 0) | ||
273 | goto out; | ||
274 | |||
275 | if ((r = sshbuf_get_string(resp, sigp, lenp)) != 0) { | ||
276 | error("%s: parse signature: %s", __func__, ssh_err(r)); | ||
277 | r = SSH_ERR_INVALID_FORMAT; | ||
278 | goto out; | ||
279 | } | ||
280 | if (sshbuf_len(resp) != 0) { | ||
281 | error("%s: trailing data in response", __func__); | ||
282 | r = SSH_ERR_INVALID_FORMAT; | ||
283 | goto out; | ||
284 | } | ||
285 | /* success */ | ||
286 | r = 0; | ||
287 | out: | ||
288 | oerrno = errno; | ||
289 | if (r != 0) { | ||
290 | freezero(*sigp, *lenp); | ||
291 | *sigp = NULL; | ||
292 | *lenp = 0; | ||
293 | } | ||
294 | sshbuf_free(kbuf); | ||
295 | sshbuf_free(req); | ||
296 | sshbuf_free(resp); | ||
297 | errno = oerrno; | ||
298 | return r; | ||
299 | } | ||
300 | |||
301 | int | ||
302 | sshsk_enroll(int type, const char *provider_path, const char *device, | ||
303 | const char *application, const char *userid, uint8_t flags, | ||
304 | const char *pin, struct sshbuf *challenge_buf, | ||
305 | struct sshkey **keyp, struct sshbuf *attest) | ||
306 | { | ||
307 | int oerrno, r = SSH_ERR_INTERNAL_ERROR; | ||
308 | struct sshbuf *kbuf = NULL, *abuf = NULL, *req = NULL, *resp = NULL; | ||
309 | struct sshkey *key = NULL; | ||
310 | |||
311 | *keyp = NULL; | ||
312 | if (attest != NULL) | ||
313 | sshbuf_reset(attest); | ||
314 | |||
315 | #ifndef ENABLE_SK | ||
316 | return SSH_ERR_KEY_TYPE_UNKNOWN; | ||
317 | #endif | ||
318 | |||
319 | if (type < 0) | ||
320 | return SSH_ERR_INVALID_ARGUMENT; | ||
321 | |||
322 | if ((abuf = sshbuf_new()) == NULL || | ||
323 | (kbuf = sshbuf_new()) == NULL || | ||
324 | (req = sshbuf_new()) == NULL) { | ||
325 | r = SSH_ERR_ALLOC_FAIL; | ||
326 | goto out; | ||
327 | } | ||
328 | |||
329 | if ((r = sshbuf_put_u32(req, (u_int)type)) != 0 || | ||
330 | (r = sshbuf_put_cstring(req, provider_path)) != 0 || | ||
331 | (r = sshbuf_put_cstring(req, device)) != 0 || | ||
332 | (r = sshbuf_put_cstring(req, application)) != 0 || | ||
333 | (r = sshbuf_put_cstring(req, userid)) != 0 || | ||
334 | (r = sshbuf_put_u8(req, flags)) != 0 || | ||
335 | (r = sshbuf_put_cstring(req, pin)) != 0 || | ||
336 | (r = sshbuf_put_stringb(req, challenge_buf)) != 0) { | ||
337 | error("%s: compose: %s", __func__, ssh_err(r)); | ||
338 | goto out; | ||
339 | } | ||
340 | |||
341 | if ((r = client_converse(req, &resp, SSH_SK_HELPER_ENROLL)) != 0) | ||
342 | goto out; | ||
343 | |||
344 | if ((r = sshbuf_get_stringb(resp, kbuf)) != 0 || | ||
345 | (r = sshbuf_get_stringb(resp, abuf)) != 0) { | ||
346 | error("%s: parse signature: %s", __func__, ssh_err(r)); | ||
347 | r = SSH_ERR_INVALID_FORMAT; | ||
348 | goto out; | ||
349 | } | ||
350 | if (sshbuf_len(resp) != 0) { | ||
351 | error("%s: trailing data in response", __func__); | ||
352 | r = SSH_ERR_INVALID_FORMAT; | ||
353 | goto out; | ||
354 | } | ||
355 | if ((r = sshkey_private_deserialize(kbuf, &key)) != 0) { | ||
356 | error("Unable to parse private key: %s", ssh_err(r)); | ||
357 | goto out; | ||
358 | } | ||
359 | if (attest != NULL && (r = sshbuf_putb(attest, abuf)) != 0) { | ||
360 | error("%s: buffer error: %s", __func__, ssh_err(r)); | ||
361 | goto out; | ||
362 | } | ||
363 | |||
364 | /* success */ | ||
365 | r = 0; | ||
366 | *keyp = key; | ||
367 | key = NULL; | ||
368 | out: | ||
369 | oerrno = errno; | ||
370 | sshkey_free(key); | ||
371 | sshbuf_free(kbuf); | ||
372 | sshbuf_free(abuf); | ||
373 | sshbuf_free(req); | ||
374 | sshbuf_free(resp); | ||
375 | errno = oerrno; | ||
376 | return r; | ||
377 | } | ||
378 | |||
379 | int | ||
380 | sshsk_load_resident(const char *provider_path, const char *device, | ||
381 | const char *pin, struct sshkey ***keysp, size_t *nkeysp) | ||
382 | { | ||
383 | int oerrno, r = SSH_ERR_INTERNAL_ERROR; | ||
384 | struct sshbuf *kbuf = NULL, *req = NULL, *resp = NULL; | ||
385 | struct sshkey *key = NULL, **keys = NULL, **tmp; | ||
386 | size_t i, nkeys = 0; | ||
387 | |||
388 | *keysp = NULL; | ||
389 | *nkeysp = 0; | ||
390 | |||
391 | if ((resp = sshbuf_new()) == NULL || | ||
392 | (kbuf = sshbuf_new()) == NULL || | ||
393 | (req = sshbuf_new()) == NULL) { | ||
394 | r = SSH_ERR_ALLOC_FAIL; | ||
395 | goto out; | ||
396 | } | ||
397 | |||
398 | if ((r = sshbuf_put_cstring(req, provider_path)) != 0 || | ||
399 | (r = sshbuf_put_cstring(req, device)) != 0 || | ||
400 | (r = sshbuf_put_cstring(req, pin)) != 0) { | ||
401 | error("%s: compose: %s", __func__, ssh_err(r)); | ||
402 | goto out; | ||
403 | } | ||
404 | |||
405 | if ((r = client_converse(req, &resp, SSH_SK_HELPER_LOAD_RESIDENT)) != 0) | ||
406 | goto out; | ||
407 | |||
408 | while (sshbuf_len(resp) != 0) { | ||
409 | /* key, comment */ | ||
410 | if ((r = sshbuf_get_stringb(resp, kbuf)) != 0 || | ||
411 | (r = sshbuf_get_cstring(resp, NULL, NULL)) != 0) { | ||
412 | error("%s: parse signature: %s", __func__, ssh_err(r)); | ||
413 | r = SSH_ERR_INVALID_FORMAT; | ||
414 | goto out; | ||
415 | } | ||
416 | if ((r = sshkey_private_deserialize(kbuf, &key)) != 0) { | ||
417 | error("Unable to parse private key: %s", ssh_err(r)); | ||
418 | goto out; | ||
419 | } | ||
420 | if ((tmp = recallocarray(keys, nkeys, nkeys + 1, | ||
421 | sizeof(*keys))) == NULL) { | ||
422 | error("%s: recallocarray keys failed", __func__); | ||
423 | goto out; | ||
424 | } | ||
425 | debug("%s: keys[%zu]: %s %s", __func__, | ||
426 | nkeys, sshkey_type(key), key->sk_application); | ||
427 | keys = tmp; | ||
428 | keys[nkeys++] = key; | ||
429 | key = NULL; | ||
430 | } | ||
431 | |||
432 | /* success */ | ||
433 | r = 0; | ||
434 | *keysp = keys; | ||
435 | *nkeysp = nkeys; | ||
436 | keys = NULL; | ||
437 | nkeys = 0; | ||
438 | out: | ||
439 | oerrno = errno; | ||
440 | for (i = 0; i < nkeys; i++) | ||
441 | sshkey_free(keys[i]); | ||
442 | free(keys); | ||
443 | sshkey_free(key); | ||
444 | sshbuf_free(kbuf); | ||
445 | sshbuf_free(req); | ||
446 | sshbuf_free(resp); | ||
447 | errno = oerrno; | ||
448 | return r; | ||
449 | } | ||