Skip to content

Commit 00213f8

Browse files
committed
Enforce RPM SHA256 floor during trust lookup
rpm_sha256_only only filtered RPM trust data while it was imported. If a weak RPM digest was already present in the active LMDB trust database, digest-based integrity lookup could still evaluate that stale SHA1 or MD5 record after the option was enabled. Snapshot rpm_sha256_only into the immutable decision config and have trust lookup skip sub-SHA256 records for SHA256 and IMA integrity modes when the floor is active. SHA1/MD5 RPM records remain usable when rpm_sha256_only is disabled, and strict mode can still trust a SHA256-or-stronger duplicate for the same path. Add decision-config coverage for the new pinned field and an LMDB regression that imports a SHA1 RPM record, verifies compatibility-mode trust, verifies strict-mode rejection without rebuilding the DB, and verifies trust again after adding a SHA256 duplicate. Update the manpage to document the ingestion and lookup-side behavior.
1 parent ee0891b commit 00213f8

6 files changed

Lines changed: 191 additions & 45 deletions

File tree

doc/fapolicyd.conf.5

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,17 @@ Example:
154154

155155
.TP
156156
.B rpm_sha256_only
157-
When this option is set to 1, it will force the daemon to work only with SHA256 and larger hashes. This is useful on the systems where the integrity is set to SHA256 or IMA and some rpms were originally built with e.g. SHA1. The daemon will ignore these SHA1 entries. If set to 0 the daemon stores SHA1/MD5 in trustdb as well. This is compatible with older behavior which works with the integrity set to NONE and SIZE. The NONE or SIZE integrity setting considers the files installed via rpm as trusted and it does not care about their hashes at all. On the other hand the integrity set to SHA256 or IMA will never consider a file with SHA1 in trustdb as trusted. The default value is 0.
157+
When this option is set to 1, it will force RPM trust entries to use SHA256
158+
or larger hashes. This is useful on systems where the integrity is set to
159+
SHA256 or IMA and some rpms were originally built with e.g. SHA1. The daemon
160+
will ignore these SHA1 entries when the RPM backend imports trust data. Hash
161+
integrity lookups also reject SHA1/MD5 records that are already present in the
162+
active trust database, so stale weak RPM entries do not remain trusted after a
163+
configuration reload enables this option. If set to 0 the daemon stores and
164+
uses SHA1/MD5 from RPM trust data as well. This is compatible with older
165+
behavior which works with the integrity set to NONE and SIZE. The NONE or SIZE
166+
integrity setting considers the files installed via rpm as trusted and it does
167+
not care about their hashes at all. The default value is 0.
158168

159169
.TP
160170
.B allow_filesystem_mark
@@ -239,17 +249,18 @@ The following
239249
settings are live after a successful reload:
240250
.RS
241251
.IP \[bu] 2
242-
\fBpermissive\fP and \fBintegrity\fP are decision-used fields and are
243-
published together as one immutable generation.
252+
\fBpermissive\fP, \fBintegrity\fP, and the lookup-side
253+
\fBrpm_sha256_only\fP digest floor are decision-used fields and are published
254+
together as one immutable generation.
244255
.IP \[bu]
245256
\fBnice_val\fP, \fBdo_stat_report\fP, \fBdetailed_report\fP,
246257
\fBreset_strategy\fP, and \fBtiming_collection\fP are applied directly by the
247258
reload path.
248259
.IP \[bu]
249260
\fBsyslog_format\fP is used by the next successfully published ruleset.
250261
.IP \[bu]
251-
\fBtrust\fP and \fBrpm_sha256_only\fP are used by the next trust database
252-
reload.
262+
\fBtrust\fP and the \fBrpm_sha256_only\fP RPM ingestion filter are used by the
263+
next trust database reload.
253264
.RE
254265
.PP
255266
The

