From d452335aa47fb1f1b11dc75bc462697431e64af3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 30 Nov 2022 18:39:45 +0100 Subject: [PATCH] dissect: add image dissection policy framework --- src/shared/image-policy.c | 679 +++++++++++++++++++++++++++++++++++ src/shared/image-policy.h | 96 +++++ src/shared/meson.build | 1 + src/test/meson.build | 1 + src/test/test-image-policy.c | 121 +++++++ 5 files changed, 898 insertions(+) create mode 100644 src/shared/image-policy.c create mode 100644 src/shared/image-policy.h create mode 100644 src/test/test-image-policy.c diff --git a/src/shared/image-policy.c b/src/shared/image-policy.c new file mode 100644 index 00000000000..98c58c09010 --- /dev/null +++ b/src/shared/image-policy.c @@ -0,0 +1,679 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "image-policy.h" +#include "logarithm.h" +#include "sort-util.h" +#include "string-util.h" +#include "strv.h" + +/* Rationale for the chosen syntax: + * + * → one line, so that it can be reasonably added to a shell command line, for example via `systemd-dissect + * --image-policy=…` or to the kernel command line via `systemd.image_policy=`. + * + * → no use of "," or ";" as separators, so that it can be included in mount/fstab-style option strings and + * doesn't require escaping. Instead, separators are ":", "=", "+" which should be fine both in shell + * command lines and in mount/fstab style option strings. + */ + +static int partition_policy_compare(const PartitionPolicy *a, const PartitionPolicy *b) { + return CMP(ASSERT_PTR(a)->designator, ASSERT_PTR(b)->designator); +} + +static PartitionPolicy* image_policy_bsearch(const ImagePolicy *policy, PartitionDesignator designator) { + if (!policy) + return NULL; + + return typesafe_bsearch( + &(PartitionPolicy) { .designator = designator }, + ASSERT_PTR(policy)->policies, + ASSERT_PTR(policy)->n_policies, + partition_policy_compare); +} + +static PartitionPolicyFlags partition_policy_normalized_flags(const PartitionPolicy *policy) { + PartitionPolicyFlags flags = ASSERT_PTR(policy)->flags; + + /* This normalizes the per-partition policy flags. This means if the user left some things + * unspecified, we'll fill in the appropriate "dontcare" policy instead. We'll also mask out bits + * that do not make any sense for specific partition types. */ + + /* If no protection flag is set, then this means all are set */ + if ((flags & _PARTITION_POLICY_USE_MASK) == 0) + flags |= PARTITION_POLICY_OPEN; + + /* If this is a verity or verity signature designator, then mask off all protection bits, this after + * all needs no protection, because it *is* the protection */ + if (partition_verity_to_data(policy->designator) >= 0 || + partition_verity_sig_to_data(policy->designator) >= 0) + flags &= ~(PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED); + + /* if this designator has no verity concept, then mask off verity protection flags */ + if (partition_verity_of(policy->designator) < 0) + flags &= ~(PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED); + + if ((flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_ABSENT) + /* If the partition must be absent, then the gpt flags don't matter */ + flags &= ~(_PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK); + else { + /* If the gpt flags bits are not specified, set both options for each */ + if ((flags & _PARTITION_POLICY_READ_ONLY_MASK) == 0) + flags |= PARTITION_POLICY_READ_ONLY_ON|PARTITION_POLICY_READ_ONLY_OFF; + if ((flags & _PARTITION_POLICY_GROWFS_MASK) == 0) + flags |= PARTITION_POLICY_GROWFS_ON|PARTITION_POLICY_GROWFS_OFF; + } + + return flags; +} + +PartitionPolicyFlags image_policy_get(const ImagePolicy *policy, PartitionDesignator designator) { + PartitionDesignator data_designator = _PARTITION_DESIGNATOR_INVALID; + PartitionPolicy *pp; + + /* No policy means: everything may be used in any mode */ + if (!policy) + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_OPEN, + .designator = designator, + }); + + pp = image_policy_bsearch(policy, designator); + if (pp) + return partition_policy_normalized_flags(pp); + + /* Hmm, so this didn't work, then let's see if we can derive some policy from the underlying data + * partition in case of verity/signature partitions */ + + data_designator = partition_verity_to_data(designator); + if (data_designator >= 0) { + PartitionPolicyFlags data_flags; + + /* So we are asked for the policy for a verity partition, and there's no explicit policy for + * that case. Let's synthesize a policy from the protection setting for the underlying data + * partition. */ + + data_flags = image_policy_get(policy, data_designator); + if (data_flags < 0) + return data_flags; + + /* We need verity if verity or verity with sig is requested */ + if (!(data_flags & (PARTITION_POLICY_SIGNED|PARTITION_POLICY_VERITY))) + return _PARTITION_POLICY_FLAGS_INVALID; + + /* If the data partition may be unused or absent, then the verity partition may too. Also, inherit the partition flags policy */ + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_UNPROTECTED | (data_flags & (PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT)) | + (data_flags & _PARTITION_POLICY_PFLAGS_MASK), + .designator = designator, + }); + } + + data_designator = partition_verity_sig_to_data(designator); + if (data_designator >= 0) { + PartitionPolicyFlags data_flags; + + /* Similar case as for verity partitions, but slightly more strict rules */ + + data_flags = image_policy_get(policy, data_designator); + if (data_flags < 0) + return data_flags; + + if (!(data_flags & PARTITION_POLICY_SIGNED)) + return _PARTITION_POLICY_FLAGS_INVALID; + + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_UNPROTECTED | (data_flags & (PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT)) | + (data_flags & _PARTITION_POLICY_PFLAGS_MASK), + .designator = designator, + }); + } + + return _PARTITION_POLICY_FLAGS_INVALID; /* got nothing */ +} + +PartitionPolicyFlags image_policy_get_exhaustively(const ImagePolicy *policy, PartitionDesignator designator) { + PartitionPolicyFlags flags; + + /* This is just like image_policy_get() but whenever there is no policy for a specific designator, we + * return the default policy. */ + + flags = image_policy_get(policy, designator); + if (flags < 0) + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(policy), + .designator = designator, + }); + + return flags; +} + +static PartitionPolicyFlags policy_flag_from_string_one(const char *s) { + assert(s); + + /* This is a bitmask (i.e. not dense), hence we don't use the "string-table.h" stuff here. */ + + if (streq(s, "verity")) + return PARTITION_POLICY_VERITY; + if (streq(s, "signed")) + return PARTITION_POLICY_SIGNED; + if (streq(s, "encrypted")) + return PARTITION_POLICY_ENCRYPTED; + if (streq(s, "unprotected")) + return PARTITION_POLICY_UNPROTECTED; + if (streq(s, "unused")) + return PARTITION_POLICY_UNUSED; + if (streq(s, "absent")) + return PARTITION_POLICY_ABSENT; + if (streq(s, "open")) /* shortcut alias */ + return PARTITION_POLICY_OPEN; + if (streq(s, "ignore")) /* ditto */ + return PARTITION_POLICY_IGNORE; + if (streq(s, "read-only-on")) + return PARTITION_POLICY_READ_ONLY_ON; + if (streq(s, "read-only-off")) + return PARTITION_POLICY_READ_ONLY_OFF; + if (streq(s, "growfs-on")) + return PARTITION_POLICY_GROWFS_ON; + if (streq(s, "growfs-off")) + return PARTITION_POLICY_GROWFS_OFF; + + return _PARTITION_POLICY_FLAGS_INVALID; +} + +PartitionPolicyFlags partition_policy_flags_from_string(const char *s) { + PartitionPolicyFlags flags = 0; + int r; + + assert(s); + + if (empty_or_dash(s)) + return 0; + + for (;;) { + _cleanup_free_ char *f = NULL; + PartitionPolicyFlags ff; + + r = extract_first_word(&s, &f, "+", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + ff = policy_flag_from_string_one(strstrip(f)); + if (ff < 0) + return -EBADRQC; /* recognizable error */ + + flags |= ff; + } + + return flags; +} + +static ImagePolicy* image_policy_new(size_t n_policies) { + ImagePolicy *p; + + if (n_policies > (SIZE_MAX - offsetof(ImagePolicy, policies)) / sizeof(PartitionPolicy)) /* overflow check */ + return NULL; + + p = malloc(offsetof(ImagePolicy, policies) + sizeof(PartitionPolicy) * n_policies); + if (!p) + return NULL; + + *p = (ImagePolicy) { + .default_flags = PARTITION_POLICY_IGNORE, + }; + return p; +} + +int image_policy_from_string(const char *s, ImagePolicy **ret) { + _cleanup_free_ ImagePolicy *p = NULL; + uint64_t dmask = 0; + ImagePolicy *t; + PartitionPolicyFlags symbolic_policy; + int r; + + assert(s); + assert_cc(sizeof(dmask) * 8 >= _PARTITION_DESIGNATOR_MAX); + + /* Recognizable errors: + * + * ENOTUNIQ → Two or more rules for the same partition + * EBADSLT → Unknown partition designator + * EBADRQC → Unknown policy flags + */ + + /* First, let's handle "symbolic" policies, i.e. "-", "*", "~" */ + if (empty_or_dash(s)) + /* ignore policy: everything may exist, but nothing used */ + symbolic_policy = PARTITION_POLICY_IGNORE; + else if (streq(s, "*")) + /* allow policy: everything is allowed */ + symbolic_policy = PARTITION_POLICY_OPEN; + else if (streq(s, "~")) + /* deny policy: nothing may exist */ + symbolic_policy = PARTITION_POLICY_ABSENT; + else + symbolic_policy = _PARTITION_POLICY_FLAGS_INVALID; + + if (symbolic_policy >= 0) { + if (!ret) + return 0; + + p = image_policy_new(0); + if (!p) + return -ENOMEM; + + p->default_flags = symbolic_policy; + *ret = TAKE_PTR(p); + return 0; + } + + /* Allocate the policy at maximum size, i.e. for all designators. We might overshoot a bit, but the + * items are cheap, and we can return unused space to libc once we know we don't need it */ + p = image_policy_new(_PARTITION_DESIGNATOR_MAX); + if (!p) + return -ENOMEM; + + const char *q = s; + bool default_specified = false; + for (;;) { + _cleanup_free_ char *e = NULL, *d = NULL; + PartitionDesignator designator; + PartitionPolicyFlags flags; + char *f, *ds, *fs; + + r = extract_first_word(&q, &e, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + f = e; + r = extract_first_word((const char**) &f, &d, "=", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Expected designator name followed by '='; got instead: %s", e); + if (!f) /* no separator? */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Missing '=' in policy expression: %s", e); + + ds = strstrip(d); + if (isempty(ds)) { + /* Not partition name? then it's the default policy */ + if (default_specified) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Default partition policy flags specified more than once."); + + designator = _PARTITION_DESIGNATOR_INVALID; + default_specified = true; + } else { + designator = partition_designator_from_string(ds); + if (designator < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADSLT), "Unknown partition designator: %s", ds); /* recognizable error */ + if (dmask & (UINT64_C(1) << designator)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition designator specified more than once: %s", ds); + dmask |= UINT64_C(1) << designator; + } + + fs = strstrip(f); + flags = partition_policy_flags_from_string(fs); + if (flags == -EBADRQC) + return log_debug_errno(flags, "Unknown partition policy flag: %s", fs); + if (flags < 0) + return log_debug_errno(flags, "Failed to parse partition policy flags '%s': %m", fs); + + if (designator < 0) + p->default_flags = flags; + else { + p->policies[p->n_policies++] = (PartitionPolicy) { + .designator = designator, + .flags = flags, + }; + } + }; + + assert(p->n_policies <= _PARTITION_DESIGNATOR_MAX); + + /* Return unused space to libc */ + t = realloc(p, offsetof(ImagePolicy, policies) + sizeof(PartitionPolicy) * p->n_policies); + if (t) + p = t; + + typesafe_qsort(p->policies, p->n_policies, partition_policy_compare); + + if (ret) + *ret = TAKE_PTR(p); + + return 0; +} + +int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret) { + _cleanup_free_ char *buf = NULL; + const char *l[CONST_LOG2U(_PARTITION_POLICY_MASK) + 1]; /* one string per known flag at most */ + size_t m = 0; + + assert(ret); + + if (flags < 0) + return -EINVAL; + + /* If 'simplify' is false we'll output the precise value of every single flag. + * + * If 'simplify' is true we'll try to make the output shorter, by doing the following: + * + * → we'll spell the long form "verity+signed+encrypted+unprotected+unused+absent" via its + * equivalent shortcut form "open" (which we happily parse btw, see above) + * + * → we'll spell the long form "unused+absent" via its shortcut "ignore" (which we are also happy + * to parse) + * + * → if the read-only/growfs policy flags are both set, we suppress them. this thus removes the + * distinction between "user explicitly declared don't care" and "we implied don't care because + * user didn't say anything". + * + * net result: the resulting string is shorter, but the effective policy declared that way will have + * the same results as the long form. */ + + if (simplify && (flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_OPEN) + l[m++] = "open"; + else if (simplify && (flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_IGNORE) + l[m++] = "ignore"; + else { + if (flags & PARTITION_POLICY_VERITY) + l[m++] = "verity"; + if (flags & PARTITION_POLICY_SIGNED) + l[m++] = "signed"; + if (flags & PARTITION_POLICY_ENCRYPTED) + l[m++] = "encrypted"; + if (flags & PARTITION_POLICY_UNPROTECTED) + l[m++] = "unprotected"; + if (flags & PARTITION_POLICY_UNUSED) + l[m++] = "unused"; + if (flags & PARTITION_POLICY_ABSENT) + l[m++] = "absent"; + } + + if (!simplify || (!(flags & PARTITION_POLICY_READ_ONLY_ON) != !(flags & PARTITION_POLICY_READ_ONLY_OFF))) { + if (flags & PARTITION_POLICY_READ_ONLY_ON) + l[m++] = "read-only-on"; + if (flags & PARTITION_POLICY_READ_ONLY_OFF) + l[m++] = "read-only-off"; + } + + if (!simplify || (!(flags & PARTITION_POLICY_GROWFS_ON) != !(flags & PARTITION_POLICY_GROWFS_OFF))) { + if (flags & PARTITION_POLICY_GROWFS_OFF) + l[m++] = "growfs-off"; + if (flags & PARTITION_POLICY_GROWFS_ON) + l[m++] = "growfs-on"; + } + + if (m == 0) + buf = strdup("-"); + else { + assert(m+1 < ELEMENTSOF(l)); + l[m] = NULL; + + buf = strv_join((char**) l, "+"); + } + if (!buf) + return -ENOMEM; + + *ret = TAKE_PTR(buf); + return 0; +} + +static int image_policy_flags_all_match(const ImagePolicy *policy, PartitionPolicyFlags expected) { + + if (expected < 0) + return -EINVAL; + + if (image_policy_default(policy) != expected) + return false; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags f, w; + + f = image_policy_get_exhaustively(policy, d); + if (f < 0) + return f; + + w = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = expected, + .designator = d, + }); + if (w < 0) + return w; + if (f != w) + return false; + } + + return true; +} + +bool image_policy_equiv_ignore(const ImagePolicy *policy) { + /* Checks if this is the ignore policy (or equivalent to it), i.e. everything is ignored, aka '-', aka '' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_IGNORE); +} + +bool image_policy_equiv_allow(const ImagePolicy *policy) { + /* Checks if this is the allow policy (or equivalent to it), i.e. everything is allowed, aka '*' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_OPEN); +} + +bool image_policy_equiv_deny(const ImagePolicy *policy) { + /* Checks if this is the deny policy (or equivalent to it), i.e. everything must be absent, aka '~' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_ABSENT); +} + +int image_policy_to_string(const ImagePolicy *policy, bool simplify, char **ret) { + _cleanup_free_ char *s = NULL; + int r; + + assert(ret); + + if (simplify) { + const char *fixed; + + if (image_policy_equiv_allow(policy)) + fixed = "*"; + else if (image_policy_equiv_ignore(policy)) + fixed = "-"; + else if (image_policy_equiv_deny(policy)) + fixed = "~"; + else + fixed = NULL; + + if (fixed) { + s = strdup(fixed); + if (!s) + return -ENOMEM; + + *ret = TAKE_PTR(s); + return 0; + } + } + + for (size_t i = 0; i < image_policy_n_entries(policy); i++) { + const PartitionPolicy *p = policy->policies + i; + _cleanup_free_ char *f = NULL; + const char *t; + + assert(i == 0 || p->designator > policy->policies[i-1].designator); /* Validate perfect ordering */ + + assert_se(t = partition_designator_to_string(p->designator)); + + if (simplify) { + /* Skip policy entries that match the default anyway */ + PartitionPolicyFlags df; + + df = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(policy), + .designator = p->designator, + }); + if (df < 0) + return df; + + if (df == p->flags) + continue; + } + + r = partition_policy_flags_to_string(p->flags, simplify, &f); + if (r < 0) + return r; + + if (!strextend(&s, isempty(s) ? "" : ":", t, "=", f)) + return -ENOMEM; + } + + if (!simplify || image_policy_default(policy) != PARTITION_POLICY_IGNORE) { + _cleanup_free_ char *df = NULL; + + r = partition_policy_flags_to_string(image_policy_default(policy), simplify, &df); + if (r < 0) + return r; + + if (!strextend(&s, isempty(s) ? "" : ":", "=", df)) + return -ENOMEM; + } + + if (isempty(s)) { /* no rule and default policy? then let's return "-" */ + s = strdup("-"); + if (!s) + return -ENOMEM; + } + + *ret = TAKE_PTR(s); + return 0; +} + +bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b) { + if (a == b) + return true; + if (image_policy_n_entries(a) != image_policy_n_entries(b)) + return false; + if (image_policy_default(a) != image_policy_default(b)) + return false; + for (size_t i = 0; i < image_policy_n_entries(a); i++) { + if (a->policies[i].designator != b->policies[i].designator) + return false; + if (a->policies[i].flags != b->policies[i].flags) + return false; + } + + return true; +} + +int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b) { + + /* The image_policy_equal() function checks if the policy is defined the exact same way. This + * function here instead looks at the outcome of the two policies instead. Where does this come to + * different results you ask? We imply some logic regarding Verity/Encryption: when no rule is + * defined for a verity partition we can synthesize it from the protection level of the data + * partition it protects. Or: any per-partition rule that is identical to the default rule is + * redundant, and will be recognized as such by image_policy_equivalent() but not by + * image_policy_equal()- */ + + if (image_policy_default(a) != image_policy_default(b)) + return false; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags f, w; + + f = image_policy_get_exhaustively(a, d); + if (f < 0) + return f; + + w = image_policy_get_exhaustively(b, d); + if (w < 0) + return w; + + if (f != w) + return false; + } + + return true; +} + +const ImagePolicy image_policy_allow = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_OPEN, +}; + +const ImagePolicy image_policy_deny = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_ABSENT, +}; + +const ImagePolicy image_policy_ignore = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_sysext = { + /* For system extensions, honour root file system, and /usr/ and ignore everything else. After all, + * we are only interested in /usr/ + /opt/ trees anyway, and that's really the only place they can + * be. */ + .n_policies = 2, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_container = { + /* For systemd-nspawn containers we use all partitions, with the exception of swap */ + .n_policies = 8, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_host = { + /* For the host policy we basically use everything */ + .n_policies = 9, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SWAP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_service = { + /* For RootImage= in services we skip ESP/XBOOTLDR and swap */ + .n_policies = 6, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; diff --git a/src/shared/image-policy.h b/src/shared/image-policy.h new file mode 100644 index 00000000000..278c06c36a6 --- /dev/null +++ b/src/shared/image-policy.h @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct ImagePolicy ImagePolicy; + +#include "dissect-image.h" +#include "errno-list.h" + +typedef enum PartitionPolicyFlags { + /* Not all policy flags really make sense on all partition types, see comments. But even if they + * don't make sense we'll parse them anyway, because maybe one day we'll add them for more partition + * types, too. Moreover, we allow configuring a "default" policy for all partition types for which no + * explicit policy is specified. It's useful if we can use policy flags in there and apply this + * default policy gracefully even to partition types where they don't really make too much sense + * on. Example: a default policy of "verity+encrypted" certainly makes sense, but for /home/ + * partitions this gracefully degrades to "encrypted" (as we do not have a concept of verity for + * /home/), and so on. */ + PARTITION_POLICY_VERITY = 1 << 0, /* must exist, activate with verity (only applies to root/usr partitions) */ + PARTITION_POLICY_SIGNED = 1 << 1, /* must exist, activate with signed verity (only applies to root/usr partitions) */ + PARTITION_POLICY_ENCRYPTED = 1 << 2, /* must exist, activate with LUKS encryption (applies to any data partition, but not to verity/signature partitions */ + PARTITION_POLICY_UNPROTECTED = 1 << 3, /* must exist, activate without encryption/verity */ + PARTITION_POLICY_UNUSED = 1 << 4, /* must exist, don't use */ + PARTITION_POLICY_ABSENT = 1 << 5, /* must not exist */ + PARTITION_POLICY_OPEN = PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED| + PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT, + PARTITION_POLICY_IGNORE = PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT, + _PARTITION_POLICY_USE_MASK = PARTITION_POLICY_OPEN, + + PARTITION_POLICY_READ_ONLY_OFF = 1 << 6, /* State of GPT partition flag "read-only" must be on */ + PARTITION_POLICY_READ_ONLY_ON = 1 << 7, + _PARTITION_POLICY_READ_ONLY_MASK = PARTITION_POLICY_READ_ONLY_OFF|PARTITION_POLICY_READ_ONLY_ON, + PARTITION_POLICY_GROWFS_OFF = 1 << 8, /* State of GPT partition flag "growfs" must be on */ + PARTITION_POLICY_GROWFS_ON = 1 << 9, + _PARTITION_POLICY_GROWFS_MASK = PARTITION_POLICY_GROWFS_OFF|PARTITION_POLICY_GROWFS_ON, + _PARTITION_POLICY_PFLAGS_MASK = _PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK, + + _PARTITION_POLICY_MASK = _PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK, + + _PARTITION_POLICY_FLAGS_INVALID = -EINVAL, + _PARTITION_POLICY_FLAGS_ERRNO_MAX = -ERRNO_MAX, /* Ensure the whole errno range fits into this enum */ +} PartitionPolicyFlags; + +assert_cc((_PARTITION_POLICY_USE_MASK | _PARTITION_POLICY_PFLAGS_MASK) >= 0); /* ensure flags don't collide with errno range */ + +typedef struct PartitionPolicy { + PartitionDesignator designator; + PartitionPolicyFlags flags; +} PartitionPolicy; + +struct ImagePolicy { + PartitionPolicyFlags default_flags; /* for any designator not listed in the list below */ + size_t n_policies; + PartitionPolicy policies[]; /* sorted by designator, hence suitable for binary search */ +}; + +/* Default policies for various usecases */ +extern const ImagePolicy image_policy_allow; +extern const ImagePolicy image_policy_deny; +extern const ImagePolicy image_policy_ignore; +extern const ImagePolicy image_policy_sysext; +extern const ImagePolicy image_policy_container; +extern const ImagePolicy image_policy_service; +extern const ImagePolicy image_policy_host; + +PartitionPolicyFlags image_policy_get(const ImagePolicy *policy, PartitionDesignator designator); +PartitionPolicyFlags image_policy_get_exhaustively(const ImagePolicy *policy, PartitionDesignator designator); + +/* We want that the NULL image policy means "everything" allowed, hence use these simple accessors to make + * NULL policies work reasonably */ +static inline PartitionPolicyFlags image_policy_default(const ImagePolicy *policy) { + return policy ? policy->default_flags : PARTITION_POLICY_OPEN; +} + +static inline size_t image_policy_n_entries(const ImagePolicy *policy) { + return policy ? policy->n_policies : 0; +} + +PartitionPolicyFlags partition_policy_flags_from_string(const char *s); +int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret); + +int image_policy_from_string(const char *s, ImagePolicy **ret); +int image_policy_to_string(const ImagePolicy *policy, bool simplify, char **ret); + +/* Recognizes three special policies by equivalence */ +bool image_policy_equiv_ignore(const ImagePolicy *policy); +bool image_policy_equiv_allow(const ImagePolicy *policy); +bool image_policy_equiv_deny(const ImagePolicy *policy); + +bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b); /* checks if defined the same way, i.e. has literally the same ruleset */ +int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b); /* checks if the outcome is the same, i.e. for all partitions results in the same decisions. */ + +static inline ImagePolicy* image_policy_free(ImagePolicy *p) { + return mfree(p); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(ImagePolicy*, image_policy_free); diff --git a/src/shared/meson.build b/src/shared/meson.build index 0f2e2d1a675..df82778f9dd 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -81,6 +81,7 @@ shared_sources = files( 'id128-print.c', 'idn-util.c', 'ima-util.c', + 'image-policy.c', 'import-util.c', 'in-addr-prefix-util.c', 'install-file.c', diff --git a/src/test/meson.build b/src/test/meson.build index d20c911e2b4..85c3115e14f 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -95,6 +95,7 @@ simple_tests += files( 'test-hostname-setup.c', 'test-hostname-util.c', 'test-id128.c', + 'test-image-policy.c', 'test-import-util.c', 'test-in-addr-prefix-util.c', 'test-in-addr-util.c', diff --git a/src/test/test-image-policy.c b/src/test/test-image-policy.c new file mode 100644 index 00000000000..8dc2044c4a5 --- /dev/null +++ b/src/test/test-image-policy.c @@ -0,0 +1,121 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "image-policy.h" +#include "pretty-print.h" +#include "string-util.h" +#include "tests.h" +#include "pager.h" + +static void test_policy(const ImagePolicy *p, const char *name) { + _cleanup_free_ char *as_string = NULL, *as_string_simplified = NULL; + _cleanup_free_ ImagePolicy *parsed = NULL; + + assert_se(image_policy_to_string(p, /* simplified= */ false, &as_string) >= 0); + assert_se(image_policy_to_string(p, /* simplified= */ true, &as_string_simplified) >= 0); + + printf("%s%s", ansi_underline(), name); + + if (!streq(as_string_simplified, name)) { + printf(" → %s", as_string_simplified); + + if (!streq(as_string, as_string_simplified)) + printf(" (aka %s)", as_string); + } + + printf("%s\n", ansi_normal()); + + assert_se(image_policy_from_string(as_string, &parsed) >= 0); + assert_se(image_policy_equal(p, parsed)); + parsed = image_policy_free(parsed); + + assert_se(image_policy_from_string(as_string_simplified, &parsed) >= 0); + assert_se(image_policy_equivalent(p, parsed)); + parsed = image_policy_free(parsed); + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + _cleanup_free_ char *k = NULL; + PartitionPolicyFlags f; + + f = image_policy_get(p, d); + if (f < 0) { + f = image_policy_get_exhaustively(p, d); + assert_se(f >= 0); + assert_se(partition_policy_flags_to_string(f, /* simplified= */ true, &k) >= 0); + + printf("%s\t%s → n/a (exhaustively: %s)%s\n", ansi_grey(), partition_designator_to_string(d), k, ansi_normal()); + } else { + assert_se(partition_policy_flags_to_string(f, /* simplified= */ true, &k) >= 0); + printf("\t%s → %s\n", partition_designator_to_string(d), k); + } + } + + _cleanup_free_ char *w = NULL; + assert_se(partition_policy_flags_to_string(image_policy_default(p), /* simplified= */ true, &w) >= 0); + printf("\tdefault → %s\n", w); +} + +static void test_policy_string(const char *t) { + _cleanup_free_ ImagePolicy *parsed = NULL; + + assert_se(image_policy_from_string(t, &parsed) >= 0); + test_policy(parsed, t); +} + +static void test_policy_equiv(const char *s, bool (*func)(const ImagePolicy *p)) { + _cleanup_(image_policy_freep) ImagePolicy *p = NULL; + + assert_se(image_policy_from_string(s, &p) >= 0); + + assert_se(func(p)); + assert_se(func == image_policy_equiv_ignore || !image_policy_equiv_ignore(p)); + assert_se(func == image_policy_equiv_allow || !image_policy_equiv_allow(p)); + assert_se(func == image_policy_equiv_deny || !image_policy_equiv_deny(p)); +} + +TEST_RET(test_image_policy_to_string) { + test_policy(&image_policy_allow, "*"); + test_policy(&image_policy_ignore, "-"); + test_policy(&image_policy_deny, "~"); + test_policy(&image_policy_sysext, "sysext"); + test_policy(&image_policy_container, "container"); + test_policy(&image_policy_host, "host"); + test_policy(&image_policy_service, "service"); + test_policy(NULL, "null"); + + test_policy_string(""); + test_policy_string("-"); + test_policy_string("*"); + test_policy_string("~"); + test_policy_string("swap=open"); + test_policy_string("swap=open:root=signed"); + test_policy_string("swap=open:root=signed+read-only-on+growfs-off:=absent"); + test_policy_string("=-"); + test_policy_string("="); + + test_policy_equiv("", image_policy_equiv_ignore); + test_policy_equiv("-", image_policy_equiv_ignore); + test_policy_equiv("*", image_policy_equiv_allow); + test_policy_equiv("~", image_policy_equiv_deny); + test_policy_equiv("=absent", image_policy_equiv_deny); + test_policy_equiv("=open", image_policy_equiv_allow); + test_policy_equiv("=verity+signed+encrypted+unprotected+unused+absent", image_policy_equiv_allow); + test_policy_equiv("=signed+verity+encrypted+unused+unprotected+absent", image_policy_equiv_allow); + test_policy_equiv("=ignore", image_policy_equiv_ignore); + test_policy_equiv("=absent+unused", image_policy_equiv_ignore); + test_policy_equiv("=unused+absent", image_policy_equiv_ignore); + test_policy_equiv("root=ignore:=ignore", image_policy_equiv_ignore); + + assert_se(image_policy_from_string("pfft", NULL) == -EINVAL); + assert_se(image_policy_from_string("öäüß", NULL) == -EINVAL); + assert_se(image_policy_from_string(":", NULL) == -EINVAL); + assert_se(image_policy_from_string("a=", NULL) == -EBADSLT); + assert_se(image_policy_from_string("=a", NULL) == -EBADRQC); + assert_se(image_policy_from_string("==", NULL) == -EBADRQC); + assert_se(image_policy_from_string("root=verity:root=encrypted", NULL) == -ENOTUNIQ); + assert_se(image_policy_from_string("root=grbl", NULL) == -EBADRQC); + assert_se(image_policy_from_string("wowza=grbl", NULL) == -EBADSLT); + + return 0; +} + +DEFINE_TEST_MAIN(LOG_INFO);