The Pedigree Project 0.1
user/applications/random-seed/main.cc
1/* Copyright (c) 2026, Pedigree Developers. See LICENSE. */
2#include "pedigree/kernel/utilities/SecureRandom.h"
3
4#include <errno.h>
5#include <fcntl.h>
6#include <stdio.h>
7#include <string.h>
8#include <unistd.h>
9
10#include <sys/file.h>
11#include <sys/ioctl.h>
12#include <sys/random.h>
13#include <sys/stat.h>
14
15namespace {
16constexpr unsigned long AddEntropy = 0x40085203UL;
17constexpr char DefaultSeed[] = "/var/lib/pedigree/random-seed";
18
19struct Secrets {
20 unsigned char saved[32] = {};
21 unsigned char next[32] = {};
22 struct {
23 int entropyBits = 256;
24 int bytes = 32;
25 unsigned char seed[32] = {};
26 } request;
27 ~Secrets() {
28 pedigree_random::erase(this, sizeof(*this));
29 }
30};
31
32int fail(const char* operation) {
33 fprintf(stderr, "random-seed: %s: %s; seed not activated\n", operation, strerror(errno));
34 return 1;
35}
36
37bool privateFile(int fd, bool directory = false) {
38 struct stat st;
39 if (fstat(fd, &st) != 0)
40 return false;
41 if (st.st_uid || (st.st_mode & 0777) != (directory ? 0700 : 0600) ||
42 (directory ? !S_ISDIR(st.st_mode) : (!S_ISREG(st.st_mode) || st.st_nlink != 1))) {
43 errno = EPERM;
44 return false;
45 }
46 return true;
47}
48
49bool transfer(int fd, unsigned char* bytes, size_t count, bool writing) {
50 size_t done = 0;
51 while (done < count) {
52 ssize_t n =
53 writing ? write(fd, bytes + done, count - done) : read(fd, bytes + done, count - done);
54 if (n < 0 && errno == EINTR)
55 continue;
56 if (n <= 0) {
57 if (!n)
58 errno = EIO;
59 return false;
60 }
61 done += static_cast<size_t>(n);
62 }
63 return true;
64}
65} // namespace
66
67int main(int argc, char** argv) {
68 if (argc == 2 && !strcmp(argv[1], "--check")) {
69 unsigned char bytes[32];
70 const bool ready = getrandom(bytes, sizeof(bytes), GRND_NONBLOCK) == sizeof(bytes);
71 pedigree_random::erase(bytes, sizeof(bytes));
72 puts(ready ? "Secure random generator is ready." : "Secure random generator is not seeded.");
73 return ready ? 0 : 1;
74 }
75 if (argc > 2 || (argc == 2 && argv[1][0] == '-')) {
76 fprintf(stderr, "Usage: random-seed [SEED_FILE] | --check\n");
77 return 2;
78 }
79 if (geteuid()) {
80 errno = EPERM;
81 return fail("requires root");
82 }
83 const char* path = argc == 2 ? argv[1] : DefaultSeed;
84 char parent[4096], name[256], temporary[272], lockname[272];
85 const char* slash = strrchr(path, '/');
86 if (!slash || slash == path || !slash[1] || static_cast<size_t>(slash - path) >= sizeof(parent) ||
87 strlen(slash + 1) >= sizeof(name)) {
88 errno = EINVAL;
89 return fail("seed path must have a private parent directory");
90 }
91 memcpy(parent, path, slash - path);
92 parent[slash - path] = 0;
93 strcpy(name, slash + 1);
94 snprintf(temporary, sizeof(temporary), "%s.next", name);
95 snprintf(lockname, sizeof(lockname), "%s.lock", name);
96 int directory = open(parent, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
97 if (directory < 0 || !privateFile(directory, true))
98 return fail("open private seed directory (root:root, mode 0700)");
99
100 // Keep this lock inode across boots. Locking the replaceable seed or its
101 // temporary pathname would permit two invocations to consume the same seed.
102 int lock = openat(directory, lockname, O_RDWR | O_CREAT | O_NOFOLLOW, 0600);
103 if (lock < 0 || !privateFile(lock) || flock(lock, LOCK_EX | LOCK_NB))
104 return fail("lock seed transaction");
105 int input = openat(directory, name, O_RDONLY | O_NOFOLLOW);
106 if (input < 0 || !privateFile(input))
107 return fail("open private seed file (root:root, mode 0600)");
108 Secrets secrets;
109 if (!transfer(input, secrets.saved, sizeof(secrets.saved), false))
110 return fail("read 32-byte seed");
111 unsigned char extra;
112 if (read(input, &extra, 1) != 0) {
113 errno = EINVAL;
114 return fail("seed must contain exactly 32 bytes");
115 }
116 if (close(input))
117 return fail("close seed input");
118 constexpr char KernelDomain[] = "Pedigree kernel seed v1";
119 constexpr char SavedDomain[] = "Pedigree saved seed v1";
120 pedigree_random::hmac_sha256(secrets.saved, KernelDomain, sizeof(KernelDomain) - 1,
121 secrets.request.seed);
122 pedigree_random::hmac_sha256(secrets.saved, SavedDomain, sizeof(SavedDomain) - 1, secrets.next);
123 pedigree_random::erase(secrets.saved, sizeof(secrets.saved));
124
125 // An interrupted transaction's .next file requires rescue recovery. Never
126 // guess whether a prior boot activated its seed or reuse a bundled default.
127 int output = openat(directory, temporary, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
128 if (output < 0)
129 return fail("create successor (recover a stale .next file from rescue Linux)");
130 if (!transfer(output, secrets.next, sizeof(secrets.next), true) || fsync(output) || close(output))
131 return fail("persist successor");
132 if (renameat(directory, temporary, directory, name) || fsync(directory))
133 return fail("persist seed replacement");
134
135 // No consumer may obtain bytes derived from this seed until its successor
136 // is durable. A crash after this point merely skips an unused generation.
137 int random = open("/dev/random", O_RDONLY);
138 if (random < 0 || ioctl(random, AddEntropy, &secrets.request))
139 return fail("activate kernel seed");
140 puts("random-seed: advanced saved seed and initialized secure randomness");
141 return 0;
142}