1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <linux/random.h>
6 #include <sys/ioctl.h>
7 #if USE_SYS_RANDOM_H
8 #  include <sys/random.h>
9 #endif
10 #include <sys/stat.h>
11 #include <sys/xattr.h>
12 #include <unistd.h>
13 
14 #include "sd-id128.h"
15 
16 #include "alloc-util.h"
17 #include "fd-util.h"
18 #include "fs-util.h"
19 #include "io-util.h"
20 #include "log.h"
21 #include "main-func.h"
22 #include "missing_random.h"
23 #include "missing_syscall.h"
24 #include "mkdir.h"
25 #include "parse-util.h"
26 #include "random-util.h"
27 #include "string-util.h"
28 #include "sync-util.h"
29 #include "sha256.h"
30 #include "util.h"
31 #include "xattr-util.h"
32 
33 typedef enum CreditEntropy {
34         CREDIT_ENTROPY_NO_WAY,
35         CREDIT_ENTROPY_YES_PLEASE,
36         CREDIT_ENTROPY_YES_FORCED,
37 } CreditEntropy;
38 
may_credit(int seed_fd)39 static CreditEntropy may_credit(int seed_fd) {
40         _cleanup_free_ char *creditable = NULL;
41         const char *e;
42         int r;
43 
44         assert(seed_fd >= 0);
45 
46         e = getenv("SYSTEMD_RANDOM_SEED_CREDIT");
47         if (!e) {
48                 log_debug("$SYSTEMD_RANDOM_SEED_CREDIT is not set, not crediting entropy.");
49                 return CREDIT_ENTROPY_NO_WAY;
50         }
51         if (streq(e, "force")) {
52                 log_debug("$SYSTEMD_RANDOM_SEED_CREDIT is set to 'force', crediting entropy.");
53                 return CREDIT_ENTROPY_YES_FORCED;
54         }
55 
56         r = parse_boolean(e);
57         if (r <= 0) {
58                 if (r < 0)
59                         log_warning_errno(r, "Failed to parse $SYSTEMD_RANDOM_SEED_CREDIT, not crediting entropy: %m");
60                 else
61                         log_debug("Crediting entropy is turned off via $SYSTEMD_RANDOM_SEED_CREDIT, not crediting entropy.");
62 
63                 return CREDIT_ENTROPY_NO_WAY;
64         }
65 
66         /* Determine if the file is marked as creditable */
67         r = fgetxattr_malloc(seed_fd, "user.random-seed-creditable", &creditable);
68         if (r < 0) {
69                 if (IN_SET(r, -ENODATA, -ENOSYS, -EOPNOTSUPP))
70                         log_debug_errno(r, "Seed file is not marked as creditable, not crediting.");
71                 else
72                         log_warning_errno(r, "Failed to read extended attribute, ignoring: %m");
73 
74                 return CREDIT_ENTROPY_NO_WAY;
75         }
76 
77         r = parse_boolean(creditable);
78         if (r <= 0) {
79                 if (r < 0)
80                         log_warning_errno(r, "Failed to parse user.random-seed-creditable extended attribute, ignoring: %s", creditable);
81                 else
82                         log_debug("Seed file is marked as not creditable, not crediting.");
83 
84                 return CREDIT_ENTROPY_NO_WAY;
85         }
86 
87         /* Don't credit the random seed if we are in first-boot mode, because we are supposed to start from
88          * scratch. This is a safety precaution for cases where we people ship "golden" images with empty
89          * /etc but populated /var that contains a random seed. */
90         if (access("/run/systemd/first-boot", F_OK) < 0) {
91 
92                 if (errno != ENOENT) {
93                         log_warning_errno(errno, "Failed to check whether we are in first-boot mode, not crediting entropy: %m");
94                         return CREDIT_ENTROPY_NO_WAY;
95                 }
96 
97                 /* If ENOENT all is good, we are not in first-boot mode. */
98         } else {
99                 log_debug("Not crediting entropy, since booted in first-boot mode.");
100                 return CREDIT_ENTROPY_NO_WAY;
101         }
102 
103         return CREDIT_ENTROPY_YES_PLEASE;
104 }
105 
run(int argc,char * argv[])106 static int run(int argc, char *argv[]) {
107         bool read_seed_file, write_seed_file, synchronous, hashed_old_seed = false;
108         _cleanup_close_ int seed_fd = -1, random_fd = -1;
109         _cleanup_free_ void* buf = NULL;
110         struct sha256_ctx hash_state;
111         size_t buf_size;
112         struct stat st;
113         ssize_t k, l;
114         int r;
115 
116         log_setup();
117 
118         if (argc != 2)
119                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
120                                        "This program requires one argument.");
121 
122         umask(0022);
123 
124         buf_size = random_pool_size();
125 
126         r = mkdir_parents(RANDOM_SEED, 0755);
127         if (r < 0)
128                 return log_error_errno(r, "Failed to create directory " RANDOM_SEED_DIR ": %m");
129 
130         /* When we load the seed we read it and write it to the device and then immediately update the saved seed with
131          * new data, to make sure the next boot gets seeded differently. */
132 
133         if (streq(argv[1], "load")) {
134 
135                 seed_fd = open(RANDOM_SEED, O_RDWR|O_CLOEXEC|O_NOCTTY|O_CREAT, 0600);
136                 if (seed_fd < 0) {
137                         int open_rw_error = -errno;
138 
139                         write_seed_file = false;
140 
141                         seed_fd = open(RANDOM_SEED, O_RDONLY|O_CLOEXEC|O_NOCTTY);
142                         if (seed_fd < 0) {
143                                 bool missing = errno == ENOENT;
144 
145                                 log_full_errno(missing ? LOG_DEBUG : LOG_ERR,
146                                                open_rw_error, "Failed to open " RANDOM_SEED " for writing: %m");
147                                 r = log_full_errno(missing ? LOG_DEBUG : LOG_ERR,
148                                                    errno, "Failed to open " RANDOM_SEED " for reading: %m");
149                                 return missing ? 0 : r;
150                         }
151                 } else
152                         write_seed_file = true;
153 
154                 random_fd = open("/dev/urandom", O_RDWR|O_CLOEXEC|O_NOCTTY, 0600);
155                 if (random_fd < 0)
156                         return log_error_errno(errno, "Failed to open /dev/urandom: %m");
157 
158                 read_seed_file = true;
159                 synchronous = true; /* make this invocation a synchronous barrier for random pool initialization */
160 
161         } else if (streq(argv[1], "save")) {
162 
163                 random_fd = open("/dev/urandom", O_RDONLY|O_CLOEXEC|O_NOCTTY);
164                 if (random_fd < 0)
165                         return log_error_errno(errno, "Failed to open /dev/urandom: %m");
166 
167                 seed_fd = open(RANDOM_SEED, O_WRONLY|O_CLOEXEC|O_NOCTTY|O_CREAT, 0600);
168                 if (seed_fd < 0)
169                         return log_error_errno(errno, "Failed to open " RANDOM_SEED ": %m");
170 
171                 read_seed_file = false;
172                 write_seed_file = true;
173                 synchronous = false;
174         } else
175                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
176                                        "Unknown verb '%s'.", argv[1]);
177 
178         if (fstat(seed_fd, &st) < 0)
179                 return log_error_errno(errno, "Failed to stat() seed file " RANDOM_SEED ": %m");
180 
181         /* If the seed file is larger than what we expect, then honour the existing size and save/restore as much as it says */
182         if ((uint64_t) st.st_size > buf_size)
183                 buf_size = MIN(st.st_size, RANDOM_POOL_SIZE_MAX);
184 
185         buf = malloc(buf_size);
186         if (!buf)
187                 return log_oom();
188 
189         if (read_seed_file) {
190                 sd_id128_t mid;
191 
192                 /* First, let's write the machine ID into /dev/urandom, not crediting entropy. Why? As an
193                  * extra protection against "golden images" that are put together sloppily, i.e. images which
194                  * are duplicated on multiple systems but where the random seed file is not properly
195                  * reset. Frequently the machine ID is properly reset on those systems however (simply
196                  * because it's easier to notice, if it isn't due to address clashes and so on, while random
197                  * seed equivalence is generally not noticed easily), hence let's simply write the machined
198                  * ID into the random pool too. */
199                 r = sd_id128_get_machine(&mid);
200                 if (r < 0)
201                         log_debug_errno(r, "Failed to get machine ID, ignoring: %m");
202                 else {
203                         r = loop_write(random_fd, &mid, sizeof(mid), false);
204                         if (r < 0)
205                                 log_debug_errno(r, "Failed to write machine ID to /dev/urandom, ignoring: %m");
206                 }
207 
208                 k = loop_read(seed_fd, buf, buf_size, false);
209                 if (k < 0)
210                         log_error_errno(k, "Failed to read seed from " RANDOM_SEED ": %m");
211                 else if (k == 0)
212                         log_debug("Seed file " RANDOM_SEED " not yet initialized, proceeding.");
213                 else {
214                         CreditEntropy lets_credit;
215 
216                         /* If we're going to later write out a seed file, initialize a hash state with
217                          * the contents of the seed file we just read, so that the new one can't regress
218                          * in entropy. */
219                         if (write_seed_file) {
220                                 sha256_init_ctx(&hash_state);
221                                 sha256_process_bytes(&k, sizeof(k), &hash_state); /* Hash length to distinguish from new seed. */
222                                 sha256_process_bytes(buf, k, &hash_state);
223                                 hashed_old_seed = true;
224                         }
225 
226                         (void) lseek(seed_fd, 0, SEEK_SET);
227 
228                         lets_credit = may_credit(seed_fd);
229 
230                         /* Before we credit or use the entropy, let's make sure to securely drop the
231                          * creditable xattr from the file, so that we never credit the same random seed
232                          * again. Note that further down we'll write a new seed again, and likely mark it as
233                          * credible again, hence this is just paranoia to close the short time window between
234                          * the time we upload the random seed into the kernel and download the new one from
235                          * it. */
236 
237                         if (fremovexattr(seed_fd, "user.random-seed-creditable") < 0) {
238                                 if (!IN_SET(errno, ENODATA, ENOSYS, EOPNOTSUPP))
239                                         log_warning_errno(errno, "Failed to remove extended attribute, ignoring: %m");
240 
241                                 /* Otherwise, there was no creditable flag set, which is OK. */
242                         } else {
243                                 r = fsync_full(seed_fd);
244                                 if (r < 0) {
245                                         log_warning_errno(r, "Failed to synchronize seed to disk, not crediting entropy: %m");
246 
247                                         if (lets_credit == CREDIT_ENTROPY_YES_PLEASE)
248                                                 lets_credit = CREDIT_ENTROPY_NO_WAY;
249                                 }
250                         }
251 
252                         r = random_write_entropy(random_fd, buf, k,
253                                                  IN_SET(lets_credit, CREDIT_ENTROPY_YES_PLEASE, CREDIT_ENTROPY_YES_FORCED));
254                         if (r < 0)
255                                 log_error_errno(r, "Failed to write seed to /dev/urandom: %m");
256                 }
257         }
258 
259         if (write_seed_file) {
260                 bool getrandom_worked = false;
261 
262                 /* This is just a safety measure. Given that we are root and most likely created the file
263                  * ourselves the mode and owner should be correct anyway. */
264                 r = fchmod_and_chown(seed_fd, 0600, 0, 0);
265                 if (r < 0)
266                         return log_error_errno(r, "Failed to adjust seed file ownership and access mode: %m");
267 
268                 /* Let's make this whole job asynchronous, i.e. let's make ourselves a barrier for
269                  * proper initialization of the random pool. */
270                 k = getrandom(buf, buf_size, GRND_NONBLOCK);
271                 if (k < 0 && errno == EAGAIN && synchronous) {
272                         log_notice("Kernel entropy pool is not initialized yet, waiting until it is.");
273                         k = getrandom(buf, buf_size, 0); /* retry synchronously */
274                 }
275                 if (k < 0)
276                         log_debug_errno(errno, "Failed to read random data with getrandom(), falling back to /dev/urandom: %m");
277                 else if ((size_t) k < buf_size)
278                         log_debug("Short read from getrandom(), falling back to /dev/urandom.");
279                 else
280                         getrandom_worked = true;
281 
282                 if (!getrandom_worked) {
283                         /* Retry with classic /dev/urandom */
284                         k = loop_read(random_fd, buf, buf_size, false);
285                         if (k < 0)
286                                 return log_error_errno(k, "Failed to read new seed from /dev/urandom: %m");
287                         if (k == 0)
288                                 return log_error_errno(SYNTHETIC_ERRNO(EIO),
289                                                        "Got EOF while reading from /dev/urandom.");
290                 }
291 
292                 /* If we previously read in a seed file, then hash the new seed into the old one,
293                  * and replace the last 32 bytes of the seed with the hash output, so that the
294                  * new seed file can't regress in entropy. */
295                 if (hashed_old_seed) {
296                         uint8_t hash[32];
297                         sha256_process_bytes(&k, sizeof(k), &hash_state); /* Hash length to distinguish from old seed. */
298                         sha256_process_bytes(buf, k, &hash_state);
299                         sha256_finish_ctx(&hash_state, hash);
300                         l = MIN((size_t)k, sizeof(hash));
301                         memcpy((uint8_t *)buf + k - l, hash, l);
302                 }
303 
304                 r = loop_write(seed_fd, buf, (size_t) k, false);
305                 if (r < 0)
306                         return log_error_errno(r, "Failed to write new random seed file: %m");
307 
308                 if (ftruncate(seed_fd, k) < 0)
309                         return log_error_errno(r, "Failed to truncate random seed file: %m");
310 
311                 r = fsync_full(seed_fd);
312                 if (r < 0)
313                         return log_error_errno(r, "Failed to synchronize seed file: %m");
314 
315                 /* If we got this random seed data from getrandom() the data is suitable for crediting
316                  * entropy later on. Let's keep that in mind by setting an extended attribute. on the file */
317                 if (getrandom_worked)
318                         if (fsetxattr(seed_fd, "user.random-seed-creditable", "1", 1, 0) < 0)
319                                 log_full_errno(ERRNO_IS_NOT_SUPPORTED(errno) ? LOG_DEBUG : LOG_WARNING, errno,
320                                                "Failed to mark seed file as creditable, ignoring: %m");
321         }
322 
323         return 0;
324 }
325 
326 DEFINE_MAIN_FUNCTION(run);
327