]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/import/import-util.c
import: make verification code generic, in preparation for using it pull-tar
[thirdparty/systemd.git] / src / import / import-util.c
CommitLineData
56ebfaf1
LP
1/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3/***
4 This file is part of systemd.
5
6 Copyright 2015 Lennart Poettering
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20***/
21
98c38001
LP
22#include <sys/prctl.h>
23
56ebfaf1
LP
24#include "util.h"
25#include "strv.h"
26#include "copy.h"
27#include "btrfs-util.h"
98c38001 28#include "import-job.h"
56ebfaf1
LP
29#include "import-util.h"
30
31#define FILENAME_ESCAPE "/.#\"\'"
32
33bool http_etag_is_valid(const char *etag) {
34 if (!endswith(etag, "\""))
35 return false;
36
37 if (!startswith(etag, "\"") && !startswith(etag, "W/\""))
38 return false;
39
40 return true;
41}
42
43int import_find_old_etags(const char *url, const char *image_root, int dt, const char *prefix, const char *suffix, char ***etags) {
44 _cleanup_free_ char *escaped_url = NULL;
45 _cleanup_closedir_ DIR *d = NULL;
46 _cleanup_strv_free_ char **l = NULL;
47 struct dirent *de;
48 int r;
49
50 assert(url);
51 assert(etags);
52
53 if (!image_root)
54 image_root = "/var/lib/machines";
55
56 escaped_url = xescape(url, FILENAME_ESCAPE);
57 if (!escaped_url)
58 return -ENOMEM;
59
60 d = opendir(image_root);
61 if (!d) {
62 if (errno == ENOENT) {
63 *etags = NULL;
64 return 0;
65 }
66
67 return -errno;
68 }
69
70 FOREACH_DIRENT_ALL(de, d, return -errno) {
71 const char *a, *b;
72 char *u;
73
74 if (de->d_type != DT_UNKNOWN &&
75 de->d_type != dt)
76 continue;
77
78 if (prefix) {
79 a = startswith(de->d_name, prefix);
80 if (!a)
81 continue;
82 } else
83 a = de->d_name;
84
85 a = startswith(a, escaped_url);
86 if (!a)
87 continue;
88
89 a = startswith(a, ".");
90 if (!a)
91 continue;
92
93 if (suffix) {
94 b = endswith(de->d_name, suffix);
95 if (!b)
96 continue;
97 } else
98 b = strchr(de->d_name, 0);
99
100 if (a >= b)
101 continue;
102
103 u = cunescape_length(a, b - a);
104 if (!u)
105 return -ENOMEM;
106
107 if (!http_etag_is_valid(u)) {
108 free(u);
109 continue;
110 }
111
112 r = strv_consume(&l, u);
113 if (r < 0)
114 return r;
115 }
116
117 *etags = l;
118 l = NULL;
119
120 return 0;
121}
122
123int import_make_local_copy(const char *final, const char *image_root, const char *local, bool force_local) {
124 const char *p;
125 int r;
126
127 assert(final);
128 assert(local);
129
130 if (!image_root)
131 image_root = "/var/lib/machines";
132
133 p = strappenda(image_root, "/", local);
134
135 if (force_local) {
136 (void) btrfs_subvol_remove(p);
137 (void) rm_rf_dangerous(p, false, true, false);
138 }
139
140 r = btrfs_subvol_snapshot(final, p, false, false);
141 if (r == -ENOTTY) {
142 r = copy_tree(final, p, false);
143 if (r < 0)
144 return log_error_errno(r, "Failed to copy image: %m");
145 } else if (r < 0)
146 return log_error_errno(r, "Failed to create local image: %m");
147
148 log_info("Created new local image '%s'.", local);
149
150 return 0;
151}
152
0d6e763b 153int import_make_read_only_fd(int fd) {
56ebfaf1
LP
154 int r;
155
0d6e763b
LP
156 assert(fd >= 0);
157
158 /* First, let's make this a read-only subvolume if it refers
159 * to a subvolume */
160 r = btrfs_subvol_set_read_only_fd(fd, true);
161 if (r == -ENOTTY || r == -ENOTDIR || r == -EINVAL) {
56ebfaf1
LP
162 struct stat st;
163
0d6e763b
LP
164 /* This doesn't refer to a subvolume, or the file
165 * system isn't even btrfs. In that, case fall back to
166 * chmod()ing */
167
168 r = fstat(fd, &st);
56ebfaf1
LP
169 if (r < 0)
170 return log_error_errno(errno, "Failed to stat temporary image: %m");
171
0d6e763b
LP
172 /* Drop "w" flag */
173 if (fchmod(fd, st.st_mode & 07555) < 0)
56ebfaf1
LP
174 return log_error_errno(errno, "Failed to chmod() final image: %m");
175
176 return 0;
0d6e763b
LP
177
178 } else if (r < 0)
179 return log_error_errno(r, "Failed to make subvolume read-only: %m");
56ebfaf1
LP
180
181 return 0;
182}
183
0d6e763b
LP
184int import_make_read_only(const char *path) {
185 _cleanup_close_ int fd = 1;
186
187 fd = open(path, O_RDONLY|O_NOCTTY|O_CLOEXEC);
188 if (fd < 0)
189 return log_error_errno(errno, "Failed to open %s: %m", path);
190
191 return import_make_read_only_fd(fd);
192}
193
56ebfaf1
LP
194int import_make_path(const char *url, const char *etag, const char *image_root, const char *prefix, const char *suffix, char **ret) {
195 _cleanup_free_ char *escaped_url = NULL;
196 char *path;
197
198 assert(url);
199 assert(ret);
200
201 if (!image_root)
202 image_root = "/var/lib/machines";
203
204 escaped_url = xescape(url, FILENAME_ESCAPE);
205 if (!escaped_url)
206 return -ENOMEM;
207
208 if (etag) {
209 _cleanup_free_ char *escaped_etag = NULL;
210
211 escaped_etag = xescape(etag, FILENAME_ESCAPE);
212 if (!escaped_etag)
213 return -ENOMEM;
214
215 path = strjoin(image_root, "/", strempty(prefix), escaped_url, ".", escaped_etag, strempty(suffix), NULL);
216 } else
217 path = strjoin(image_root, "/", strempty(prefix), escaped_url, strempty(suffix), NULL);
218 if (!path)
219 return -ENOMEM;
220
221 *ret = path;
222 return 0;
223}
85dbc41d
LP
224
225int import_url_last_component(const char *url, char **ret) {
226 const char *e, *p;
227 char *s;
228
229 e = strchrnul(url, '?');
230
231 while (e > url && e[-1] == '/')
232 e--;
233
234 p = e;
235 while (p > url && p[-1] != '/')
236 p--;
237
238 if (e <= p)
239 return -EINVAL;
240
241 s = strndup(p, e - p);
242 if (!s)
243 return -ENOMEM;
244
245 *ret = s;
246 return 0;
247}
248
249
250int import_url_change_last_component(const char *url, const char *suffix, char **ret) {
251 const char *e;
252 char *s;
253
254 assert(url);
255 assert(ret);
256
257 e = strchrnul(url, '?');
258
259 while (e > url && e[-1] == '/')
260 e--;
261
262 while (e > url && e[-1] != '/')
263 e--;
264
265 if (e <= url)
266 return -EINVAL;
267
268 s = new(char, (e - url) + strlen(suffix) + 1);
269 if (!s)
270 return -ENOMEM;
271
272 strcpy(mempcpy(s, url, e - url), suffix);
273 *ret = s;
274 return 0;
275}
8f695058
LP
276
277static const char* const import_verify_table[_IMPORT_VERIFY_MAX] = {
278 [IMPORT_VERIFY_NO] = "no",
279 [IMPORT_VERIFY_SUM] = "sum",
280 [IMPORT_VERIFY_SIGNATURE] = "signature",
281};
282
283DEFINE_STRING_TABLE_LOOKUP(import_verify, ImportVerify);
98c38001
LP
284
285int import_make_verification_jobs(
286 ImportJob **ret_checksum_job,
287 ImportJob **ret_signature_job,
288 ImportVerify verify,
289 const char *url,
290 CurlGlue *glue,
291 ImportJobFinished on_finished,
292 void *userdata) {
293
294 _cleanup_(import_job_unrefp) ImportJob *checksum_job = NULL, *signature_job = NULL;
295 int r;
296
297 assert(ret_checksum_job);
298 assert(ret_signature_job);
299 assert(verify >= 0);
300 assert(verify < _IMPORT_VERIFY_MAX);
301 assert(url);
302 assert(glue);
303
304 if (verify != IMPORT_VERIFY_NO) {
305 _cleanup_free_ char *checksum_url = NULL;
306
307 /* Queue job for the SHA256SUMS file for the image */
308 r = import_url_change_last_component(url, "SHA256SUMS", &checksum_url);
309 if (r < 0)
310 return r;
311
312 r = import_job_new(&checksum_job, checksum_url, glue, userdata);
313 if (r < 0)
314 return r;
315
316 checksum_job->on_finished = on_finished;
317 checksum_job->uncompressed_max = checksum_job->compressed_max = 1ULL * 1024ULL * 1024ULL;
318 }
319
320 if (verify == IMPORT_VERIFY_SIGNATURE) {
321 _cleanup_free_ char *signature_url = NULL;
322
323 /* Queue job for the SHA256SUMS.gpg file for the image. */
324 r = import_url_change_last_component(url, "SHA256SUMS.gpg", &signature_url);
325 if (r < 0)
326 return r;
327
328 r = import_job_new(&signature_job, signature_url, glue, userdata);
329 if (r < 0)
330 return r;
331
332 signature_job->on_finished = on_finished;
333 signature_job->uncompressed_max = signature_job->compressed_max = 1ULL * 1024ULL * 1024ULL;
334 }
335
336 *ret_checksum_job = checksum_job;
337 *ret_signature_job = signature_job;
338
339 checksum_job = signature_job = NULL;
340
341 return 0;
342}
343
344int import_verify(
345 ImportJob *main_job,
346 ImportJob *checksum_job,
347 ImportJob *signature_job) {
348
349 _cleanup_close_pair_ int gpg_pipe[2] = { -1, -1 };
350 _cleanup_free_ char *fn = NULL;
351 _cleanup_close_ int sig_file = -1;
352 const char *p, *line;
353 char sig_file_path[] = "/tmp/sigXXXXXX";
354 _cleanup_sigkill_wait_ pid_t pid = 0;
355 int r;
356
357 assert(main_job);
358 assert(main_job->state == IMPORT_JOB_DONE);
359
360 if (!checksum_job)
361 return 0;
362
363 assert(main_job->calc_checksum);
364 assert(main_job->checksum);
365 assert(checksum_job->state == IMPORT_JOB_DONE);
366
367 if (!checksum_job->payload || checksum_job->payload_size <= 0) {
368 log_error("Checksum is empty, cannot verify.");
369 return -EBADMSG;
370 }
371
372 r = import_url_last_component(main_job->url, &fn);
373 if (r < 0)
374 return log_oom();
375
376 if (!filename_is_valid(fn)) {
377 log_error("Cannot verify checksum, could not determine valid server-side file name.");
378 return -EBADMSG;
379 }
380
381 line = strappenda(main_job->checksum, " *", fn, "\n");
382
383 p = memmem(checksum_job->payload,
384 checksum_job->payload_size,
385 line,
386 strlen(line));
387
388 if (!p || (p != (char*) checksum_job->payload && p[-1] != '\n')) {
389 log_error("Checksum did not check out, payload has been tempered with.");
390 return -EBADMSG;
391 }
392
393 log_info("SHA256 checksum of %s is valid.", main_job->url);
394
395 if (!signature_job)
396 return 0;
397
398 assert(signature_job->state == IMPORT_JOB_DONE);
399
400 if (!signature_job->payload || signature_job->payload_size <= 0) {
401 log_error("Signature is empty, cannot verify.");
402 return -EBADMSG;
403 }
404
405 r = pipe2(gpg_pipe, O_CLOEXEC);
406 if (r < 0)
407 return log_error_errno(errno, "Failed to create pipe: %m");
408
409 sig_file = mkostemp(sig_file_path, O_RDWR);
410 if (sig_file < 0)
411 return log_error_errno(errno, "Failed to create temporary file: %m");
412
413 r = loop_write(sig_file, signature_job->payload, signature_job->payload_size, false);
414 if (r < 0) {
415 log_error_errno(r, "Failed to write to temporary file: %m");
416 goto finish;
417 }
418
419 pid = fork();
420 if (pid < 0)
421 return log_error_errno(errno, "Failed to fork off gpg: %m");
422 if (pid == 0) {
423 const char *cmd[] = {
424 "gpg",
425 "--no-options",
426 "--no-default-keyring",
427 "--no-auto-key-locate",
428 "--no-auto-check-trustdb",
429 "--batch",
430 "--trust-model=always",
431 "--keyring=" VENDOR_KEYRING_PATH,
432 NULL, /* maybe user keyring */
433 NULL, /* --verify */
434 NULL, /* signature file */
435 NULL, /* dash */
436 NULL /* trailing NULL */
437 };
438 unsigned k = ELEMENTSOF(cmd) - 5;
439 int null_fd;
440
441 /* Child */
442
443 reset_all_signal_handlers();
444 reset_signal_mask();
445 assert_se(prctl(PR_SET_PDEATHSIG, SIGTERM) == 0);
446
447 gpg_pipe[1] = safe_close(gpg_pipe[1]);
448
449 if (dup2(gpg_pipe[0], STDIN_FILENO) != STDIN_FILENO) {
450 log_error_errno(errno, "Failed to dup2() fd: %m");
451 _exit(EXIT_FAILURE);
452 }
453
454 if (gpg_pipe[0] != STDIN_FILENO)
455 gpg_pipe[0] = safe_close(gpg_pipe[0]);
456
457 null_fd = open("/dev/null", O_WRONLY|O_NOCTTY);
458 if (null_fd < 0) {
459 log_error_errno(errno, "Failed to open /dev/null: %m");
460 _exit(EXIT_FAILURE);
461 }
462
463 if (dup2(null_fd, STDOUT_FILENO) != STDOUT_FILENO) {
464 log_error_errno(errno, "Failed to dup2() fd: %m");
465 _exit(EXIT_FAILURE);
466 }
467
468 if (null_fd != STDOUT_FILENO)
469 null_fd = safe_close(null_fd);
470
471 /* We add the user keyring only to the command line
472 * arguments, if it's around since gpg fails
473 * otherwise. */
474 if (access(USER_KEYRING_PATH, F_OK) >= 0)
475 cmd[k++] = "--keyring=" USER_KEYRING_PATH;
476
477 cmd[k++] = "--verify";
478 cmd[k++] = sig_file_path;
479 cmd[k++] = "-";
480 cmd[k++] = NULL;
481
482 execvp("gpg", (char * const *) cmd);
483 log_error_errno(errno, "Failed to execute gpg: %m");
484 _exit(EXIT_FAILURE);
485 }
486
487 gpg_pipe[0] = safe_close(gpg_pipe[0]);
488
489 r = loop_write(gpg_pipe[1], checksum_job->payload, checksum_job->payload_size, false);
490 if (r < 0) {
491 log_error_errno(r, "Failed to write to pipe: %m");
492 goto finish;
493 }
494
495 gpg_pipe[1] = safe_close(gpg_pipe[1]);
496
497 r = wait_for_terminate_and_warn("gpg", pid, true);
498 pid = 0;
499 if (r < 0)
500 goto finish;
501 if (r > 0) {
502 log_error("Signature verification failed.");
503 r = -EBADMSG;
504 } else {
505 log_info("Signature verification succeeded.");
506 r = 0;
507 }
508
509finish:
510 if (sig_file >= 0)
511 unlink(sig_file_path);
512
513 return r;
514}