journalctl: verify sealed log epochs are continuous
Currently empty epochs are not sealed. This allows an attacker to truncate a sealed log and continue it without any problems showing when verifying the log. This partially addresses CVE-2023-31438. One way to extend this change to address CVE-2023-31438 completely, would be to verify that there is exactly one seal per epoch (and not sealing when the epoch has not ended yet). the change also adds a journal-file flag: HEADER_COMPATIBLE_SEALED_CONTINUOUS this flag indicates that a journal file is sealed continuously and decides whether any missing crypto epochs should trigger a warning or an error.
This commit is contained in:
@@ -677,6 +677,7 @@ static int server_archive_offline_user_journals(Server *s) {
|
||||
|
||||
TAKE_FD(fd); /* Donated to journal_file_open() */
|
||||
|
||||
journal_file_write_final_tag(f);
|
||||
r = journal_file_archive(f, NULL);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to archive journal file '%s', ignoring: %m", full);
|
||||
|
||||
@@ -45,8 +45,11 @@ int journal_file_append_tag(JournalFile *f) {
|
||||
if (!JOURNAL_HEADER_SEALED(f->header))
|
||||
return 0;
|
||||
|
||||
if (!f->hmac_running)
|
||||
return 0;
|
||||
if (!f->hmac_running) {
|
||||
r = journal_file_hmac_start(f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
assert(f->hmac);
|
||||
|
||||
@@ -166,6 +169,11 @@ int journal_file_fsprg_evolve(JournalFile *f, uint64_t realtime) {
|
||||
|
||||
FSPRG_Evolve(f->fsprg_state);
|
||||
epoch = FSPRG_GetEpoch(f->fsprg_state);
|
||||
if (epoch < goal) {
|
||||
r = journal_file_append_tag(f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,10 +192,12 @@ enum {
|
||||
enum {
|
||||
HEADER_COMPATIBLE_SEALED = 1 << 0,
|
||||
HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID = 1 << 1, /* if set, the last_entry_boot_id field in the header is exclusively refreshed when an entry is appended */
|
||||
HEADER_COMPATIBLE_ANY = HEADER_COMPATIBLE_SEALED|
|
||||
HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID,
|
||||
HEADER_COMPATIBLE_SEALED_CONTINUOUS = 1 << 2,
|
||||
HEADER_COMPATIBLE_ANY = HEADER_COMPATIBLE_SEALED |
|
||||
HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID |
|
||||
HEADER_COMPATIBLE_SEALED_CONTINUOUS,
|
||||
|
||||
HEADER_COMPATIBLE_SUPPORTED = (HAVE_GCRYPT ? HEADER_COMPATIBLE_SEALED : 0) |
|
||||
HEADER_COMPATIBLE_SUPPORTED = (HAVE_GCRYPT ? HEADER_COMPATIBLE_SEALED | HEADER_COMPATIBLE_SEALED_CONTINUOUS : 0) |
|
||||
HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID,
|
||||
};
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ static int journal_file_init_header(
|
||||
keyed_hash_requested() * HEADER_INCOMPATIBLE_KEYED_HASH |
|
||||
compact_mode_requested() * HEADER_INCOMPATIBLE_COMPACT),
|
||||
.compatible_flags = htole32(
|
||||
(seal * HEADER_COMPATIBLE_SEALED) |
|
||||
(seal * (HEADER_COMPATIBLE_SEALED | HEADER_COMPATIBLE_SEALED_CONTINUOUS) ) |
|
||||
HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID),
|
||||
};
|
||||
|
||||
@@ -487,6 +487,8 @@ static bool warn_wrong_flags(const JournalFile *f, bool compatible) {
|
||||
if (compatible) {
|
||||
if (flags & HEADER_COMPATIBLE_SEALED)
|
||||
strv[n++] = "sealed";
|
||||
if (flags & HEADER_COMPATIBLE_SEALED_CONTINUOUS)
|
||||
strv[n++] = "sealed-continuous";
|
||||
} else {
|
||||
if (flags & HEADER_INCOMPATIBLE_COMPRESSED_XZ)
|
||||
strv[n++] = "xz-compressed";
|
||||
@@ -3827,7 +3829,7 @@ void journal_file_print_header(JournalFile *f) {
|
||||
"Boot ID: %s\n"
|
||||
"Sequential number ID: %s\n"
|
||||
"State: %s\n"
|
||||
"Compatible flags:%s%s%s\n"
|
||||
"Compatible flags:%s%s%s%s\n"
|
||||
"Incompatible flags:%s%s%s%s%s%s\n"
|
||||
"Header size: %"PRIu64"\n"
|
||||
"Arena size: %"PRIu64"\n"
|
||||
@@ -3850,6 +3852,7 @@ void journal_file_print_header(JournalFile *f) {
|
||||
f->header->state == STATE_ONLINE ? "ONLINE" :
|
||||
f->header->state == STATE_ARCHIVED ? "ARCHIVED" : "UNKNOWN",
|
||||
JOURNAL_HEADER_SEALED(f->header) ? " SEALED" : "",
|
||||
JOURNAL_HEADER_SEALED_CONTINUOUS(f->header) ? " SEALED_CONTINUOUS" : "",
|
||||
JOURNAL_HEADER_TAIL_ENTRY_BOOT_ID(f->header) ? " TAIL_ENTRY_BOOT_ID" : "",
|
||||
(le32toh(f->header->compatible_flags) & ~HEADER_COMPATIBLE_ANY) ? " ???" : "",
|
||||
JOURNAL_HEADER_COMPRESSED_XZ(f->header) ? " COMPRESSED-XZ" : "",
|
||||
|
||||
@@ -189,6 +189,9 @@ static inline bool VALID_EPOCH(uint64_t u) {
|
||||
#define JOURNAL_HEADER_SEALED(h) \
|
||||
FLAGS_SET(le32toh((h)->compatible_flags), HEADER_COMPATIBLE_SEALED)
|
||||
|
||||
#define JOURNAL_HEADER_SEALED_CONTINUOUS(h) \
|
||||
FLAGS_SET(le32toh((h)->compatible_flags), HEADER_COMPATIBLE_SEALED_CONTINUOUS)
|
||||
|
||||
#define JOURNAL_HEADER_TAIL_ENTRY_BOOT_ID(h) \
|
||||
FLAGS_SET(le32toh((h)->compatible_flags), HEADER_COMPATIBLE_TAIL_ENTRY_BOOT_ID)
|
||||
|
||||
|
||||
@@ -816,7 +816,7 @@ int journal_file_verify(
|
||||
bool show_progress) {
|
||||
int r;
|
||||
Object *o;
|
||||
uint64_t p = 0, last_epoch = 0, last_tag_realtime = 0, last_sealed_realtime = 0;
|
||||
uint64_t p = 0, last_epoch = 0, last_tag_realtime = 0;
|
||||
|
||||
uint64_t entry_seqnum = 0, entry_monotonic = 0, entry_realtime = 0;
|
||||
usec_t min_entry_realtime = USEC_INFINITY, max_entry_realtime = 0;
|
||||
@@ -925,6 +925,10 @@ int journal_file_verify(
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!JOURNAL_HEADER_SEALED_CONTINUOUS(f->header))
|
||||
warning(p,
|
||||
"This log file was sealed with an old journald version where the sequence of seals might not be continuous. We cannot guarantee completeness.");
|
||||
|
||||
/* First iteration: we go through all objects, verify the
|
||||
* superficial structure, headers, hashes. */
|
||||
|
||||
@@ -1128,13 +1132,25 @@ int journal_file_verify(
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (le64toh(o->tag.epoch) < last_epoch) {
|
||||
error(p,
|
||||
"Epoch sequence out of synchronization (%"PRIu64" < %"PRIu64")",
|
||||
le64toh(o->tag.epoch),
|
||||
last_epoch);
|
||||
r = -EBADMSG;
|
||||
goto fail;
|
||||
if (JOURNAL_HEADER_SEALED_CONTINUOUS(f->header)) {
|
||||
if (!(n_tags == 0 || (n_tags == 1 && le64toh(o->tag.epoch) == last_epoch)
|
||||
|| le64toh(o->tag.epoch) == last_epoch + 1)) {
|
||||
error(p,
|
||||
"Epoch sequence not continuous (%"PRIu64" vs %"PRIu64")",
|
||||
le64toh(o->tag.epoch),
|
||||
last_epoch);
|
||||
r = -EBADMSG;
|
||||
goto fail;
|
||||
}
|
||||
} else {
|
||||
if (le64toh(o->tag.epoch) < last_epoch) {
|
||||
error(p,
|
||||
"Epoch sequence out of synchronization (%"PRIu64" < %"PRIu64")",
|
||||
le64toh(o->tag.epoch),
|
||||
last_epoch);
|
||||
r = -EBADMSG;
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_GCRYPT
|
||||
@@ -1216,7 +1232,6 @@ int journal_file_verify(
|
||||
|
||||
f->hmac_running = false;
|
||||
last_tag_realtime = rt;
|
||||
last_sealed_realtime = entry_realtime;
|
||||
}
|
||||
|
||||
last_tag = p + ALIGN64(le64toh(o->object.size));
|
||||
@@ -1389,8 +1404,10 @@ int journal_file_verify(
|
||||
|
||||
if (first_contained)
|
||||
*first_contained = le64toh(f->header->head_entry_realtime);
|
||||
#if HAVE_GCRYPT
|
||||
if (last_validated)
|
||||
*last_validated = last_sealed_realtime;
|
||||
*last_validated = last_tag_realtime + f->fss_interval_usec;
|
||||
#endif
|
||||
if (last_contained)
|
||||
*last_contained = le64toh(f->header->tail_entry_realtime);
|
||||
|
||||
|
||||
@@ -379,20 +379,23 @@ bool journal_file_is_offlining(JournalFile *f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void journal_file_write_final_tag(JournalFile *f) {
|
||||
assert(f);
|
||||
#if HAVE_GCRYPT
|
||||
if (!JOURNAL_HEADER_SEALED(f->header) || !journal_file_writable(f))
|
||||
return;
|
||||
|
||||
int r = journal_file_append_tag(f);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to append tag when closing journal: %m");
|
||||
#endif
|
||||
}
|
||||
|
||||
JournalFile* journal_file_offline_close(JournalFile *f) {
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
#if HAVE_GCRYPT
|
||||
/* Write the final tag */
|
||||
if (JOURNAL_HEADER_SEALED(f->header) && journal_file_writable(f)) {
|
||||
int r;
|
||||
|
||||
r = journal_file_append_tag(f);
|
||||
if (r < 0)
|
||||
log_error_errno(r, "Failed to append tag when closing journal: %m");
|
||||
}
|
||||
#endif
|
||||
journal_file_write_final_tag(f);
|
||||
|
||||
if (sd_event_source_get_enabled(f->post_change_timer, NULL) > 0)
|
||||
journal_file_post_change(f);
|
||||
@@ -435,6 +438,7 @@ int journal_file_rotate(
|
||||
assert(f);
|
||||
assert(*f);
|
||||
|
||||
journal_file_write_final_tag(*f);
|
||||
r = journal_file_archive(*f, &path);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
int journal_file_set_offline(JournalFile *f, bool wait);
|
||||
bool journal_file_is_offlining(JournalFile *f);
|
||||
void journal_file_write_final_tag(JournalFile *f);
|
||||
JournalFile* journal_file_offline_close(JournalFile *f);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(JournalFile*, journal_file_offline_close);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user