1
0
mirror of https://github.com/systemd/systemd synced 2026-03-23 23:34:52 +01:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Lennart Poettering
7d963260a1
Merge pull request #20172 from poettering/import-util-tweaks
Import-util: URL processing tweaks
2021-07-30 19:05:06 +02:00
Lennart Poettering
ee96d41eba
Merge pull request #20170 from poettering/moar-glyphs
util: define more emoji glyphs
2021-07-30 19:03:59 +02:00
Lennart Poettering
07697bfee6 tpm2-util: auto-detect supported PCR banks
Previously, we'd encode PCR policies strictly with the SHA256 PCR bank
set. However, as it appears not all hw implement those. Sad.

Let's add some minimal logic to auto-detect supported PCR banks: if
SHA256 is supported, use that. But if not, automatically fall back to
SHA1.

This then changes both the LUKS code, and the credentials code to
serialize the selected bank, along with the rest of the data in order to
make this robust.

This extends the LUK2 JSON metadata in a compatible way. The credentials
encryption format is modified in an incompatible way however, but given
that this is not part of any official release should be OK.

Fixes: #20134
2021-07-30 19:03:35 +02:00
Lennart Poettering
9554c51425 test: add simple test for import-util 2021-07-30 16:31:24 +02:00
Lennart Poettering
56ce4adafe import-util: tweak url patching helper
let's share some code between import_url_last_component() and
import_url_change_last_component(), and make sure we never eat up the
hostname component of the URL when parsing out the last component.

Let's also make import_url_change_last_component() more generic so that
we can also use it for append components to paths, instead of replacing
suffixes.
2021-07-30 16:23:20 +02:00
Lennart Poettering
30763a32b2 util: add one more helper for generating colored check mark glyphs
This one is useful for a outputs with a slightly more "positive"
outlook, i.e. where only the checkmarks are shown but the crossmarks are
replaced by spaces.

(Usecase: a larger table with many checkmarks, where the red crossmarks
might just be too much negative noise)
2021-07-30 16:18:40 +02:00
Lennart Poettering
bf18f4b671 glyph-util: add three more emojis to emoji list 2021-07-30 16:18:05 +02:00
Lennart Poettering
1f0fb7d544 rm-rf: refactor rm_rf_children(), split out body of directory iteration loop
This splits out rm_rf_children_inner() as body of the loop. We can use
that to implement rm_rf_child() for deleting one specific entry in a
directory.
2021-07-30 16:14:37 +02:00
17 changed files with 529 additions and 147 deletions

View File

@ -375,6 +375,9 @@ const char *special_glyph(SpecialGlyph code) {
[SPECIAL_GLYPH_DEPRESSED_SMILEY] = ":-[",
[SPECIAL_GLYPH_LOCK_AND_KEY] = "o-,",
[SPECIAL_GLYPH_TOUCH] = "O=", /* Yeah, not very convincing, can you do it better? */
[SPECIAL_GLYPH_RECYCLING] = "~",
[SPECIAL_GLYPH_DOWNLOAD] = "\\",
[SPECIAL_GLYPH_SPARKLES] = "*",
},
/* UTF-8 */
@ -421,7 +424,12 @@ const char *special_glyph(SpecialGlyph code) {
[SPECIAL_GLYPH_LOCK_AND_KEY] = "\360\237\224\220", /* 🔐 (actually called: CLOSED LOCK WITH KEY) */
/* This emoji is a single character cell glyph in Unicode, and two in ASCII */
[SPECIAL_GLYPH_TOUCH] = "\360\237\221\206", /* 👆 (actually called: BACKHAND INDEX POINTING UP */
[SPECIAL_GLYPH_TOUCH] = "\360\237\221\206", /* 👆 (actually called: BACKHAND INDEX POINTING UP) */
/* These three emojis are single character cell glyphs in Unicode and also in ASCII. */
[SPECIAL_GLYPH_RECYCLING] = "\u267B\uFE0F ", /* ♻️ (actually called: UNIVERSAL RECYCLNG SYMBOL) */
[SPECIAL_GLYPH_DOWNLOAD] = "\u2935\uFE0F ", /* ⤵️ (actually called: RIGHT ARROW CURVING DOWN) */
[SPECIAL_GLYPH_SPARKLES] = "\u2728", /* ✨ */
},
};

View File

