import: merge default OpenPGP keyrings
When an administrator installed a custom keyring under /etc, signature verification selected it instead of the vendor keyring under /usr. That could make vendor-signed system updates stop verifying after adding local keys. Collect all readable default keyrings, including the current and legacy /etc paths, and pass temporary copies to GnuPG with separate --keyring options. Keep SYSTEMD_OPENPGP_KEYRING as an explicit override, add regression coverage for keyring coexistence, override behavior, and current-path signing, and silence the expected shellcheck false positive in the test cleanup helper. Closes: #40702
This commit is contained in:
@@ -43,6 +43,11 @@ CHANGES WITH 262:
|
||||
have been provided. This clears the way for a new
|
||||
"systemd-sysupdate@.service" unit for varlink activation of sysupdate.
|
||||
|
||||
* The default OpenPGP keyring lookup now combines the vendor keyring in
|
||||
/usr with the local keyring in /etc. Deployments that relied on a
|
||||
single /etc keyring to exclude vendor trust should set
|
||||
$SYSTEMD_OPENPGP_KEYRING explicitly.
|
||||
|
||||
* TPM-sealed credentials are now pinned to the TPM's SRK. This prevents
|
||||
MITM interposer attacks from stealing the decrypted credentials by
|
||||
ensuring that communication with the TPM is protected by a private key
|
||||
|
||||
+3
-2
@@ -516,8 +516,9 @@ is suppressed by default.
|
||||
|
||||
* `$SYSTEMD_OPENPGP_KEYRING` — takes an absolute path to an OpenPGP keyring
|
||||
file. If set and non-empty, signature verification on download uses this
|
||||
keyring instead of the default `/etc/systemd/import-pubring.pgp` and
|
||||
`/usr/lib/systemd/import-pubring.pgp` keyrings.
|
||||
keyring instead of the default `/etc/systemd/import-pubring.pgp`,
|
||||
`/etc/systemd/import-pubring.gpg`, and `/usr/lib/systemd/import-pubring.pgp`
|
||||
keyrings.
|
||||
Useful when running unprivileged in the user context, with custom transfer
|
||||
definitions (e.g. `systemd-sysupdate --definitions=…`), or for testing.
|
||||
Has no effect when signature verification is disabled.
|
||||
|
||||
+8
-6
@@ -124,9 +124,10 @@
|
||||
<filename>.sha256</filename> suffixed file or the <filename>SHA256SUMS</filename> file. With
|
||||
<option>--verify=signature</option>, the sha checksum file is first verified with the detached GPG
|
||||
signature of <filename>.sha256</filename> or <filename>SHA256SUMS</filename>. The public key for
|
||||
this verification step needs to be available in
|
||||
<filename>/usr/lib/systemd/import-pubring.pgp</filename> or
|
||||
<filename>/etc/systemd/import-pubring.pgp</filename>.</para>
|
||||
this verification step needs to be available in the system keyring consisting of
|
||||
<filename>/usr/lib/systemd/import-pubring.pgp</filename> and the local keyring in
|
||||
<filename>/etc/systemd/import-pubring.pgp</filename> (or, if that does not exist,
|
||||
<filename>/etc/systemd/import-pubring.gpg</filename>).</para>
|
||||
|
||||
<para>If <option>-keep-download=yes</option> is specified the image will be downloaded and stored in
|
||||
a read-only subvolume/directory in the image directory that is named after the specified URL and its
|
||||
@@ -314,9 +315,10 @@
|
||||
<literal>signature</literal>. If <literal>no</literal>, no verification is done. If
|
||||
<literal>checksum</literal> is specified, the download is checked for integrity after the transfer is
|
||||
complete, but no signatures are verified. If <literal>signature</literal> is specified, the checksum
|
||||
is verified and the image's signature is checked against a local keyring of trustable vendors. It is
|
||||
strongly recommended to set this option to <literal>signature</literal> if the server and protocol
|
||||
support this. Defaults to <literal>signature</literal>.</para>
|
||||
is verified and the image's signature is checked against a local keyring of trustable vendors, in
|
||||
combination with the vendor keyring in <filename>/usr/lib/systemd/import-pubring.pgp</filename>.
|
||||
It is strongly recommended to set this option to <literal>signature</literal> if the server and
|
||||
protocol support this. Defaults to <literal>signature</literal>.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
+3
-2
@@ -501,8 +501,9 @@
|
||||
downloaded resources (specifically: validate the GPG signatures for downloaded
|
||||
<filename>SHA256SUMS</filename> manifest files, via their detached signature files
|
||||
<filename>SHA256SUMS.gpg</filename> in combination with the system keyring
|
||||
<filename>/usr/lib/systemd/import-pubring.pgp</filename> or
|
||||
<filename>/etc/systemd/import-pubring.pgp</filename>).</para>
|
||||
consisting of <filename>/usr/lib/systemd/import-pubring.pgp</filename> and the local
|
||||
keyring in <filename>/etc/systemd/import-pubring.pgp</filename> (or, if that does not
|
||||
exist, <filename>/etc/systemd/import-pubring.gpg</filename>).</para>
|
||||
|
||||
<para>This option is essential to provide integrity guarantees for downloaded resources and thus
|
||||
should be left enabled, outside of test environments.</para>
|
||||
|
||||
+107
-49
@@ -125,6 +125,22 @@ static int hash_url(const char *url, char **ret) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int keyring_add_if_exists(char ***keyrings, const char *path) {
|
||||
assert(keyrings);
|
||||
assert(path);
|
||||
|
||||
if (access(path, F_OK) < 0) {
|
||||
if (errno != ENOENT)
|
||||
log_warning_errno(errno,
|
||||
"Failed to check if keyring '%s' exists, ignoring: %m",
|
||||
path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return strv_extend(keyrings, path);
|
||||
}
|
||||
|
||||
int pull_make_path(const char *url, const char *etag, const char *image_root, const char *prefix, const char *suffix, char **ret) {
|
||||
_cleanup_free_ char *escaped_url = NULL, *escaped_etag = NULL;
|
||||
char *path;
|
||||
@@ -410,12 +426,13 @@ static int verify_gpg(
|
||||
_cleanup_(rm_rf_physical_and_freep) char *gpg_home = NULL;
|
||||
char sig_file_path[] = "/tmp/sigXXXXXX";
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_free_ char *keyring_copy = NULL;
|
||||
const char *keyring_override, *keyring_source;
|
||||
_cleanup_strv_free_ char **cmd = NULL;
|
||||
_cleanup_strv_free_ char **keyring_sources = NULL, **keyring_copies = NULL;
|
||||
const char *keyring_override;
|
||||
int r;
|
||||
|
||||
assert(iovec_is_valid(payload));
|
||||
assert(iovec_is_valid(signature));
|
||||
assert(iovec_is_set(signature));
|
||||
|
||||
/* Support using a custom keyring, see docs/ENVIRONMENT.md. */
|
||||
keyring_override = empty_to_null(secure_getenv("SYSTEMD_OPENPGP_KEYRING"));
|
||||
@@ -428,7 +445,7 @@ static int verify_gpg(
|
||||
if (r < 0)
|
||||
return log_error_errno(errno, "Failed to create pipe for gpg: %m");
|
||||
|
||||
if (iovec_is_set(signature)) {
|
||||
{
|
||||
_cleanup_close_ int sig_file = -EBADF;
|
||||
|
||||
sig_file = mkostemp(sig_file_path, O_RDWR);
|
||||
@@ -448,28 +465,98 @@ static int verify_gpg(
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (keyring_override)
|
||||
keyring_source = keyring_override;
|
||||
else if (access(USER_KEYRING_PATH, F_OK) >= 0)
|
||||
keyring_source = USER_KEYRING_PATH;
|
||||
else if (access(USER_KEYRING_PATH_LEGACY, F_OK) >= 0)
|
||||
keyring_source = USER_KEYRING_PATH_LEGACY;
|
||||
else
|
||||
keyring_source = VENDOR_KEYRING_PATH;
|
||||
if (keyring_override) {
|
||||
r = strv_extend(&keyring_sources, keyring_override);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to store custom keyring path: %m");
|
||||
goto finish;
|
||||
}
|
||||
} else {
|
||||
r = keyring_add_if_exists(&keyring_sources, USER_KEYRING_PATH);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to collect keyring '%s': %m", USER_KEYRING_PATH);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (strv_isempty(keyring_sources)) {
|
||||
r = keyring_add_if_exists(&keyring_sources, USER_KEYRING_PATH_LEGACY);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to collect keyring '%s': %m", USER_KEYRING_PATH_LEGACY);
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
|
||||
r = keyring_add_if_exists(&keyring_sources, VENDOR_KEYRING_PATH);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to collect keyring '%s': %m", VENDOR_KEYRING_PATH);
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
|
||||
if (strv_isempty(keyring_sources)) {
|
||||
r = log_error_errno(SYNTHETIC_ERRNO(ENOENT),
|
||||
"No OpenPGP keyrings found, checked '%s', '%s', and '%s'.",
|
||||
USER_KEYRING_PATH, USER_KEYRING_PATH_LEGACY, VENDOR_KEYRING_PATH);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
/* For whatever reason gpg --auto-key-import writes the key material embedded in the signature
|
||||
* (the signing subkey) back into the keyring it verifies against instead of just using it
|
||||
* temporarily. Verify against a throwaway copy in the tmp home so we never write to the
|
||||
* original keyring. */
|
||||
keyring_copy = path_join(gpg_home, "keyring.gpg");
|
||||
if (!keyring_copy) {
|
||||
STRV_FOREACH(keyring_source, keyring_sources) {
|
||||
_cleanup_free_ char *copy = NULL;
|
||||
|
||||
if (asprintf(©, "%s/keyring-%zu.gpg", gpg_home, strv_length(keyring_copies)) < 0) {
|
||||
r = log_oom();
|
||||
goto finish;
|
||||
}
|
||||
|
||||
r = copy_file(*keyring_source, copy, 0, 0600, /* copy_flags= */ 0);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to copy keyring '%s': %m", *keyring_source);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
r = strv_consume(&keyring_copies, TAKE_PTR(copy));
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to store temporary keyring path: %m");
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
|
||||
cmd = strv_new(
|
||||
"gpg",
|
||||
"--no-options",
|
||||
"--no-default-keyring",
|
||||
"--no-auto-key-locate",
|
||||
"--no-auto-check-trustdb",
|
||||
"--batch",
|
||||
"--trust-model=always",
|
||||
"--auto-key-import",
|
||||
"--import-options=merge-only,import-clean");
|
||||
if (!cmd) {
|
||||
r = log_oom();
|
||||
goto finish;
|
||||
}
|
||||
|
||||
r = copy_file(keyring_source, keyring_copy, 0, 0600, /* copy_flags= */ 0);
|
||||
r = strv_extendf(&cmd, "--homedir=%s", gpg_home);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to copy keyring '%s': %m", keyring_source);
|
||||
log_error_errno(r, "Failed to build gpg command line: %m");
|
||||
goto finish;
|
||||
}
|
||||
|
||||
STRV_FOREACH(keyring_source, keyring_copies) {
|
||||
r = strv_extendf(&cmd, "--keyring=%s", *keyring_source);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to build gpg command line: %m");
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
|
||||
r = strv_extend_many(&cmd, "--verify", sig_file_path, "-");
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to build gpg command line: %m");
|
||||
goto finish;
|
||||
}
|
||||
|
||||
@@ -480,40 +567,12 @@ static int verify_gpg(
|
||||
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE,
|
||||
&pidref);
|
||||
if (r < 0)
|
||||
return r;
|
||||
goto finish;
|
||||
if (r == 0) {
|
||||
const char *cmd[] = {
|
||||
"gpg",
|
||||
"--no-options",
|
||||
"--no-default-keyring",
|
||||
"--no-auto-key-locate",
|
||||
"--no-auto-check-trustdb",
|
||||
"--batch",
|
||||
"--trust-model=always",
|
||||
"--auto-key-import",
|
||||
"--import-options=merge-only,import-clean",
|
||||
NULL, /* --homedir= */
|
||||
NULL, /* --keyring= */
|
||||
NULL, /* --verify */
|
||||
NULL, /* signature file */
|
||||
NULL, /* dash */
|
||||
NULL /* trailing NULL */
|
||||
};
|
||||
size_t k = ELEMENTSOF(cmd) - 6;
|
||||
|
||||
/* Child */
|
||||
|
||||
cmd[k++] = strjoina("--homedir=", gpg_home);
|
||||
cmd[k++] = strjoina("--keyring=", keyring_copy);
|
||||
cmd[k++] = "--verify";
|
||||
if (signature) {
|
||||
cmd[k++] = sig_file_path;
|
||||
cmd[k++] = "-";
|
||||
cmd[k++] = NULL;
|
||||
}
|
||||
|
||||
execvp("gpg2", (char * const *) cmd);
|
||||
execvp("gpg", (char * const *) cmd);
|
||||
execvp("gpg2", cmd);
|
||||
execvp("gpg", cmd);
|
||||
log_error_errno(errno, "Failed to execute gpg: %m");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
@@ -543,8 +602,7 @@ static int verify_gpg(
|
||||
}
|
||||
|
||||
finish:
|
||||
if (iovec_is_set(signature))
|
||||
(void) unlink(sig_file_path);
|
||||
(void) unlink(sig_file_path);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -1281,7 +1281,153 @@ EOF
|
||||
cmp "$sigdir/payload-v2.raw" "$target/payload-v2.raw"
|
||||
}
|
||||
|
||||
backup_import_keyring() {
|
||||
local path="$1"
|
||||
local backup="$2"
|
||||
|
||||
rm -f "$backup"
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
mv "$path" "$backup"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_import_keyring() {
|
||||
local path="$1"
|
||||
local backup="$2"
|
||||
|
||||
rm -f "$path"
|
||||
if [ -e "$backup" ] || [ -L "$backup" ]; then
|
||||
mv "$backup" "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
test_signature_keyring_overlay() (
|
||||
if ! command -v gpg >/dev/null; then
|
||||
echo "gpg not available, skipping signature overlay test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local gpg_version gpg_rest
|
||||
gpg_version="$(gpg --version | sed -n '1p' | awk '{print $NF}')"
|
||||
gpg_rest="${gpg_version#*.}"
|
||||
if [ "${gpg_version%%.*}" -lt 2 ] || { [ "${gpg_version%%.*}" -eq 2 ] && [ "${gpg_rest%%.*}" -lt 4 ]; }; then
|
||||
echo "gpg $gpg_version too old (need >= 2.4), skipping signature overlay test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local sigdir="$WORKDIR/sigtest-overlay-source"
|
||||
local defdir="$WORKDIR/sigtest-overlay-defs"
|
||||
local user_home="$WORKDIR/sigtest-overlay-userhome"
|
||||
local vendor_home="$WORKDIR/sigtest-overlay-vendorhome"
|
||||
local other_home="$WORKDIR/sigtest-overlay-otherhome"
|
||||
local target="$WORKDIR/sigtest-overlay-target"
|
||||
local user_keyring="$WORKDIR/sigtest-overlay-user.gpg"
|
||||
local vendor_keyring="$WORKDIR/sigtest-overlay-vendor.gpg"
|
||||
local other_keyring="$WORKDIR/sigtest-overlay-other.gpg"
|
||||
local etc_pgp_backup="$WORKDIR/import-pubring.etc.pgp.bak"
|
||||
local etc_gpg_backup="$WORKDIR/import-pubring.etc.gpg.bak"
|
||||
local usr_pgp_backup="$WORKDIR/import-pubring.usr.pgp.bak"
|
||||
# Called via trap on EXIT; shellcheck cannot see the indirect invocation.
|
||||
# shellcheck disable=SC2329,SC2317
|
||||
cleanup_overlay_keyrings() {
|
||||
set +e
|
||||
gpgconf --homedir "$user_home" --kill all 2>/dev/null || :
|
||||
gpgconf --homedir "$vendor_home" --kill all 2>/dev/null || :
|
||||
gpgconf --homedir "$other_home" --kill all 2>/dev/null || :
|
||||
restore_import_keyring /etc/systemd/import-pubring.pgp "$etc_pgp_backup"
|
||||
restore_import_keyring /etc/systemd/import-pubring.gpg "$etc_gpg_backup"
|
||||
restore_import_keyring /usr/lib/systemd/import-pubring.pgp "$usr_pgp_backup"
|
||||
}
|
||||
|
||||
mkdir -p "$sigdir" "$defdir" "$user_home" "$vendor_home" "$other_home" "$target" /etc/systemd /usr/lib/systemd
|
||||
chmod 700 "$user_home" "$vendor_home" "$other_home"
|
||||
|
||||
trap cleanup_overlay_keyrings EXIT
|
||||
|
||||
backup_import_keyring /etc/systemd/import-pubring.pgp "$etc_pgp_backup"
|
||||
backup_import_keyring /etc/systemd/import-pubring.gpg "$etc_gpg_backup"
|
||||
backup_import_keyring /usr/lib/systemd/import-pubring.pgp "$usr_pgp_backup"
|
||||
|
||||
GNUPGHOME="$user_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--quick-gen-key 'Overlay User <overlay-user@example.com>' rsa2048 cert,sign never
|
||||
GNUPGHOME="$user_home" gpg --export --output "$user_keyring"
|
||||
|
||||
GNUPGHOME="$vendor_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--quick-gen-key 'Overlay Vendor <overlay-vendor@example.com>' rsa2048 cert,sign never
|
||||
GNUPGHOME="$vendor_home" gpg --export --output "$vendor_keyring"
|
||||
|
||||
GNUPGHOME="$other_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--quick-gen-key 'Overlay Other <overlay-other@example.com>' rsa2048 cert,sign never
|
||||
GNUPGHOME="$other_home" gpg --export --output "$other_keyring"
|
||||
|
||||
install -Dm0644 "$user_keyring" /etc/systemd/import-pubring.gpg
|
||||
install -Dm0644 "$vendor_keyring" /usr/lib/systemd/import-pubring.pgp
|
||||
rm -f /etc/systemd/import-pubring.pgp
|
||||
|
||||
dd if=/dev/urandom of="$sigdir/payload-v1.raw" bs=1024 count=8 status=none
|
||||
(cd "$sigdir" && sha256sum payload-v1.raw > SHA256SUMS)
|
||||
GNUPGHOME="$vendor_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--detach-sign --include-key-block --yes \
|
||||
--output "$sigdir/SHA256SUMS.gpg" "$sigdir/SHA256SUMS"
|
||||
|
||||
cat >"$defdir/01-sigtest.transfer" <<EOF
|
||||
[Source]
|
||||
Type=url-file
|
||||
Path=file://$sigdir
|
||||
MatchPattern=payload-@v.raw
|
||||
|
||||
[Target]
|
||||
Type=regular-file
|
||||
Path=$target
|
||||
MatchPattern=payload-@v.raw
|
||||
InstancesMax=3
|
||||
EOF
|
||||
|
||||
"$SYSUPDATE" --definitions="$defdir" check-new
|
||||
"$SYSUPDATE" --definitions="$defdir" update
|
||||
cmp "$sigdir/payload-v1.raw" "$target/payload-v1.raw"
|
||||
|
||||
dd if=/dev/urandom of="$sigdir/payload-v2.raw" bs=1024 count=8 status=none
|
||||
(cd "$sigdir" && sha256sum payload-v1.raw payload-v2.raw > SHA256SUMS)
|
||||
GNUPGHOME="$other_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--detach-sign --include-key-block --yes \
|
||||
--output "$sigdir/SHA256SUMS.gpg" "$sigdir/SHA256SUMS"
|
||||
if "$SYSUPDATE" --definitions="$defdir" update; then
|
||||
echo "ERROR: accepted an update signed by a key not in the default keyrings" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$target/payload-v2.raw" ]; then
|
||||
echo "ERROR: payload-v2 should not have been installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dd if=/dev/urandom of="$sigdir/payload-v3.raw" bs=1024 count=8 status=none
|
||||
(cd "$sigdir" && sha256sum payload-v1.raw payload-v2.raw payload-v3.raw > SHA256SUMS)
|
||||
GNUPGHOME="$vendor_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--detach-sign --include-key-block --yes \
|
||||
--output "$sigdir/SHA256SUMS.gpg" "$sigdir/SHA256SUMS"
|
||||
if SYSTEMD_OPENPGP_KEYRING="$other_keyring" "$SYSUPDATE" --definitions="$defdir" update; then
|
||||
echo "ERROR: accepted an update with an override keyring that lacks the signing key" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$target/payload-v3.raw" ]; then
|
||||
echo "ERROR: payload-v3 should not have been installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -Dm0644 "$user_keyring" /etc/systemd/import-pubring.pgp
|
||||
rm -f /etc/systemd/import-pubring.gpg
|
||||
dd if=/dev/urandom of="$sigdir/payload-v4.raw" bs=1024 count=8 status=none
|
||||
(cd "$sigdir" && sha256sum payload-v1.raw payload-v2.raw payload-v3.raw payload-v4.raw > SHA256SUMS)
|
||||
GNUPGHOME="$user_home" gpg --batch --pinentry-mode loopback --passphrase '' \
|
||||
--detach-sign --include-key-block --yes \
|
||||
--output "$sigdir/SHA256SUMS.gpg" "$sigdir/SHA256SUMS"
|
||||
"$SYSUPDATE" --definitions="$defdir" update
|
||||
cmp "$sigdir/payload-v4.raw" "$target/payload-v4.raw"
|
||||
)
|
||||
|
||||
test_signature_verification
|
||||
test_signature_keyring_overlay
|
||||
|
||||
# Test '**/' as prefix in MatchPattern= for subpaths in SHA256SUMS
|
||||
rm -rf "$CONFIGDIR" "$WORKDIR/blobs" "$WORKDIR/source/sub"
|
||||
|
||||
Reference in New Issue
Block a user