src/library/database.c

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4748,6 +4748,7 @@ static int read_trust_db(struct trust_db_read_handle *read,
47484748
{
47494749
int do_integrity = 0, mode = READ_TEST_KEY;
47504750
integrity_t integrity = decision_config_integrity(NULL);
4751+
unsigned int rpm_sha256_only = decision_config_rpm_sha256_only(NULL);
47514752
char *res;
47524753
int retry = 0;
47534754
char sha_xattr[FILE_DIGEST_STRING_MAX];
@@ -4757,11 +4758,16 @@ static int read_trust_db(struct trust_db_read_handle *read,
47574758
struct file_info *info = lookup->info;
47584759
int fd = lookup->fd;
47594760
int *error = lookup->error;
4761+
file_hash_alg_t calc_alg = FILE_HASH_ALG_NONE;
4762+
file_hash_alg_t ima_alg = FILE_HASH_ALG_NONE;
4763+
int have_ima_hash = 0;
4764+
int ima_rehash_attempted = 0;
47604765

47614766
if (integrity != IN_NONE && info) {
47624767
do_integrity = 1;
47634768
mode = READ_DATA;
47644769
sha_xattr[0] = 0; // Make sure we can't re-use stack value
4770+
calc_digest[0] = 0;
47654771
}
47664772

47674773
retry_res:
@@ -4801,6 +4807,17 @@ static int read_trust_db(struct trust_db_read_handle *read,
48014807
// prepare for next reading
48024808
mode = READ_DATA_DUP;
48034809

4810+
/*
4811+
* rpm_sha256_only is enforced during RPM ingestion, but a stale
4812+
* LMDB generation can still contain old SHA1/MD5 records after
4813+
* a config change or failed rebuild. Digest-based integrity
4814+
* modes must not trust those records when the floor is enabled.
4815+
*/
4816+
if (rpm_sha256_only &&
4817+
(integrity == IN_SHA256 || integrity == IN_IMA) &&
4818+
file_hash_length(record.alg) < SHA256_LEN)
4819+
goto retry_res;
4820+
48044821
if (integrity == IN_SIZE) {
48054822

48064823
// match!
@@ -4813,23 +4830,29 @@ static int read_trust_db(struct trust_db_read_handle *read,
48134830
} else if (integrity == IN_IMA) {
48144831
int rc = 1;
48154832
char *hash = NULL;
4816-
file_hash_alg_t ima_alg = FILE_HASH_ALG_NONE;
48174833

4818-
// read xattr only the first time
4819-
if (retry == 1)
4834+
// read xattr only once, even when weak records are skipped
4835+
if (!have_ima_hash) {
48204836
rc = get_ima_hash(fd, &ima_alg, sha_xattr);
4837+
if (rc)
4838+
have_ima_hash = 1;
4839+
}
48214840

4822-
if (rc) {
4823-
if ((record.size == info->size) &&
4824-
(strcmp(record.digest,
4825-
sha_xattr) == 0)) {
4826-
file_info_cache_digest(info, ima_alg);
4827-
strncpy(info->digest, sha_xattr,
4828-
FILE_DIGEST_STRING_MAX-1);
4829-
info->digest[FILE_DIGEST_STRING_MAX-1]=0;
4830-
return 1;
4831-
} else if (retry == 1 &&
4832-
ima_alg != FILE_HASH_ALG_NONE) {
4841+
if (!have_ima_hash) {
4842+
*error = 1;
4843+
return 0;
4844+
}
4845+
4846+
if ((record.size == info->size) &&
4847+
(strcmp(record.digest, sha_xattr) == 0)) {
4848+
file_info_cache_digest(info, ima_alg);
4849+
strncpy(info->digest, sha_xattr,
4850+
FILE_DIGEST_STRING_MAX - 1);
4851+
info->digest[FILE_DIGEST_STRING_MAX - 1] = 0;
4852+
return 1;
4853+
} else if (!ima_rehash_attempted &&
4854+
ima_alg != FILE_HASH_ALG_NONE) {
4855+
ima_rehash_attempted = 1;
48334856
/*
48344857
* Rehash using the IMA algorithm to separate
48354858
* metadata drift from content changes. This maps
@@ -4840,30 +4863,25 @@ static int read_trust_db(struct trust_db_read_handle *read,
48404863
hash = get_hash_from_fd2(fd, info->size, ima_alg);
48414864
if (hash) {
48424865
strncpy(calc_digest, hash,
4843-
FILE_DIGEST_STRING_MAX-1);
4844-
calc_digest[FILE_DIGEST_STRING_MAX-1]=0;
4866+
FILE_DIGEST_STRING_MAX - 1);
4867+
calc_digest[FILE_DIGEST_STRING_MAX - 1] = 0;
48454868
free(hash);
48464869
file_info_cache_digest(info, ima_alg);
48474870
strncpy(info->digest, calc_digest,
4848-
FILE_DIGEST_STRING_MAX-1);
4849-
info->digest[FILE_DIGEST_STRING_MAX-1]=0;
4871+
FILE_DIGEST_STRING_MAX - 1);
4872+
info->digest[FILE_DIGEST_STRING_MAX - 1] = 0;
48504873
if ((record.size == info->size) &&
4851-
(strcmp(record.digest, calc_digest)==0))
4874+
(strcmp(record.digest, calc_digest) == 0))
48524875
return 1;
48534876
} else {
48544877
*error = 1;
48554878
return 0;
48564879
}
4857-
}
4858-
4859-
log_ima_mismatch(path, record.alg, ima_alg);
4860-
goto retry_res;
4861-
4862-
} else {
4863-
*error = 1;
4864-
return 0;
48654880
}
48664881

4882+
log_ima_mismatch(path, record.alg, ima_alg);
4883+
goto retry_res;
4884+
48674885
} else if (integrity == IN_SHA256) {
48684886
/*
48694887
* The name is historical; recomputation follows the
@@ -4875,22 +4893,22 @@ static int read_trust_db(struct trust_db_read_handle *read,
48754893

48764894
char *hash = NULL;
48774895

4878-
// Calculate a hash only one time
4879-
if (retry == 1) {
4896+
// Calculate once per digest algorithm seen in duplicates.
4897+
if (calc_alg != record.alg) {
48804898
hash = get_hash_from_fd2(fd, info->size,
48814899
record.alg);
48824900
if (hash) {
48834901
strncpy(calc_digest, hash,
4884-
FILE_DIGEST_STRING_MAX-1);
4885-
calc_digest[FILE_DIGEST_STRING_MAX-1]=0;
4902+
FILE_DIGEST_STRING_MAX - 1);
4903+
calc_digest[FILE_DIGEST_STRING_MAX - 1] = 0;
48864904
if (digest_len < FILE_DIGEST_STRING_MAX)
48874905
calc_digest[digest_len] = 0;
48884906
free(hash);
4889-
file_info_cache_digest(info,
4890-
record.alg);
4907+
file_info_cache_digest(info, record.alg);
48914908
strncpy(info->digest, calc_digest,
4892-
FILE_DIGEST_STRING_MAX-1);
4893-
info->digest[FILE_DIGEST_STRING_MAX-1] = 0;
4909+
FILE_DIGEST_STRING_MAX - 1);
4910+
info->digest[FILE_DIGEST_STRING_MAX - 1] = 0;
4911+
calc_alg = record.alg;
48944912
} else {
48954913
*error = 1;
48964914
return 0;

src/library/decision-config.c

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
* A permission event pins one generation before event construction and keeps
2525
* using it through trust checks and the final fanotify response. Reloads
2626
* publish a new object instead of mutating the active one, so a decision never
27-
* observes permissive mode from one reload and integrity mode from another.
27+
* observes permissive mode from one reload and digest policy from another.
2828
*/
2929
struct decision_config {
3030
unsigned int permissive;
3131
integrity_t integrity;
32+
unsigned int rpm_sha256_only;
3233
unsigned int generation;
3334
time_t effective_since;
3435
struct decision_config *previous;
@@ -37,6 +38,7 @@ struct decision_config {
3738
static struct decision_config default_decision_config = {
3839
.permissive = 0,
3940
.integrity = IN_NONE,
41+
.rpm_sha256_only = 0,
4042
.generation = 0,
4143
.effective_since = 0,
4244
.previous = NULL,
@@ -79,6 +81,7 @@ int decision_config_publish(const conf_t *config)
7981

8082
snapshot->permissive = config->permissive ? 1 : 0;
8183
snapshot->integrity = config->integrity;
84+
snapshot->rpm_sha256_only = config->rpm_sha256_only ? 1 : 0;
8285
now = time(NULL);
8386
snapshot->effective_since = now == (time_t)-1 ? 0 : now;
8487

@@ -163,6 +166,19 @@ integrity_t decision_config_integrity(const struct decision_config *config)
163166
return config->integrity;
164167
}
165168

169+
/*
170+
* decision_config_rpm_sha256_only - return the active RPM digest floor flag.
171+
* @config: config generation to inspect, or NULL for the current generation.
172+
* Returns 1 when RPM trust records must be SHA256 or stronger, otherwise 0.
173+
*/
174+
unsigned int decision_config_rpm_sha256_only(
175+
const struct decision_config *config)
176+
{
177+
if (config == NULL)
178+
config = decision_config_current();
179+
return config->rpm_sha256_only;
180+
}
181+
166182
unsigned int decision_config_active_generation(void)
167183
{
168184
return decision_config_generation(decision_config_active());

src/library/decision-config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ unsigned int decision_config_generation(const struct decision_config *config);
2626
time_t decision_config_effective_since(const struct decision_config *config);
2727
unsigned int decision_config_permissive(const struct decision_config *config);
2828
integrity_t decision_config_integrity(const struct decision_config *config);
29+
unsigned int decision_config_rpm_sha256_only(
30+
const struct decision_config *config);
2931
unsigned int decision_config_active_generation(void);
3032
time_t decision_config_active_effective_since(void);
3133
void decision_config_destroy(void);

src/tests/decision_config_test.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ int main(void)
4040
"[ERROR:4] pinned permissive mode is wrong");
4141
CHECK(decision_config_integrity(pinned) == IN_NONE, 5,
4242
"[ERROR:5] pinned integrity mode is wrong");
43+
CHECK(decision_config_rpm_sha256_only(pinned) == 0, 15,
44+
"[ERROR:15] pinned rpm_sha256_only flag is wrong");
4345

4446
config.permissive = 1;
4547
config.integrity = IN_SHA256;
48+
config.rpm_sha256_only = 1;
4649
CHECK(decision_config_publish(&config) == 0, 6,
4750
"[ERROR:6] second publish failed");
4851
second_generation = decision_config_active_generation();
@@ -55,6 +58,8 @@ int main(void)
5558
"[ERROR:9] pinned permissive mode was mutated");
5659
CHECK(decision_config_integrity(NULL) == IN_NONE, 10,
5760
"[ERROR:10] pinned integrity mode was mutated");
61+
CHECK(decision_config_rpm_sha256_only(NULL) == 0, 16,
62+
"[ERROR:16] pinned rpm_sha256_only flag was mutated");
5863

5964
decision_config_unpin(pinned);
6065
CHECK(decision_config_generation(NULL) == second_generation, 11,
@@ -63,6 +68,8 @@ int main(void)
6368
"[ERROR:12] active permissive mode is wrong");
6469
CHECK(decision_config_integrity(NULL) == IN_SHA256, 13,
6570
"[ERROR:13] active integrity mode is wrong");
71+
CHECK(decision_config_rpm_sha256_only(NULL) == 1, 17,
72+
"[ERROR:17] active rpm_sha256_only flag is wrong");
6673

6774
decision_config_destroy();
6875
CHECK(decision_config_generation(NULL) == 0, 14,

0 commit comments

Comments
 (0)