mirror of
https://github.com/trussed-dev/piv-authenticator.git
synced 2026-06-20 04:16:15 -07:00
Update to heapless 0.9.1
This commit is contained in:
+5
-5
@@ -13,12 +13,12 @@ required-features = ["vpicc"]
|
||||
|
||||
[dependencies]
|
||||
apdu-app = { version = "0.1", optional = true }
|
||||
cbor-smol = { version = "0.5", features = ["heapless-bytes-v0-3"] }
|
||||
cbor-smol = { version = "0.5", features = ["heapless-bytes-v0-5"] }
|
||||
delog = { version = "0.1.5", optional = true }
|
||||
flexiber = { version = "0.1", features = ["derive", "heapless"] }
|
||||
heapless = "0.7"
|
||||
flexiber = { version = "0.2", features = ["derive", "heapless"] }
|
||||
heapless = "0.9.1"
|
||||
hex-literal = "0.3"
|
||||
iso7816 = "0.1.3"
|
||||
iso7816 = "0.2.0"
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
trussed = { version = "0.1", default-features = false, features = ["aes256-cbc", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "p256", "p384", "shared-secret", "serde-extensions", "tdes", "x255"], optional = true }
|
||||
trussed-auth = "0.4"
|
||||
@@ -26,7 +26,7 @@ trussed-auth-backend = { version = "0.1.0", optional = true }
|
||||
untrusted = "0.9"
|
||||
vpicc = { version = "0.1.0", optional = true }
|
||||
log = "0.4"
|
||||
heapless-bytes = "0.3.0"
|
||||
heapless-bytes = "0.5.0"
|
||||
subtle = { version = "2", default-features = false }
|
||||
# TODO: only enable rsa features when needed
|
||||
trussed-core = { version = "0.1.0-rc.1", features = ["aes256-cbc", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "p256", "p384", "rsa2048", "rsa3072", "rsa4096", "shared-secret", "tdes", "x255"] }
|
||||
|
||||
+10
-4
@@ -1,10 +1,11 @@
|
||||
use crate::{reply::Reply, Authenticator, /*constants::PIV_AID,*/ Result};
|
||||
|
||||
use apdu_app::{App, CommandView, Data};
|
||||
use apdu_app::{App, CommandView};
|
||||
use heapless::VecView;
|
||||
use iso7816::{Interface, Status};
|
||||
|
||||
#[cfg(feature = "apdu-dispatch")]
|
||||
impl<T, const R: usize> App<R> for Authenticator<T>
|
||||
impl<T> App for Authenticator<T>
|
||||
where
|
||||
T: crate::Client,
|
||||
{
|
||||
@@ -12,7 +13,7 @@ where
|
||||
&mut self,
|
||||
interface: Interface,
|
||||
_apdu: CommandView<'_>,
|
||||
reply: &mut Data<R>,
|
||||
reply: &mut VecView<u8>,
|
||||
) -> Result {
|
||||
if interface != Interface::Contact {
|
||||
return Err(Status::ConditionsOfUseNotSatisfied);
|
||||
@@ -24,7 +25,12 @@ where
|
||||
self.deselect()
|
||||
}
|
||||
|
||||
fn call(&mut self, interface: Interface, apdu: CommandView<'_>, reply: &mut Data<R>) -> Result {
|
||||
fn call(
|
||||
&mut self,
|
||||
interface: Interface,
|
||||
apdu: CommandView<'_>,
|
||||
reply: &mut VecView<u8>,
|
||||
) -> Result {
|
||||
if interface != Interface::Contact {
|
||||
return Err(Status::ConditionsOfUseNotSatisfied);
|
||||
}
|
||||
|
||||
+27
-38
@@ -21,6 +21,7 @@ mod reply;
|
||||
pub mod state;
|
||||
mod tlv;
|
||||
|
||||
use heapless::VecView;
|
||||
pub use piv_types::{AsymmetricAlgorithms, Pin, Puk};
|
||||
use trussed_chunked::ChunkedClient;
|
||||
use trussed_hpke::HpkeClient;
|
||||
@@ -35,7 +36,7 @@ use core::convert::TryInto;
|
||||
|
||||
use flexiber::EncodableHeapless;
|
||||
use heapless_bytes::Bytes;
|
||||
use iso7816::{Data, Status};
|
||||
use iso7816::Status;
|
||||
use trussed_auth::AuthClient;
|
||||
use trussed_core::mechanisms::Tdes;
|
||||
use trussed_core::types::{KeySerialization, Location, Mechanism, PathBuf, StorageAttributes};
|
||||
@@ -136,7 +137,7 @@ where
|
||||
// The way apdu-dispatch currently works, this would deselect, resetting security indicators.
|
||||
pub fn deselect(&mut self) {}
|
||||
|
||||
pub fn select<const R: usize>(&mut self, mut reply: Reply<'_, R>) -> Result {
|
||||
pub fn select(&mut self, mut reply: Reply<'_>) -> Result {
|
||||
use piv_types::Algorithms::*;
|
||||
info!("selecting PIV maybe");
|
||||
|
||||
@@ -154,10 +155,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn respond<const R: usize>(
|
||||
pub fn respond(
|
||||
&mut self,
|
||||
command: iso7816::command::CommandView<'_>,
|
||||
reply: &mut Data<R>,
|
||||
reply: &mut VecView<u8>,
|
||||
) -> Result {
|
||||
let just_verified = self.state.volatile.app_security_status.pin_just_verified;
|
||||
self.state.volatile.app_security_status.pin_just_verified = false;
|
||||
@@ -192,11 +193,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn yubico_piv_extension<const R: usize>(
|
||||
pub fn yubico_piv_extension(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
instruction: YubicoPivExtension,
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
) -> Result {
|
||||
info!("yubico extension: {:?}", &instruction);
|
||||
match instruction {
|
||||
@@ -256,11 +257,11 @@ where
|
||||
}
|
||||
|
||||
impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
pub fn yubico_set_administration_key<const R: usize>(
|
||||
pub fn yubico_set_administration_key(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
_touch_policy: TouchPolicy,
|
||||
_reply: Reply<'_, R>,
|
||||
_reply: Reply<'_>,
|
||||
) -> Result {
|
||||
// cmd := apdu{
|
||||
// instruction: insSetMGMKey,
|
||||
@@ -424,12 +425,12 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
// - 9000, 61XX for success
|
||||
// - 6982 security status
|
||||
// - 6A80, 6A86 for data, P1/P2 issue
|
||||
pub fn general_authenticate<const R: usize>(
|
||||
pub fn general_authenticate(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
data: &[u8],
|
||||
just_verified: bool,
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
) -> Result {
|
||||
// For "SSH", we need implement A.4.2 in SP-800-73-4 Part 2, ECDSA signatures
|
||||
//
|
||||
@@ -562,11 +563,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(self.state.persistent.keys.administration)
|
||||
}
|
||||
|
||||
fn single_auth_1<const R: usize>(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
mut reply: Reply<'_, R>,
|
||||
) -> Result {
|
||||
fn single_auth_1(&mut self, auth: GeneralAuthenticate, mut reply: Reply<'_>) -> Result {
|
||||
info!("Single auth 1");
|
||||
let key = self.validate_auth_management(auth)?;
|
||||
let plaintext = syscall!(self.trussed.random_bytes(key.alg.challenge_length())).bytes;
|
||||
@@ -576,7 +573,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
.encrypt(key.alg.mechanism(), key.id, &plaintext, &[], None))
|
||||
.ciphertext;
|
||||
self.state.volatile.command_cache = Some(CommandCache::SingleAuthChallengeReference(
|
||||
Bytes::from_slice(&ciphertext).unwrap(),
|
||||
Bytes::try_from(&*ciphertext).unwrap(),
|
||||
));
|
||||
|
||||
reply.expand(&[0x7C])?;
|
||||
@@ -619,11 +616,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mutual_auth_1<const R: usize>(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
mut reply: Reply<'_, R>,
|
||||
) -> Result {
|
||||
fn mutual_auth_1(&mut self, auth: GeneralAuthenticate, mut reply: Reply<'_>) -> Result {
|
||||
info!("Mutual auth 1");
|
||||
let key = self.validate_auth_management(auth)?;
|
||||
let plaintext = syscall!(self.trussed.random_bytes(key.alg.challenge_length())).bytes;
|
||||
@@ -635,7 +628,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
.ciphertext;
|
||||
|
||||
self.state.volatile.command_cache = Some(CommandCache::MutualAuthWitnessReference(
|
||||
Bytes::from_slice(&plaintext).unwrap(),
|
||||
Bytes::try_from(&*plaintext).unwrap(),
|
||||
));
|
||||
|
||||
reply.expand(&[0x7C])?;
|
||||
@@ -649,12 +642,12 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mutual_auth_2<const R: usize>(
|
||||
fn mutual_auth_2(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
response: &[u8],
|
||||
challenge: &[u8],
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
) -> Result {
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
@@ -703,12 +696,12 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
}
|
||||
|
||||
// Sign a message. For RSA, since the key is exposed as a raw key, so it can also be used for decryption
|
||||
fn sign<const R: usize>(
|
||||
fn sign(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
message: &[u8],
|
||||
just_verified: bool,
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
) -> Result {
|
||||
debug!("Request for sign, data length: {}, data:", message.len());
|
||||
// error!("{}", delog::hexstr!(message));
|
||||
@@ -754,11 +747,11 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
)
|
||||
}
|
||||
|
||||
fn key_agreement<const R: usize>(
|
||||
fn key_agreement(
|
||||
&mut self,
|
||||
auth: GeneralAuthenticate,
|
||||
data: &[u8],
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
just_verified: bool,
|
||||
) -> Result {
|
||||
info!("Request for exponentiation");
|
||||
@@ -839,11 +832,11 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_asymmetric_keypair<const R: usize>(
|
||||
pub fn generate_asymmetric_keypair(
|
||||
&mut self,
|
||||
reference: GenerateKeyReference,
|
||||
data: &[u8],
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
) -> Result {
|
||||
if !self
|
||||
.state
|
||||
@@ -967,11 +960,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_data<const R: usize>(
|
||||
&mut self,
|
||||
container: Container,
|
||||
mut reply: Reply<'_, R>,
|
||||
) -> Result {
|
||||
fn get_data(&mut self, container: Container, mut reply: Reply<'_>) -> Result {
|
||||
let read_valid =
|
||||
self.state
|
||||
.volatile
|
||||
@@ -1045,7 +1034,7 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_key_history_object<const R: usize>(&mut self, mut reply: Reply<'_, R>) -> Result {
|
||||
fn get_key_history_object(&mut self, mut reply: Reply<'_>) -> Result {
|
||||
use state::ContainerStorage;
|
||||
|
||||
let mut num_certs = 0;
|
||||
@@ -1063,12 +1052,12 @@ impl<T: Client> LoadedAuthenticator<'_, T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_asymmetric_key<const R: usize>(
|
||||
fn import_asymmetric_key(
|
||||
&mut self,
|
||||
algo: AsymmetricAlgorithms,
|
||||
key: AsymmetricKeyReference,
|
||||
#[cfg_attr(not(feature = "rsa"), allow(unused))] data: &[u8],
|
||||
mut _reply: Reply<'_, R>,
|
||||
mut _reply: Reply<'_>,
|
||||
) -> Result {
|
||||
if !self
|
||||
.state
|
||||
|
||||
+6
-6
@@ -3,22 +3,22 @@ use iso7816::Status;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Reply<'v, const R: usize>(pub &'v mut heapless::Vec<u8, R>);
|
||||
pub struct Reply<'v>(pub &'v mut heapless::VecView<u8>);
|
||||
|
||||
impl<'v, const R: usize> Deref for Reply<'v, R> {
|
||||
type Target = &'v mut heapless::Vec<u8, R>;
|
||||
impl<'v> Deref for Reply<'v> {
|
||||
type Target = &'v mut heapless::VecView<u8>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const R: usize> DerefMut for Reply<'_, R> {
|
||||
impl DerefMut for Reply<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const R: usize> Reply<'_, R> {
|
||||
impl Reply<'_> {
|
||||
/// Extend the reply and return an error otherwise
|
||||
/// The MoreAvailable and GET RESPONSE mechanisms are handled by adpu_dispatch
|
||||
///
|
||||
@@ -82,7 +82,7 @@ impl<const R: usize> Reply<'_, R> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lend(&mut self) -> Reply<'_, R> {
|
||||
pub fn lend(&mut self) -> Reply<'_> {
|
||||
Reply(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
+23
-23
@@ -200,8 +200,8 @@ impl Keys {
|
||||
storage,
|
||||
user_public_key,
|
||||
new.id,
|
||||
Bytes::from_slice(key.name().as_str().as_bytes()).unwrap(),
|
||||
Bytes::from_slice(HPKE_SEALKEY_REFERENCE_INFO).unwrap(),
|
||||
Bytes::try_from(key.name().as_str().as_bytes()).unwrap(),
|
||||
Bytes::try_from(HPKE_SEALKEY_REFERENCE_INFO).unwrap(),
|
||||
));
|
||||
|
||||
KeyOrEncryptedWithAlg::Encrypted(match key {
|
||||
@@ -382,8 +382,8 @@ impl LoadedState<'_> {
|
||||
key.name().into(),
|
||||
options.storage,
|
||||
Location::Volatile,
|
||||
Bytes::from_slice(key.name().as_str().as_bytes()).unwrap(),
|
||||
Bytes::from_slice(HPKE_SEALKEY_REFERENCE_INFO).unwrap(),
|
||||
Bytes::try_from(key.name().as_str().as_bytes()).unwrap(),
|
||||
Bytes::try_from(HPKE_SEALKEY_REFERENCE_INFO).unwrap(),
|
||||
))
|
||||
.map_err(|_err| {
|
||||
error!("Failed to unseal key: {_err:?}");
|
||||
@@ -460,7 +460,7 @@ impl Volatile {
|
||||
client: &mut T,
|
||||
) -> &'this mut PinVerified {
|
||||
self.clear_pin_verified(client);
|
||||
let pin = Bytes::from_slice(&value.0).expect("Convertion of static array");
|
||||
let pin = Bytes::try_from(&value.0).expect("Convertion of static array");
|
||||
let syscall_res = try_syscall!(client.get_pin_key(PinType::UserPin, pin));
|
||||
let pin_key = match syscall_res {
|
||||
Err(_err) => {
|
||||
@@ -532,8 +532,8 @@ impl Volatile {
|
||||
data_encryption_sealed_key_path,
|
||||
options.storage,
|
||||
Location::Volatile,
|
||||
Bytes::from_slice(ContainerStorage(container).path_key_str().as_bytes()).unwrap(),
|
||||
Bytes::from_slice(HPKE_SEALKEY_CONTAINER_INFO).unwrap(),
|
||||
Bytes::try_from(ContainerStorage(container).path_key_str().as_bytes()).unwrap(),
|
||||
Bytes::try_from(HPKE_SEALKEY_CONTAINER_INFO).unwrap(),
|
||||
)) else {
|
||||
return Ok(ReadValid::EncryptedNotFound);
|
||||
};
|
||||
@@ -680,7 +680,7 @@ impl Persistent {
|
||||
value: &Puk,
|
||||
client: &mut T,
|
||||
) -> Result<Option<KeyId>, Status> {
|
||||
let puk = Bytes::from_slice(&value.0).expect("Convertion of static array");
|
||||
let puk = Bytes::try_from(&value.0).expect("Convertion of static array");
|
||||
try_syscall!(client.get_pin_key(PinType::Puk, puk))
|
||||
.map(|r| r.result)
|
||||
.map_err(|_err| {
|
||||
@@ -695,8 +695,8 @@ impl Persistent {
|
||||
new_value: &Pin,
|
||||
client: &mut T,
|
||||
) -> bool {
|
||||
let old_pin = Bytes::from_slice(&old_value.0).expect("Convertion of static array");
|
||||
let new_pin = Bytes::from_slice(&new_value.0).expect("Convertion of static array");
|
||||
let old_pin = Bytes::try_from(&old_value.0).expect("Convertion of static array");
|
||||
let new_pin = Bytes::try_from(&new_value.0).expect("Convertion of static array");
|
||||
try_syscall!(client.change_pin(PinType::UserPin, old_pin, new_pin))
|
||||
.map(|r| r.success)
|
||||
.unwrap_or(false)
|
||||
@@ -708,8 +708,8 @@ impl Persistent {
|
||||
new_value: &Puk,
|
||||
client: &mut T,
|
||||
) -> bool {
|
||||
let old_puk = Bytes::from_slice(&old_value.0).expect("Convertion of static array");
|
||||
let new_puk = Bytes::from_slice(&new_value.0).expect("Convertion of static array");
|
||||
let old_puk = Bytes::try_from(&old_value.0).expect("Convertion of static array");
|
||||
let new_puk = Bytes::try_from(&new_value.0).expect("Convertion of static array");
|
||||
try_syscall!(client.change_pin(PinType::Puk, old_puk, new_puk))
|
||||
.map(|r| r.success)
|
||||
.unwrap_or(false)
|
||||
@@ -721,7 +721,7 @@ impl Persistent {
|
||||
old_key: KeyId,
|
||||
client: &mut T,
|
||||
) -> Result<(), Status> {
|
||||
let new_pin = Bytes::from_slice(&new_pin.0).expect("Convertion of static array");
|
||||
let new_pin = Bytes::try_from(&new_pin.0).expect("Convertion of static array");
|
||||
try_syscall!(client.set_pin_with_key(
|
||||
PinType::UserPin,
|
||||
new_pin,
|
||||
@@ -740,7 +740,7 @@ impl Persistent {
|
||||
new_puk: Puk,
|
||||
client: &mut T,
|
||||
) -> Result<(), Status> {
|
||||
let new_puk = Bytes::from_slice(&new_puk.0).expect("Convertion of static array");
|
||||
let new_puk = Bytes::try_from(&new_puk.0).expect("Convertion of static array");
|
||||
try_syscall!(client.set_pin(PinType::Puk, new_puk, Some(Self::PUK_RETRIES_DEFAULT), true))
|
||||
.map_err(|_err| {
|
||||
error!("Failed to set puk");
|
||||
@@ -859,7 +859,7 @@ impl Persistent {
|
||||
|
||||
fn init_pins<T: crate::Client>(client: &mut T, options: &crate::Options) -> Result<(), Status> {
|
||||
let default_pin =
|
||||
Bytes::from_slice(&Self::DEFAULT_PIN.0).expect("Convertion of static array");
|
||||
Bytes::try_from(&Self::DEFAULT_PIN.0).expect("Convertion of static array");
|
||||
try_syscall!(client.set_pin(
|
||||
PinType::UserPin,
|
||||
default_pin.clone(),
|
||||
@@ -871,7 +871,7 @@ impl Persistent {
|
||||
Status::UnspecifiedPersistentExecutionError
|
||||
})?;
|
||||
let default_puk =
|
||||
Bytes::from_slice(&Self::DEFAULT_PUK.0).expect("Convertion of static array");
|
||||
Bytes::try_from(&Self::DEFAULT_PUK.0).expect("Convertion of static array");
|
||||
try_syscall!(client.set_pin(
|
||||
PinType::Puk,
|
||||
default_puk.clone(),
|
||||
@@ -933,7 +933,7 @@ impl Persistent {
|
||||
syscall!(client.write_file(
|
||||
options.storage,
|
||||
PathBuf::from(USER_PUBLIC_KEY),
|
||||
Bytes::from_slice(&key).unwrap(),
|
||||
Bytes::try_from(&*key).unwrap(),
|
||||
None
|
||||
));
|
||||
|
||||
@@ -1068,11 +1068,11 @@ fn load_if_exists(
|
||||
}
|
||||
|
||||
/// Returns false if the file does not exist
|
||||
fn load_if_exists_streaming<const R: usize>(
|
||||
fn load_if_exists_streaming(
|
||||
client: &mut impl crate::Client,
|
||||
location: Location,
|
||||
path: &PathBuf,
|
||||
mut buffer: Reply<'_, R>,
|
||||
mut buffer: Reply<'_>,
|
||||
encryption: Option<KeyId>,
|
||||
) -> Result<bool, Status> {
|
||||
let offset = buffer.len();
|
||||
@@ -1218,11 +1218,11 @@ impl ContainerStorage {
|
||||
}
|
||||
|
||||
// Write the length of the file and write
|
||||
pub fn load<const R: usize>(
|
||||
pub fn load(
|
||||
self,
|
||||
client: &mut impl crate::Client,
|
||||
storage: Location,
|
||||
mut reply: Reply<'_, R>,
|
||||
mut reply: Reply<'_>,
|
||||
read_valid: ReadValid,
|
||||
) -> Result<bool, Status> {
|
||||
let encryption = match read_valid {
|
||||
@@ -1292,8 +1292,8 @@ impl ContainerStorage {
|
||||
storage,
|
||||
user_public_key,
|
||||
key_to_seal,
|
||||
Bytes::from_slice(self.path_key_str().as_bytes()).unwrap(),
|
||||
Bytes::from_slice(HPKE_SEALKEY_CONTAINER_INFO).unwrap(),
|
||||
Bytes::try_from(self.path_key_str().as_bytes()).unwrap(),
|
||||
Bytes::try_from(HPKE_SEALKEY_CONTAINER_INFO).unwrap(),
|
||||
));
|
||||
syscall!(client.delete(user_public_key));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user