@ -69,6 +69,9 @@ typedef enum SpecialGlyph {
SPECIAL_GLYPH_DEPRESSED_SMILEY,
SPECIAL_GLYPH_LOCK_AND_KEY,
SPECIAL_GLYPH_TOUCH,
SPECIAL_GLYPH_RECYCLING,
SPECIAL_GLYPH_DOWNLOAD,
SPECIAL_GLYPH_SPARKLES,
_SPECIAL_GLYPH_MAX,
_SPECIAL_GLYPH_INVALID = -EINVAL,
} SpecialGlyph;
@ -95,3 +98,7 @@ static inline void locale_variables_freep(char*(*l)[_VARIABLE_LC_MAX]) {
static inline const char *special_glyph_check_mark(bool b) {
return b ? special_glyph(SPECIAL_GLYPH_CHECK_MARK) : special_glyph(SPECIAL_GLYPH_CROSS_MARK);
}
static inline const char *special_glyph_check_mark_space(bool b) {
return b ? special_glyph(SPECIAL_GLYPH_CHECK_MARK) : " ";
}

View File

@ -65,6 +65,7 @@ int enroll_tpm2(struct crypt_device *cd,
_cleanup_(erase_and_freep) char *base64_encoded = NULL;
size_t secret_size, secret2_size, blob_size, hash_size;
_cleanup_free_ void *blob = NULL, *hash = NULL;
uint16_t pcr_bank;
const char *node;
int r, keyslot;
@ -75,7 +76,7 @@ int enroll_tpm2(struct crypt_device *cd,
assert_se(node = crypt_get_device_name(cd));
r = tpm2_seal(device, pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size);
r = tpm2_seal(device, pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size, &pcr_bank);
if (r < 0)
return r;
@ -92,7 +93,7 @@ int enroll_tpm2(struct crypt_device *cd,
/* Quick verification that everything is in order, we are not in a hurry after all. */
log_debug("Unsealing for verification...");
r = tpm2_unseal(device, pcr_mask, blob, blob_size, hash, hash_size, &secret2, &secret2_size);
r = tpm2_unseal(device, pcr_mask, pcr_bank, blob, blob_size, hash, hash_size, &secret2, &secret2_size);
if (r < 0)
return r;
@ -118,7 +119,7 @@ int enroll_tpm2(struct crypt_device *cd,
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node);
r = tpm2_make_luks2_json(keyslot, pcr_mask, blob, blob_size, hash, hash_size, &v);
r = tpm2_make_luks2_json(keyslot, pcr_mask, pcr_bank, blob, blob_size, hash, hash_size, &v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");

View File

@ -13,6 +13,7 @@ int acquire_tpm2_key(
const char *volume_name,
const char *device,
uint32_t pcr_mask,
uint16_t pcr_bank,
const char *key_file,
size_t key_file_size,
uint64_t key_file_offset,
@ -62,7 +63,7 @@ int acquire_tpm2_key(
blob = loaded_blob;
}
return tpm2_unseal(device, pcr_mask, blob, blob_size, policy_hash, policy_hash_size, ret_decrypted_key, ret_decrypted_key_size);
return tpm2_unseal(device, pcr_mask, pcr_bank, blob, blob_size, policy_hash, policy_hash_size, ret_decrypted_key, ret_decrypted_key_size);
}
int find_tpm2_auto_data(
@ -70,6 +71,7 @@ int find_tpm2_auto_data(
uint32_t search_pcr_mask,
int start_token,
uint32_t *ret_pcr_mask,
uint16_t *ret_pcr_bank,
void **ret_blob,
size_t *ret_blob_size,
void **ret_policy_hash,
@ -81,6 +83,7 @@ int find_tpm2_auto_data(
size_t blob_size = 0, policy_hash_size = 0;
int r, keyslot = -1, token = -1;
uint32_t pcr_mask = 0;
uint16_t pcr_bank = UINT16_MAX; /* default: pick automatically */
assert(cd);
@ -119,6 +122,23 @@ int find_tpm2_auto_data(
search_pcr_mask != pcr_mask) /* PCR mask doesn't match what is configured, ignore this entry */
continue;
/* The bank field is optional, since it was added in systemd 250 only. Before the bank was hardcoded to SHA256 */
assert(pcr_bank == UINT16_MAX);
w = json_variant_by_key(v, "tpm2-pcr-bank");
if (w) {
/* The PCR bank field is optional */
if (!json_variant_is_string(w))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"TPM2 PCR bank is not a string.");
r = tpm2_pcr_bank_from_string(json_variant_string(w));
if (r < 0)
return log_error_errno(r, "TPM2 PCR bank invalid or not supported: %s", json_variant_string(w));
pcr_bank = r;
}
assert(!blob);
w = json_variant_by_key(v, "tpm2-blob");
if (!w || !json_variant_is_string(w))
@ -163,6 +183,7 @@ int find_tpm2_auto_data(
*ret_policy_hash_size = policy_hash_size;
*ret_keyslot = keyslot;
*ret_token = token;
*ret_pcr_bank = pcr_bank;
return 0;
}

View File

@ -13,6 +13,7 @@ int acquire_tpm2_key(
const char *volume_name,
const char *device,
uint32_t pcr_mask,
uint16_t pcr_bank,
const char *key_file,
size_t key_file_size,
uint64_t key_file_offset,
@ -28,6 +29,7 @@ int find_tpm2_auto_data(
uint32_t search_pcr_mask,
int start_token,
uint32_t *ret_pcr_mask,
uint16_t *ret_pcr_bank,
void **ret_blob,
size_t *ret_blob_size,
void **ret_policy_hash,
@ -41,6 +43,7 @@ static inline int acquire_tpm2_key(
const char *volume_name,
const char *device,
uint32_t pcr_mask,
uint16_t pcr_bank,
const char *key_file,
size_t key_file_size,
uint64_t key_file_offset,
@ -60,6 +63,7 @@ static inline int find_tpm2_auto_data(
uint32_t search_pcr_mask,
int start_token,
uint32_t *ret_pcr_mask,
uint16_t *ret_pcr_bank,
void **ret_blob,
size_t *ret_blob_size,
void **ret_policy_hash,

View File

@ -1103,6 +1103,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
name,
arg_tpm2_device,
arg_tpm2_pcr_mask == UINT32_MAX ? TPM2_PCR_MASK_DEFAULT : arg_tpm2_pcr_mask,
UINT16_MAX,
key_file, arg_keyfile_size, arg_keyfile_offset,
key_data, key_data_size,
NULL, 0, /* we don't know the policy hash */
@ -1139,12 +1140,14 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
for (;;) {
uint32_t pcr_mask;
uint16_t pcr_bank;
r = find_tpm2_auto_data(
cd,
arg_tpm2_pcr_mask, /* if != UINT32_MAX we'll only look for tokens with this PCR mask */
token, /* search for the token with this index, or any later index than this */
&pcr_mask,
&pcr_bank,
&blob, &blob_size,
&policy_hash, &policy_hash_size,
&keyslot,
@ -1166,6 +1169,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
name,
arg_tpm2_device,
pcr_mask,
pcr_bank,
NULL, 0, 0, /* no key file */
blob, blob_size,
policy_hash, policy_hash_size,

View File

@ -2624,9 +2624,10 @@ static int partition_encrypt(
_cleanup_(erase_and_freep) void *secret = NULL;
_cleanup_free_ void *blob = NULL, *hash = NULL;
size_t secret_size, blob_size, hash_size;
uint16_t pcr_bank;
int keyslot;
r = tpm2_seal(arg_tpm2_device, arg_tpm2_pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size);
r = tpm2_seal(arg_tpm2_device, arg_tpm2_pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size, &pcr_bank);
if (r < 0)
return log_error_errno(r, "Failed to seal to TPM2: %m");
@ -2648,7 +2649,7 @@ static int partition_encrypt(
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node);
r = tpm2_make_luks2_json(keyslot, arg_tpm2_pcr_mask, blob, blob_size, hash, hash_size, &v);
r = tpm2_make_luks2_json(keyslot, arg_tpm2_pcr_mask, pcr_bank, blob, blob_size, hash, hash_size, &v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");

View File

@ -369,7 +369,10 @@ struct _packed_ encrypted_credential_header {
};
struct _packed_ tpm2_credential_header {
le64_t pcr_mask;
le64_t pcr_mask; /* Note that the spec for PC Clients only mandates 24 PCRs, and that's what systems
* generally have. But keep the door open for more. */
le16_t pcr_bank; /* For now, either TPM2_ALG_SHA256 or TPM2_ALG_SHA1 */
le16_t _zero; /* Filler to maintain 32bit alignment */
le32_t blob_size;
le32_t policy_hash_size;
uint8_t policy_hash_and_blob[];
@ -439,6 +442,7 @@ int encrypt_credential_and_warn(
struct encrypted_credential_header *h;
int ksz, bsz, ivsz, tsz, added, r;
uint8_t md[SHA256_DIGEST_LENGTH];
uint16_t tpm2_pcr_bank = 0;
const EVP_CIPHER *cc;
#if HAVE_TPM2
bool try_tpm2 = false;
@ -505,7 +509,8 @@ int encrypt_credential_and_warn(
&tpm2_blob,
&tpm2_blob_size,
&tpm2_policy_hash,
&tpm2_policy_hash_size);
&tpm2_policy_hash_size,
&tpm2_pcr_bank);
if (r < 0) {
if (!sd_id128_is_null(with_key))
return r;
@ -598,6 +603,7 @@ int encrypt_credential_and_warn(
t = (struct tpm2_credential_header*) ((uint8_t*) output + p);
t->pcr_mask = htole64(tpm2_pcr_mask);
t->pcr_bank = htole16(tpm2_pcr_bank);
t->blob_size = htole32(tpm2_blob_size);
t->policy_hash_size = htole32(tpm2_policy_hash_size);
memcpy(t->policy_hash_and_blob, tpm2_blob, tpm2_blob_size);
@ -739,6 +745,10 @@ int decrypt_credential_and_warn(
if (le64toh(t->pcr_mask) >= (UINT64_C(1) << TPM2_PCRS_MAX))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "TPM2 PCR mask out of range.");
if (!tpm2_pcr_bank_supported(le16toh(t->pcr_bank)))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "TPM2 PCR bank invalid or not supported");
if (le16toh(t->_zero) != 0)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "TPM2 padding space not zero.");
if (le32toh(t->blob_size) > CREDENTIAL_FIELD_SIZE_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected TPM2 blob size.");
if (le32toh(t->policy_hash_size) > CREDENTIAL_FIELD_SIZE_MAX)
@ -755,6 +765,7 @@ int decrypt_credential_and_warn(
r = tpm2_unseal(tpm2_device,
le64toh(t->pcr_mask),
le16toh(t->pcr_bank),
t->policy_hash_and_blob,
le32toh(t->blob_size),
t->policy_hash_and_blob + le32toh(t->blob_size),

View File

@ -14,53 +14,114 @@
#include "string-table.h"
#include "string-util.h"
int import_url_last_component(const char *url, char **ret) {
const char *e, *p;
char *s;
static const char *skip_protocol_and_hostname(const char *url) {
const char *d;
size_t n;
e = strchrnul(url, '?');
/* A very very lenient implementation of RFC3986 Section 3.2 */
while (e > url && e[-1] == '/')
/* Find colon separating protocol and hostname */
d = strchr(url, ':');
if (!d || url == d)
return NULL;
d++;
/* Skip slashes after colon */
d += strspn(d, "/");
/* Skip everything till next slash or end */
n = strcspn(d, "/?#");
if (n == 0)
return NULL;
return d + n;
}
int import_url_last_component(
const char *url,
char **ret) {
const char *e, *p, *h;
/* This extracts the last path component of the specified URI, i.e. the last non-empty substrings
* between two "/" characters. This ignores "Query" and "Fragment" suffixes (as per RFC3986). */
h = skip_protocol_and_hostname(url);
if (!h)
return -EINVAL;
e = h + strcspn(h, "?#"); /* Cut off "Query" and "Fragment" */
while (e > h && e[-1] == '/') /* Eat trailing slashes */
e--;
p = e;
while (p > url && p[-1] != '/')
while (p > h && p[-1] != '/') /* Find component before that */
p--;
if (e <= p)
return -EINVAL;
if (e <= p) /* Empty component? */
return -EADDRNOTAVAIL;
s = strndup(p, e - p);
if (!s)
return -ENOMEM;
if (ret) {
char *s;
s = strndup(p, e - p);
if (!s)
return -ENOMEM;
*ret = s;
}
*ret = s;
return 0;
}
int import_url_change_last_component(const char *url, const char *suffix, char **ret) {
const char *e;
int import_url_change_suffix(
const char *url,
size_t n_drop_components,
const char *suffix,
char **ret) {
const char *e, *h;
char *s;
assert(url);
assert(ret);
e = strchrnul(url, '?');
/* This drops the specified number of path components of the specified URI, i.e. the specified number
* of non-empty substring between two "/" characters from the end of the string, and then append the
* specified suffix instead. Before doing all this it chops off the "Query" and "Fragment" suffixes
* (they are *not* readded to the final URL). Note that n_drop_components may be 0 (in which case the
* component are simply added to the end). The suffix may be specified as NULL or empty string in
* which case nothing is appended, only the specified number of components chopped off. Note that the
* function may be called with n_drop_components == 0 and suffix == NULL, in which case the "Query"
* and "Fragment" is chopped off, and ensured the URL ends in a single "/", and that's it. */
while (e > url && e[-1] == '/')
e--;
while (e > url && e[-1] != '/')
e--;
if (e <= url)
h = skip_protocol_and_hostname(url);
if (!h)
return -EINVAL;
s = new(char, (e - url) + strlen(suffix) + 1);
e = h + strcspn(h, "?#"); /* Cut off "Query" and "Fragment" */
while (e > h && e[-1] == '/') /* Eat trailing slashes */
e--;
/* Drop the specified number of components from the end. Note that this is pretty lenient: if there
* are less component we silently drop those and then append the suffix to the top. */
while (n_drop_components > 0) {
while (e > h && e[-1] != '/') /* Eat last word (we don't mind if empty) */
e--;
while (e > h && e[-1] == '/') /* Eat slashes before the last word */
e--;
n_drop_components--;
}
s = new(char, (e - url) + 1 + strlen_ptr(suffix) + 1);
if (!s)
return -ENOMEM;
strcpy(mempcpy(s, url, e - url), suffix);
strcpy(stpcpy(mempcpy(s, url, e - url), "/"), strempty(suffix));
*ret = s;
return 0;
}

View File

@ -14,7 +14,16 @@ typedef enum ImportVerify {
} ImportVerify;
int import_url_last_component(const char *url, char **ret);
int import_url_change_last_component(const char *url, const char *suffix, char **ret);
int import_url_change_suffix(const char *url, size_t n_drop_components, const char *suffix, char **ret);
static inline int import_url_change_last_component(const char *url, const char *suffix, char **ret) {
return import_url_change_suffix(url, 1, suffix, ret);
}
static inline int import_url_append_component(const char *url, const char *suffix, char **ret) {
return import_url_change_suffix(url, 0, suffix, ret);
}
const char* import_verify_to_string(ImportVerify v) _const_;
ImportVerify import_verify_from_string(const char *s) _pure_;

View File

@ -19,6 +19,9 @@
#include "stat-util.h"
#include "string-util.h"
/* We treat tmpfs/ramfs + cgroupfs as non-physical file sytems. cgroupfs is similar to tmpfs in a way after
* all: we can create arbitrary directory hierarchies in it, and hence can also use rm_rf() on it to remove
* those again. */
static bool is_physical_fs(const struct statfs *sfs) {
return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs);
}
@ -113,133 +116,145 @@ int fstatat_harder(int dfd,
return 0;
}
int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) {
static int rm_rf_children_inner(
int fd,
const char *fname,
int is_dir,
RemoveFlags flags,
const struct stat *root_dev) {
struct stat st;
int r;
assert(fd >= 0);
assert(fname);
if (is_dir < 0 || (is_dir > 0 && (root_dev || (flags & REMOVE_SUBVOLUME)))) {
r = fstatat_harder(fd, fname, &st, AT_SYMLINK_NOFOLLOW, flags);
if (r < 0)
return r;
is_dir = S_ISDIR(st.st_mode);
}
if (is_dir) {
_cleanup_close_ int subdir_fd = -1;
int q;
/* if root_dev is set, remove subdirectories only if device is same */
if (root_dev && st.st_dev != root_dev->st_dev)
return 0;
/* Stop at mount points */
r = fd_is_mount_point(fd, fname, 0);
if (r < 0)
return r;
if (r > 0)
return 0;
if ((flags & REMOVE_SUBVOLUME) && btrfs_might_be_subvol(&st)) {
/* This could be a subvolume, try to remove it */
r = btrfs_subvol_remove_fd(fd, fname, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA);
if (r < 0) {
if (!IN_SET(r, -ENOTTY, -EINVAL))
return r;
/* ENOTTY, then it wasn't a btrfs subvolume, continue below. */
} else
/* It was a subvolume, done. */
return 1;
}
subdir_fd = openat(fd, fname, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME);
if (subdir_fd < 0)
return -errno;
/* We pass REMOVE_PHYSICAL here, to avoid doing the fstatfs() to check the file system type
* again for each directory */
q = rm_rf_children(TAKE_FD(subdir_fd), flags | REMOVE_PHYSICAL, root_dev);
r = unlinkat_harder(fd, fname, AT_REMOVEDIR, flags);
if (r < 0)
return r;
if (q < 0)
return q;
return 1;
} else if (!(flags & REMOVE_ONLY_DIRECTORIES)) {
r = unlinkat_harder(fd, fname, 0, flags);
if (r < 0)
return r;
return 1;
}
return 0;
}
int rm_rf_children(
int fd,
RemoveFlags flags,
const struct stat *root_dev) {
_cleanup_closedir_ DIR *d = NULL;
struct dirent *de;
int ret = 0, r;
struct statfs sfs;
assert(fd >= 0);
/* This returns the first error we run into, but nevertheless tries to go on. This closes the passed
* fd, in all cases, including on failure.. */
if (!(flags & REMOVE_PHYSICAL)) {
r = fstatfs(fd, &sfs);
if (r < 0) {
safe_close(fd);
return -errno;
}
if (is_physical_fs(&sfs)) {
/* We refuse to clean physical file systems with this call,
* unless explicitly requested. This is extra paranoia just
* to be sure we never ever remove non-state data. */
_cleanup_free_ char *path = NULL;
(void) fd_get_path(fd, &path);
log_error("Attempted to remove disk file system under \"%s\", and we can't allow that.",
strna(path));
safe_close(fd);
return -EPERM;
}
}
* fd, in all cases, including on failure. */
d = fdopendir(fd);
if (!d) {
safe_close(fd);
return errno == ENOENT ? 0 : -errno;
return -errno;
}
if (!(flags & REMOVE_PHYSICAL)) {
struct statfs sfs;
if (fstatfs(dirfd(d), &sfs) < 0)
return -errno;
if (is_physical_fs(&sfs)) {
/* We refuse to clean physical file systems with this call, unless explicitly
* requested. This is extra paranoia just to be sure we never ever remove non-state
* data. */
_cleanup_free_ char *path = NULL;
(void) fd_get_path(fd, &path);
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"Attempted to remove disk file system under \"%s\", and we can't allow that.",
strna(path));
}
}
FOREACH_DIRENT_ALL(de, d, return -errno) {
bool is_dir;
struct stat st;
int is_dir;
if (dot_or_dot_dot(de->d_name))
continue;
if (de->d_type == DT_UNKNOWN ||
(de->d_type == DT_DIR && (root_dev || (flags & REMOVE_SUBVOLUME)))) {
r = fstatat_harder(fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW, flags);
if (r < 0) {
if (ret == 0 && r != -ENOENT)
ret = r;
continue;
}
is_dir =
de->d_type == DT_UNKNOWN ? -1 :
de->d_type == DT_DIR;
is_dir = S_ISDIR(st.st_mode);
} else
is_dir = de->d_type == DT_DIR;
if (is_dir) {
_cleanup_close_ int subdir_fd = -1;
/* if root_dev is set, remove subdirectories only if device is same */
if (root_dev && st.st_dev != root_dev->st_dev)
continue;
subdir_fd = openat(fd, de->d_name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME);
if (subdir_fd < 0) {
if (ret == 0 && errno != ENOENT)
ret = -errno;
continue;
}
/* Stop at mount points */
r = fd_is_mount_point(fd, de->d_name, 0);
if (r < 0) {
if (ret == 0 && r != -ENOENT)
ret = r;
continue;
}
if (r > 0)
continue;
if ((flags & REMOVE_SUBVOLUME) && btrfs_might_be_subvol(&st)) {
/* This could be a subvolume, try to remove it */
r = btrfs_subvol_remove_fd(fd, de->d_name, BTRFS_REMOVE_RECURSIVE|BTRFS_REMOVE_QUOTA);
if (r < 0) {
if (!IN_SET(r, -ENOTTY, -EINVAL)) {
if (ret == 0)
ret = r;
continue;
}
/* ENOTTY, then it wasn't a btrfs subvolume, continue below. */
} else
/* It was a subvolume, continue. */
continue;
}
/* We pass REMOVE_PHYSICAL here, to avoid doing the fstatfs() to check the file
* system type again for each directory */
r = rm_rf_children(TAKE_FD(subdir_fd), flags | REMOVE_PHYSICAL, root_dev);
if (r < 0 && ret == 0)
ret = r;
r = unlinkat_harder(fd, de->d_name, AT_REMOVEDIR, flags);
if (r < 0 && r != -ENOENT && ret == 0)
ret = r;
} else if (!(flags & REMOVE_ONLY_DIRECTORIES)) {
r = unlinkat_harder(fd, de->d_name, 0, flags);
if (r < 0 && r != -ENOENT && ret == 0)
ret = r;
}
r = rm_rf_children_inner(dirfd(d), de->d_name, is_dir, flags, root_dev);
if (r < 0 && r != -ENOENT && ret == 0)
ret = r;
}
return ret;
}
int rm_rf(const char *path, RemoveFlags flags) {
int fd, r;
struct statfs s;
assert(path);
@ -284,9 +299,10 @@ int rm_rf(const char *path, RemoveFlags flags) {
if (FLAGS_SET(flags, REMOVE_ROOT)) {
if (!FLAGS_SET(flags, REMOVE_PHYSICAL)) {
struct statfs s;
if (statfs(path, &s) < 0)
return -errno;
if (is_physical_fs(&s))
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"Attempted to remove files from a disk file system under \"%s\", refusing.",
@ -314,3 +330,22 @@ int rm_rf(const char *path, RemoveFlags flags) {
return r;
}
int rm_rf_child(int fd, const char *name, RemoveFlags flags) {
/* Removes one specific child of the specified directory */
if (fd < 0)
return -EBADF;
if (!filename_is_valid(name))
return -EINVAL;
if ((flags & (REMOVE_ROOT|REMOVE_MISSING_OK)) != 0) /* Doesn't really make sense here, we are not supposed to remove 'fd' anyway */
return -EINVAL;
if (FLAGS_SET(flags, REMOVE_ONLY_DIRECTORIES|REMOVE_SUBVOLUME))
return -EINVAL;
return rm_rf_children_inner(fd, name, -1, flags, NULL);
}

View File

@ -23,7 +23,8 @@ int fstatat_harder(int dfd,
int fstatat_flags,
RemoveFlags remove_flags);
int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev);
int rm_rf_children(int fd, RemoveFlags flags, const struct stat *root_dev);
int rm_rf_child(int fd, const char *name, RemoveFlags flags);
int rm_rf(const char *path, RemoveFlags flags);
/* Useful for usage with _cleanup_(), destroys a directory and frees the pointer */

View File

@ -25,6 +25,7 @@ TSS2_RC (*sym_Esys_CreatePrimary)(ESYS_CONTEXT *esysContext, ESYS_TR primaryHand
void (*sym_Esys_Finalize)(ESYS_CONTEXT **context) = NULL;
TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle) = NULL;
void (*sym_Esys_Free)(void *ptr) = NULL;
TSS2_RC (*sym_Esys_GetCapability)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2_CAP capability, UINT32 property, UINT32 propertyCount, TPMI_YES_NO *moreData, TPMS_CAPABILITY_DATA **capabilityData);
TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes) = NULL;
TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion) = NULL;
TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle) = NULL;
@ -51,6 +52,7 @@ int dlopen_tpm2(void) {
DLSYM_ARG(Esys_Finalize),
DLSYM_ARG(Esys_FlushContext),
DLSYM_ARG(Esys_Free),
DLSYM_ARG(Esys_GetCapability),
DLSYM_ARG(Esys_GetRandom),
DLSYM_ARG(Esys_Initialize),
DLSYM_ARG(Esys_Load),
@ -310,11 +312,93 @@ static int tpm2_make_primary(
return 0;
}
static int tpm2_get_best_pcr_bank(
ESYS_CONTEXT *c,
TPMI_ALG_HASH *ret) {
_cleanup_(Esys_Freep) TPMS_CAPABILITY_DATA *pcap = NULL;
TPMI_ALG_HASH hash = TPM2_ALG_SHA1;
bool found = false;
TPMI_YES_NO more;
TSS2_RC rc;
rc = sym_Esys_GetCapability(
c,
ESYS_TR_NONE,
ESYS_TR_NONE,
ESYS_TR_NONE,
TPM2_CAP_PCRS,
0,
1,
&more,
&pcap);
if (rc != TSS2_RC_SUCCESS)
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
"Failed to determine TPM2 PCR bank capabilities: %s", sym_Tss2_RC_Decode(rc));
assert(pcap->capability == TPM2_CAP_PCRS);
for (size_t i = 0; i < pcap->data.assignedPCR.count; i++) {
bool valid = true;
/* As per
* https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf a
* TPM2 on a Client PC must have at least 24 PCRs. If this TPM has less, just skip over
* it. */
if (pcap->data.assignedPCR.pcrSelections[i].sizeofSelect < TPM2_PCRS_MAX/8) {
log_debug("Skipping TPM2 PCR bank %s with fewer than 24 PCRs.",
strna(tpm2_pcr_bank_to_string(pcap->data.assignedPCR.pcrSelections[i].hash)));
continue;
}
assert_cc(TPM2_PCRS_MAX % 8 == 0);
/* It's not enought to check how many PCRs there are, we also need to check that the 24 are
* enabled for this bank. Otherwise this TPM doesn't qualify. */
for (size_t j = 0; j < TPM2_PCRS_MAX/8; j++)
if (pcap->data.assignedPCR.pcrSelections[i].pcrSelect[j] != 0xFF) {
valid = false;
break;
}
if (!valid) {
log_debug("TPM2 PCR bank %s has fewer than 24 PCR bits enabled, ignoring.",
strna(tpm2_pcr_bank_to_string(pcap->data.assignedPCR.pcrSelections[i].hash)));
continue;
}
if (pcap->data.assignedPCR.pcrSelections[i].hash == TPM2_ALG_SHA256) {
hash = TPM2_ALG_SHA256;
found = true;
break;
}
if (pcap->data.assignedPCR.pcrSelections[i].hash == TPM2_ALG_SHA1)
found = true;
}
if (!found)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"TPM2 module supports neither SHA1 nor SHA256 PCR banks, cannot operate.");
if (hash == TPM2_ALG_SHA256)
log_debug("TPM2 device supports SHA256 PCR banks, yay!");
else {
assert(hash == TPM2_ALG_SHA1);
log_debug("TPM2 device lacks support for SHA256 PCR banks, falling back to SHA1 banks.");
}
*ret = hash;
return 0;
}
static int tpm2_make_pcr_session(
ESYS_CONTEXT *c,
uint32_t pcr_mask,
uint16_t pcr_bank, /* If UINT16_MAX, pick best bank automatically, otherwise specify bank explicitly. */
ESYS_TR *ret_session,
TPM2B_DIGEST **ret_policy_digest) {
TPM2B_DIGEST **ret_policy_digest,
TPMI_ALG_HASH *ret_pcr_bank) {
static const TPMT_SYM_DEF symmetric = {
.algorithm = TPM2_ALG_AES,
@ -327,7 +411,7 @@ static int tpm2_make_pcr_session(
};
TPML_PCR_SELECTION pcr_selection = {
.count = 1,
.pcrSelections[0].hash = TPM2_ALG_SHA256,
.pcrSelections[0].hash = TPM2_ALG_SHA256, /* overriden below, depending on TPM2 capabilities */
.pcrSelections[0].sizeofSelect = 3,
.pcrSelections[0].pcrSelect[0] = pcr_mask & 0xFF,
.pcrSelections[0].pcrSelect[1] = (pcr_mask >> 8) & 0xFF,
@ -342,6 +426,16 @@ static int tpm2_make_pcr_session(
log_debug("Starting authentication session.");
if (pcr_bank != UINT16_MAX)
pcr_selection.pcrSelections[0].hash = pcr_bank;
else {
/* No bank configured, pick automatically. Some TPM2 devices only can do SHA1. If we detect
* that use that, but preferably use SHA256 */
r = tpm2_get_best_pcr_bank(c, &pcr_selection.pcrSelections[0].hash);
if (r < 0)
return r;
}
rc = sym_Esys_StartAuthSession(
c,
ESYS_TR_NONE,
@ -412,6 +506,9 @@ static int tpm2_make_pcr_session(
if (ret_policy_digest)
*ret_policy_digest = TAKE_PTR(policy_digest);
if (ret_pcr_bank)
*ret_pcr_bank = pcr_selection.pcrSelections[0].hash;
r = 0;
finish:
@ -427,7 +524,8 @@ int tpm2_seal(
void **ret_blob,
size_t *ret_blob_size,
void **ret_pcr_hash,
size_t *ret_pcr_hash_size) {
size_t *ret_pcr_hash_size,
uint16_t *ret_pcr_bank) {
_cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
_cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
@ -439,6 +537,7 @@ int tpm2_seal(
TPM2B_SENSITIVE_CREATE hmac_sensitive;
ESYS_TR primary = ESYS_TR_NONE;
TPM2B_PUBLIC hmac_template;
TPMI_ALG_HASH pcr_bank;
size_t k, blob_size;
usec_t start;
TSS2_RC rc;
@ -450,6 +549,7 @@ int tpm2_seal(
assert(ret_blob_size);
assert(ret_pcr_hash);
assert(ret_pcr_hash_size);
assert(ret_pcr_bank);
assert(pcr_mask < (UINT32_C(1) << TPM2_PCRS_MAX)); /* Support 24 PCR banks */
@ -478,7 +578,7 @@ int tpm2_seal(
if (r < 0)
return r;
r = tpm2_make_pcr_session(c.esys_context, pcr_mask, NULL, &policy_digest);
r = tpm2_make_pcr_session(c.esys_context, pcr_mask, UINT16_MAX, NULL, &policy_digest, &pcr_bank);
if (r < 0)
goto finish;
@ -600,6 +700,7 @@ int tpm2_seal(
*ret_blob_size = blob_size;
*ret_pcr_hash = TAKE_PTR(hash);
*ret_pcr_hash_size = policy_digest->size;
*ret_pcr_bank = pcr_bank;
r = 0;
@ -611,6 +712,7 @@ finish:
int tpm2_unseal(
const char *device,
uint32_t pcr_mask,
uint16_t pcr_bank,
const void *blob,
size_t blob_size,
const void *known_policy_hash,
@ -670,7 +772,7 @@ int tpm2_unseal(
if (r < 0)
return r;
r = tpm2_make_pcr_session(c.esys_context, pcr_mask, &session, &policy_digest);
r = tpm2_make_pcr_session(c.esys_context, pcr_mask, pcr_bank, &session, &policy_digest, NULL);
if (r < 0)
goto finish;
@ -909,6 +1011,7 @@ int tpm2_parse_pcrs(const char *s, uint32_t *ret) {
int tpm2_make_luks2_json(
int keyslot,
uint32_t pcr_mask,
uint16_t pcr_bank,
const void *blob,
size_t blob_size,
const void *policy_hash,
@ -951,6 +1054,7 @@ int tpm2_make_luks2_json(
JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
JSON_BUILD_PAIR("tpm2-blob", JSON_BUILD_BASE64(blob, blob_size)),
JSON_BUILD_PAIR("tpm2-pcrs", JSON_BUILD_VARIANT(a)),
JSON_BUILD_PAIR_CONDITION(!!tpm2_pcr_bank_to_string(pcr_bank), "tpm2-pcr-bank", JSON_BUILD_STRING(tpm2_pcr_bank_to_string(pcr_bank))),
JSON_BUILD_PAIR("tpm2-policy-hash", JSON_BUILD_HEX(policy_hash, policy_hash_size))));
if (r < 0)
return r;
@ -960,3 +1064,36 @@ int tpm2_make_luks2_json(
return keyslot;
}
/* We want the helpers below to work also if TPM2 libs are not available, hence define these two defines if
* they are missing. */
#ifndef TPM2_ALG_SHA256
#define TPM2_ALG_SHA256 0xB
#endif
#ifndef TPM2_ALG_SHA1
#define TPM2_ALG_SHA1 0x4
#endif
int tpm2_pcr_bank_supported(uint16_t bank) {
/* For now, let's officially only support these two. We can extend this later on, should the need
* arise. */
return IN_SET(bank, TPM2_ALG_SHA256, TPM2_ALG_SHA1);
}
const char *tpm2_pcr_bank_to_string(uint16_t bank) {
/* Similar here, only support the two for now, we can always extend this later. */
if (bank == TPM2_ALG_SHA256)
return "sha256";
if (bank == TPM2_ALG_SHA1)
return "sha1";
return NULL;
}
int tpm2_pcr_bank_from_string(const char *bank) {
if (streq_ptr(bank, "sha256"))
return TPM2_ALG_SHA256;
if (streq_ptr(bank, "sha1"))
return TPM2_ALG_SHA1;
return -EINVAL;
}

View File

@ -15,6 +15,7 @@ extern TSS2_RC (*sym_Esys_CreatePrimary)(ESYS_CONTEXT *esysContext, ESYS_TR prim
extern void (*sym_Esys_Finalize)(ESYS_CONTEXT **context);
extern TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle);
extern void (*sym_Esys_Free)(void *ptr);
extern TSS2_RC (*sym_Esys_GetCapability)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2_CAP capability, UINT32 property, UINT32 propertyCount, TPMI_YES_NO *moreData, TPMS_CAPABILITY_DATA **capabilityData);
extern TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes);
extern TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion);
extern TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle);
@ -33,8 +34,8 @@ extern TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], siz
int dlopen_tpm2(void);
int tpm2_seal(const char *device, uint32_t pcr_mask, void **ret_secret, size_t *ret_secret_size, void **ret_blob, size_t *ret_blob_size, void **ret_pcr_hash, size_t *ret_pcr_hash_size);
int tpm2_unseal(const char *device, uint32_t pcr_mask, const void *blob, size_t blob_size, const void *pcr_hash, size_t pcr_hash_size, void **ret_secret, size_t *ret_secret_size);
int tpm2_seal(const char *device, uint32_t pcr_mask, void **ret_secret, size_t *ret_secret_size, void **ret_blob, size_t *ret_blob_size, void **ret_pcr_hash, size_t *ret_pcr_hash_size, uint16_t *ret_pcr_bank);
int tpm2_unseal(const char *device, uint32_t pcr_mask, uint16_t pcr_bank, const void *blob, size_t blob_size, const void *pcr_hash, size_t pcr_hash_size, void **ret_secret, size_t *ret_secret_size);
#endif
@ -43,13 +44,17 @@ int tpm2_find_device_auto(int log_level, char **ret);
int tpm2_parse_pcrs(const char *s, uint32_t *ret);
int tpm2_make_luks2_json(int keyslot, uint32_t pcr_mask, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, JsonVariant **ret);
int tpm2_make_luks2_json(int keyslot, uint32_t pcr_mask, uint16_t pcr_bank, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, JsonVariant **ret);
#define TPM2_PCRS_MAX 24
/* Default to PCR 7 only */
#define TPM2_PCR_MASK_DEFAULT (UINT32_C(1) << 7)
int tpm2_pcr_bank_supported(uint16_t bank);
const char *tpm2_pcr_bank_to_string(uint16_t bank);
int tpm2_pcr_bank_from_string(const char *bank);
typedef struct {
uint32_t search_pcr_mask;
const char *device;

View File

@ -250,6 +250,8 @@ tests += [
[['src/test/test-sysctl-util.c']],
[['src/test/test-import-util.c']],
[['src/test/test-user-record.c']],
[['src/test/test-user-util.c']],

View File

@ -0,0 +1,72 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "import-util.h"
#include "log.h"
#include "string-util.h"
#include "tests.h"
static void test_import_url_last_component_one(const char *input, const char *output, int ret) {
_cleanup_free_ char *s = NULL;
assert_se(import_url_last_component(input, &s) == ret);
assert_se(streq_ptr(output, s));
}
static void test_import_url_last_component(void) {
test_import_url_last_component_one("https://foobar/waldo/quux", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/quux/", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/", "waldo", 0);
test_import_url_last_component_one("https://foobar/", NULL, -EADDRNOTAVAIL);
test_import_url_last_component_one("https://foobar", NULL, -EADDRNOTAVAIL);
test_import_url_last_component_one("https://foobar/waldo/quux?foo=bar", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar#piep", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/quux/#piep", "quux", 0);
test_import_url_last_component_one("https://foobar/waldo/quux#piep", "quux", 0);
test_import_url_last_component_one("https://", NULL, -EINVAL);
test_import_url_last_component_one("", NULL, -EINVAL);
test_import_url_last_component_one(":", NULL, -EINVAL);
test_import_url_last_component_one(":/", NULL, -EINVAL);
test_import_url_last_component_one("x:/", NULL, -EINVAL);
test_import_url_last_component_one("x:y", NULL, -EADDRNOTAVAIL);
test_import_url_last_component_one("x:y/z", "z", 0);
}
static void test_import_url_change_suffix_one(const char *input, size_t n, const char *suffix, const char *output, int ret) {
_cleanup_free_ char *s = NULL;
assert_se(import_url_change_suffix(input, n, suffix, &s) == ret);
assert_se(streq_ptr(output, s));
}
static void test_import_url_change_suffix(void) {
test_import_url_change_suffix_one("https://foobar/waldo/quux", 1, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux/", 1, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief", 1, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief#opopo", 1, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 2, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux/quff/", 2, "wuff", "https://foobar/waldo/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0);
test_import_url_change_suffix_one("https://foobar/waldo/quux/quff?aa?bb##4", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0);
test_import_url_change_suffix_one("https://", 0, "wuff", NULL, -EINVAL);
test_import_url_change_suffix_one("", 0, "wuff", NULL, -EINVAL);
test_import_url_change_suffix_one(":", 0, "wuff", NULL, -EINVAL);
test_import_url_change_suffix_one(":/", 0, "wuff", NULL, -EINVAL);
test_import_url_change_suffix_one("x:/", 0, "wuff", NULL, -EINVAL);
test_import_url_change_suffix_one("x:y", 0, "wuff", "x:y/wuff", 0);
test_import_url_change_suffix_one("x:y/z", 0, "wuff", "x:y/z/wuff", 0);
test_import_url_change_suffix_one("x:y/z/", 0, "wuff", "x:y/z/wuff", 0);
test_import_url_change_suffix_one("x:y/z/", 1, "wuff", "x:y/wuff", 0);
test_import_url_change_suffix_one("x:y/z/", 2, "wuff", "x:y/wuff", 0);
}
int main(int argc, char *argv[]) {
test_setup_logging(LOG_INFO);
test_import_url_last_component();
test_import_url_change_suffix();
return 0;
}

View File

@ -89,7 +89,7 @@ static void test_keymaps(void) {
#define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x))
static void dump_special_glyphs(void) {
assert_cc(SPECIAL_GLYPH_TOUCH + 1 == _SPECIAL_GLYPH_MAX);
assert_cc(SPECIAL_GLYPH_SPARKLES + 1 == _SPECIAL_GLYPH_MAX);
log_info("/* %s */", __func__);
@ -120,6 +120,9 @@ static void dump_special_glyphs(void) {
dump_glyph(SPECIAL_GLYPH_DEPRESSED_SMILEY);
dump_glyph(SPECIAL_GLYPH_LOCK_AND_KEY);
dump_glyph(SPECIAL_GLYPH_TOUCH);
dump_glyph(SPECIAL_GLYPH_RECYCLING);
dump_glyph(SPECIAL_GLYPH_DOWNLOAD);
dump_glyph(SPECIAL_GLYPH_SPARKLES);
}
int main(int argc, char *argv[]) {