mirror of
https://github.com/zerotier/zssp.git
synced 2026-05-22 16:28:40 -07:00
@@ -19,11 +19,11 @@ use rand_core::OsRng;
|
||||
use rand_core::RngCore;
|
||||
|
||||
use zssp_proto::application::{
|
||||
AcceptAction, ApplicationLayer, RatchetState, RatchetStates, RatchetUpdate, Settings, RATCHET_SIZE, CryptoLayer,
|
||||
AcceptAction, ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate, Settings, RATCHET_SIZE,
|
||||
};
|
||||
use zssp_proto::crypto::P384KeyPair;
|
||||
use zssp_proto::crypto_impl::{
|
||||
Aes256Crate, AesGcmCrate, RustKyber1024PrivateKey, P384CrateKeyPair, P384CratePublicKey, Sha512Crate,
|
||||
Aes256Crate, AesGcmCrate, Kyber1024CratePrivateKey, P384CrateKeyPair, P384CratePublicKey, Sha512Crate,
|
||||
};
|
||||
use zssp_proto::Session;
|
||||
|
||||
@@ -61,7 +61,7 @@ impl CryptoLayer for TestApplication {
|
||||
type Hash = Sha512Crate;
|
||||
type PublicKey = P384CratePublicKey;
|
||||
type KeyPair = P384CrateKeyPair;
|
||||
type Kem = RustKyber1024PrivateKey;
|
||||
type Kem = Kyber1024CratePrivateKey;
|
||||
|
||||
type SessionData = u128;
|
||||
}
|
||||
@@ -77,7 +77,11 @@ impl ApplicationLayer for &mut TestApplication {
|
||||
true
|
||||
}
|
||||
|
||||
fn check_accept_session(&mut self, remote_static_key: &P384CratePublicKey, identity: &[u8]) -> AcceptAction<TestApplication> {
|
||||
fn check_accept_session(
|
||||
&mut self,
|
||||
remote_static_key: &P384CratePublicKey,
|
||||
identity: &[u8],
|
||||
) -> AcceptAction<TestApplication> {
|
||||
AcceptAction {
|
||||
session_data: Some(1),
|
||||
responder_disallows_downgrade: true,
|
||||
@@ -305,7 +309,8 @@ fn bob_main(
|
||||
}
|
||||
|
||||
if current_time >= next_service {
|
||||
next_service = current_time + context.service(&mut bob_app, |_| Some((|b| bob_out.send(b).is_ok(), TEST_MTU)));
|
||||
next_service =
|
||||
current_time + context.service(&mut bob_app, |_| Some((|b| bob_out.send(b).is_ok(), TEST_MTU)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,11 +326,7 @@ fn core(time: u64, packet_success_rate: u32) {
|
||||
};
|
||||
let bob_keypair = P384CrateKeyPair::generate(&mut OsRng);
|
||||
let bob_pubkey = bob_keypair.public_key();
|
||||
let bob_app = TestApplication {
|
||||
time: Instant::now(),
|
||||
name: "bob",
|
||||
ratchets: Ratchets::new(),
|
||||
};
|
||||
let bob_app = TestApplication { time: Instant::now(), name: "bob", ratchets: Ratchets::new() };
|
||||
|
||||
let (alice_out, bob_in) = mpsc::sync_channel::<Vec<u8>>(256);
|
||||
let (bob_out, alice_in) = mpsc::sync_channel::<Vec<u8>>(256);
|
||||
@@ -0,0 +1,218 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* (c) ZeroTier, Inc.
|
||||
* https://www.zerotier.com/
|
||||
*/
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::convert::Infallible;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use zssp_proto::application::{
|
||||
AcceptAction, ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE,
|
||||
};
|
||||
use zssp_proto::crypto::{rand_core::OsRng, P384KeyPair};
|
||||
use zssp_proto::crypto_impl::{
|
||||
Aes256Crate, AesGcmCrate, Kyber1024CratePrivateKey, P384CrateKeyPair, P384CratePublicKey, Sha512Crate,
|
||||
};
|
||||
use zssp_proto::{Context, Session};
|
||||
|
||||
/// The MTU can go as low as 128 bytes, and it does not have to be constant either!
|
||||
const TEST_MTU: usize = 512;
|
||||
|
||||
struct MyApp {
|
||||
time: Instant,
|
||||
remote_peers_session: Option<Arc<Session<Self>>>,
|
||||
}
|
||||
|
||||
/// We specify which crypto implementations to use here, you can use those provided by the
|
||||
/// `crypto_impl` module or provide your own.
|
||||
impl CryptoLayer for MyApp {
|
||||
type Rng = OsRng;
|
||||
type Prp = Aes256Crate;
|
||||
type Aead = AesGcmCrate;
|
||||
type Hash = Sha512Crate;
|
||||
type PublicKey = P384CratePublicKey;
|
||||
type KeyPair = P384CrateKeyPair;
|
||||
type Kem = Kyber1024CratePrivateKey;
|
||||
|
||||
type SessionData = ();
|
||||
}
|
||||
/// In this example for simplicity we won't be hooking up ratchet keys to a filesystem backend.
|
||||
/// They are dropped and peers ignore if they are missing.
|
||||
#[allow(unused)]
|
||||
impl ApplicationLayer for &mut MyApp {
|
||||
type Crypto = MyApp;
|
||||
type StorageError = Infallible;
|
||||
|
||||
fn hello_requires_recognized_ratchet(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn initiator_disallows_downgrade(&mut self, session: &Arc<Session<MyApp>>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn check_accept_session(&mut self, remote_static_key: &P384CratePublicKey, identity: &[u8]) -> AcceptAction<MyApp> {
|
||||
self.remote_peers_session.take();
|
||||
AcceptAction {
|
||||
session_data: Some(()),
|
||||
responder_disallows_downgrade: false,
|
||||
responder_silently_rejects: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_by_fingerprint(
|
||||
&mut self,
|
||||
ratchet_fingerprint: &[u8; RATCHET_SIZE],
|
||||
) -> Result<Option<RatchetState>, Infallible> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn restore_by_identity(
|
||||
&mut self,
|
||||
remote_static_key: &P384CratePublicKey,
|
||||
session_data: &(),
|
||||
) -> Result<Option<RatchetStates>, Infallible> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn save_ratchet_state(
|
||||
&mut self,
|
||||
remote_static_key: &P384CratePublicKey,
|
||||
session_data: &(),
|
||||
update_data: RatchetUpdate<'_>,
|
||||
) -> Result<(), Infallible> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn time(&mut self) -> i64 {
|
||||
self.time.elapsed().as_millis() as i64
|
||||
}
|
||||
}
|
||||
|
||||
/// In this example protocol the two peers simply bounce the message "ping" and "pong" back and
|
||||
/// forth forever.
|
||||
fn process_message(decrypted_message: &[u8], peer_name: &'static str) -> Option<Vec<u8>> {
|
||||
match decrypted_message {
|
||||
b"ping" => {
|
||||
println!("[{}]: pong", peer_name);
|
||||
Some(b"pong".to_vec())
|
||||
}
|
||||
b"pong" => {
|
||||
println!("[{}]: ping", peer_name);
|
||||
Some(b"ping".to_vec())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// For simplicity's sake this function assumes there is only one remote peer we can talk to.
|
||||
fn receive(
|
||||
context: &mut Context<MyApp>,
|
||||
app: &RefCell<MyApp>,
|
||||
peer_name: &'static str,
|
||||
recv_queue: &RefCell<Vec<Vec<u8>>>,
|
||||
send_queue: &RefCell<Vec<Vec<u8>>>,
|
||||
) {
|
||||
if let Some(recv_packet) = recv_queue.borrow_mut().pop() {
|
||||
let push_onto_send_queue = |packet: Vec<u8>| {
|
||||
assert!(packet.len() <= TEST_MTU);
|
||||
send_queue.borrow_mut().push(packet);
|
||||
true
|
||||
};
|
||||
|
||||
use zssp_proto::result::ReceiveOk::*;
|
||||
use zssp_proto::result::SessionEvent::*;
|
||||
let result = context.receive(
|
||||
app.borrow_mut().deref_mut(),
|
||||
push_onto_send_queue,
|
||||
TEST_MTU,
|
||||
|_| Some((push_onto_send_queue, TEST_MTU)),
|
||||
&0,
|
||||
recv_packet,
|
||||
);
|
||||
let (session, reply_message) = match result {
|
||||
Ok(Unassociated) => return,
|
||||
Ok(Session(session, event)) => match event {
|
||||
NewSession | NewDowngradedSession => {
|
||||
println!("[{}]: session received", peer_name);
|
||||
app.borrow_mut().remote_peers_session = Some(session);
|
||||
return;
|
||||
}
|
||||
Data(data) => {
|
||||
if let Some(reply) = process_message(&data, peer_name) {
|
||||
(session, reply)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Control => return,
|
||||
Established => {
|
||||
println!("[{}]: ping", peer_name);
|
||||
(session, b"ping".to_vec())
|
||||
}
|
||||
Rejected => return,
|
||||
DowngradedRatchetKey => return,
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
context
|
||||
.send(&session, push_onto_send_queue, TEST_MTU, reply_message)
|
||||
.unwrap();
|
||||
|
||||
// This example does not properly track time so we just call service on every update.
|
||||
context.service(app.borrow_mut().deref_mut(), |_| Some((push_onto_send_queue, TEST_MTU)));
|
||||
}
|
||||
}
|
||||
|
||||
/// We create two peers, Alice and Bob, then we have Alice initiate a ZSSP session with Bob, and
|
||||
/// then we have Alice send the message "ping" to Bob.
|
||||
/// Bob replies with the message "pong", which Alice replies to with "ping" and so on forever.
|
||||
fn main() {
|
||||
let alice_keypair = P384CrateKeyPair::generate(&mut OsRng);
|
||||
let alice_app = RefCell::new(MyApp { time: Instant::now(), remote_peers_session: None });
|
||||
let alice_send_queue = RefCell::new(Vec::<Vec<u8>>::new());
|
||||
let mut alice_context = Context::<MyApp>::new(alice_keypair, OsRng);
|
||||
|
||||
let bob_keypair = P384CrateKeyPair::generate(&mut OsRng);
|
||||
let bob_pubkey = bob_keypair.public_key();
|
||||
let bob_app = RefCell::new(MyApp { time: Instant::now(), remote_peers_session: None });
|
||||
let bob_send_queue = RefCell::new(Vec::<Vec<u8>>::new());
|
||||
let mut bob_context = Context::<MyApp>::new(bob_keypair, OsRng);
|
||||
|
||||
let result = alice_context.open(
|
||||
alice_app.borrow_mut().deref_mut(),
|
||||
|packet| {
|
||||
assert!(packet.len() <= TEST_MTU);
|
||||
alice_send_queue.borrow_mut().push(packet);
|
||||
true
|
||||
},
|
||||
TEST_MTU,
|
||||
bob_pubkey,
|
||||
(),
|
||||
Vec::new(),
|
||||
);
|
||||
alice_app.borrow_mut().remote_peers_session = Some(result.unwrap());
|
||||
println!("[Alice]: session opened");
|
||||
|
||||
for _ in 0..16 {
|
||||
receive(&mut bob_context, &bob_app, "Bob", &alice_send_queue, &bob_send_queue);
|
||||
|
||||
receive(
|
||||
&mut alice_context,
|
||||
&alice_app,
|
||||
"Alice",
|
||||
&bob_send_queue,
|
||||
&alice_send_queue,
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -183,7 +183,11 @@ pub trait ApplicationLayer: Sized {
|
||||
/// To prevent desync, if this function specifies that we should connect, no other open session
|
||||
/// with the same remote peer must exist. Drop or call expire on any pre-existing sessions
|
||||
/// before returning.
|
||||
fn check_accept_session(&mut self, remote_static_key: &<Self::Crypto as CryptoLayer>::PublicKey, identity: &[u8]) -> AcceptAction<Self::Crypto>;
|
||||
fn check_accept_session(
|
||||
&mut self,
|
||||
remote_static_key: &<Self::Crypto as CryptoLayer>::PublicKey,
|
||||
identity: &[u8],
|
||||
) -> AcceptAction<Self::Crypto>;
|
||||
|
||||
/// Lookup a specific ratchet state based on its ratchet fingerprint.
|
||||
/// This function will be called whenever Alice attempts to connect to us with a non-empty
|
||||
@@ -241,7 +245,8 @@ pub trait ApplicationLayer: Sized {
|
||||
/// These are provided for debugging, logging or metrics purposes, and must be used for
|
||||
/// nothing else. Do not base protocol-level decisions upon the events passed to this function.
|
||||
#[cfg(feature = "logging")]
|
||||
fn event_log(&mut self, event: LogEvent<'_, Self::Crypto>);
|
||||
#[allow(unused)]
|
||||
fn event_log(&mut self, event: LogEvent<'_, Self::Crypto>) {}
|
||||
}
|
||||
|
||||
/// A collection of fields specifying how to complete the key exchange with a specific remote peer,
|
||||
|
||||
+21
-6
@@ -101,7 +101,10 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
|
||||
static_remote_key: Crypto::PublicKey,
|
||||
session_data: Crypto::SessionData,
|
||||
identity: Vec<u8>,
|
||||
) -> Result<Arc<Session<Crypto>>, OpenError<App::StorageError>> where App: ApplicationLayer<Crypto = Crypto>{
|
||||
) -> Result<Arc<Session<Crypto>>, OpenError<App::StorageError>>
|
||||
where
|
||||
App: ApplicationLayer<Crypto = Crypto>,
|
||||
{
|
||||
mtu = mtu.max(MIN_TRANSPORT_MTU);
|
||||
if identity.len() > IDENTITY_MAX_SIZE {
|
||||
return Err(OpenError::IdentityTooLarge);
|
||||
@@ -138,7 +141,10 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
|
||||
send_to: impl FnOnce(&Arc<Session<Crypto>>) -> Option<(SendFn, usize)>,
|
||||
remote_address: &impl Hash,
|
||||
raw_fragment: Vec<u8>,
|
||||
) -> Result<ReceiveOk<Crypto>, ReceiveError<App::StorageError>> where App: ApplicationLayer<Crypto = Crypto> {
|
||||
) -> Result<ReceiveOk<Crypto>, ReceiveError<App::StorageError>>
|
||||
where
|
||||
App: ApplicationLayer<Crypto = Crypto>,
|
||||
{
|
||||
use crate::result::FaultType::*;
|
||||
send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU);
|
||||
let ctx = &self.0;
|
||||
@@ -201,7 +207,12 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
|
||||
let (p, _) = from_nonce(&pn);
|
||||
let ret = match p {
|
||||
PACKET_TYPE_DATA => {
|
||||
received_payload_in_place::<App>(&mut zeta, kid_recv, to_aes_nonce(&pn), &mut assembled_packet)?;
|
||||
received_payload_in_place::<App>(
|
||||
&mut zeta,
|
||||
kid_recv,
|
||||
to_aes_nonce(&pn),
|
||||
&mut assembled_packet,
|
||||
)?;
|
||||
SessionEvent::Data(assembled_packet)
|
||||
}
|
||||
PACKET_TYPE_HANDSHAKE_RESPONSE => {
|
||||
@@ -334,11 +345,12 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
|
||||
},
|
||||
)?;
|
||||
log!(app, X3IsAuthSentKeyConfirm(&session));
|
||||
Ok(ReceiveOk::Session(session, if should_warn_missing_ratchet {
|
||||
let ret = if should_warn_missing_ratchet {
|
||||
SessionEvent::NewDowngradedSession
|
||||
} else {
|
||||
SessionEvent::NewSession
|
||||
}))
|
||||
};
|
||||
Ok(ReceiveOk::Session(session, ret))
|
||||
} else {
|
||||
Ok(ReceiveOk::Unassociated)
|
||||
}
|
||||
@@ -478,7 +490,10 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
|
||||
&mut self,
|
||||
mut app: App,
|
||||
mut send_to: impl FnMut(&Arc<Session<Crypto>>) -> Option<(SendFn, usize)>,
|
||||
) -> i64 where App: ApplicationLayer<Crypto = Crypto> {
|
||||
) -> i64
|
||||
where
|
||||
App: ApplicationLayer<Crypto = Crypto>,
|
||||
{
|
||||
let ctx = &self.0;
|
||||
let sessions = ctx.sessions.borrow_mut();
|
||||
let current_time = app.time();
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::crypto::*;
|
||||
|
||||
/// A wrapper for a buffer the size of a pqc_kyber secret key.
|
||||
/// The crate `pqc_kyber` is low level and operates directly on buffers of bytes.
|
||||
pub type RustKyber1024PrivateKey = Zeroizing<[u8; pqc_kyber::KYBER_SECRETKEYBYTES]>;
|
||||
impl<Rng: RngCore + CryptoRng> Kyber1024PrivateKey<Rng> for RustKyber1024PrivateKey {
|
||||
pub type Kyber1024CratePrivateKey = Zeroizing<[u8; pqc_kyber::KYBER_SECRETKEYBYTES]>;
|
||||
impl<Rng: RngCore + CryptoRng> Kyber1024PrivateKey<Rng> for Kyber1024CratePrivateKey {
|
||||
fn generate(rng: &mut Rng) -> (Self, [u8; KYBER_PUBLIC_KEY_SIZE]) {
|
||||
let keypair = pqc_kyber::keypair(rng);
|
||||
(Zeroizing::new(keypair.secret), keypair.public)
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::application::{CryptoLayer, ApplicationLayer};
|
||||
use crate::application::{ApplicationLayer, CryptoLayer};
|
||||
use crate::crypto::{Aes256Prp, AES_256_KEY_SIZE};
|
||||
use crate::proto::*;
|
||||
use crate::result::{byzantine_fault, ReceiveError};
|
||||
|
||||
+25
-7
@@ -7,7 +7,7 @@ use std::sync::{Arc, Weak};
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::application::{ApplicationLayer, RatchetState, RatchetStates, RatchetUpdate, CryptoLayer};
|
||||
use crate::application::{ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate};
|
||||
use crate::challenge::{gen_null_response, respond_to_challenge_in_place};
|
||||
use crate::context::{log, ContextInner, SessionMap};
|
||||
use crate::crypto::*;
|
||||
@@ -700,8 +700,8 @@ pub(crate) fn received_x3_trans<App: ApplicationLayer>(
|
||||
if !noise.decrypt_and_hash_in_place(to_nonce(PACKET_TYPE_HANDSHAKE_COMPLETION, 1), &mut x3[i..j], tag) {
|
||||
return Err(byzantine_fault!(FailedAuth, true));
|
||||
}
|
||||
let s_remote =
|
||||
<App::Crypto as CryptoLayer>::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap()).ok_or(byzantine_fault!(FailedAuth, true))?;
|
||||
let s_remote = <App::Crypto as CryptoLayer>::PublicKey::from_bytes((&x3[i..j]).try_into().unwrap())
|
||||
.ok_or(byzantine_fault!(FailedAuth, true))?;
|
||||
i = k;
|
||||
// Process message pattern 3 se token.
|
||||
noise
|
||||
@@ -888,7 +888,10 @@ pub(crate) fn received_c1_trans<App: ApplicationLayer>(
|
||||
zeta.ratchet_state2 = None;
|
||||
zeta.key_index ^= true;
|
||||
let r = rng.borrow_mut().next_u64() % <App::Crypto as CryptoLayer>::SETTINGS.rekey_time_max_jitter;
|
||||
zeta.timeout_timer = app.time() + <App::Crypto as CryptoLayer>::SETTINGS.rekey_after_time.saturating_sub(r) as i64;
|
||||
zeta.timeout_timer = app.time()
|
||||
+ <App::Crypto as CryptoLayer>::SETTINGS
|
||||
.rekey_after_time
|
||||
.saturating_sub(r) as i64;
|
||||
zeta.resend_timer = i64::MAX;
|
||||
zeta.beta = ZetaAutomata::S2;
|
||||
}
|
||||
@@ -925,7 +928,13 @@ pub(crate) fn received_c2_trans<App: ApplicationLayer>(
|
||||
}
|
||||
|
||||
let tag = c2[..].try_into().unwrap();
|
||||
if !<App::Crypto as CryptoLayer>::Aead::decrypt_in_place(zeta.key_ref(false).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) {
|
||||
if !<App::Crypto as CryptoLayer>::Aead::decrypt_in_place(
|
||||
zeta.key_ref(false).recv.kek.as_ref().unwrap(),
|
||||
&n,
|
||||
None,
|
||||
&mut [],
|
||||
tag,
|
||||
) {
|
||||
return Err(byzantine_fault!(FailedAuth, true));
|
||||
}
|
||||
let (_, c) = from_nonce(&n);
|
||||
@@ -934,7 +943,10 @@ pub(crate) fn received_c2_trans<App: ApplicationLayer>(
|
||||
}
|
||||
|
||||
let r = rng.borrow_mut().next_u64() % <App::Crypto as CryptoLayer>::SETTINGS.rekey_time_max_jitter;
|
||||
zeta.timeout_timer = app.time() + <App::Crypto as CryptoLayer>::SETTINGS.rekey_after_time.saturating_sub(r) as i64;
|
||||
zeta.timeout_timer = app.time()
|
||||
+ <App::Crypto as CryptoLayer>::SETTINGS
|
||||
.rekey_after_time
|
||||
.saturating_sub(r) as i64;
|
||||
zeta.resend_timer = i64::MAX;
|
||||
zeta.beta = ZetaAutomata::S2;
|
||||
Ok(())
|
||||
@@ -957,7 +969,13 @@ pub(crate) fn received_d_trans<App: ApplicationLayer>(
|
||||
}
|
||||
|
||||
let tag = d[..].try_into().unwrap();
|
||||
if !<App::Crypto as CryptoLayer>::Aead::decrypt_in_place(zeta.key_ref(true).recv.kek.as_ref().unwrap(), &n, None, &mut [], tag) {
|
||||
if !<App::Crypto as CryptoLayer>::Aead::decrypt_in_place(
|
||||
zeta.key_ref(true).recv.kek.as_ref().unwrap(),
|
||||
&n,
|
||||
None,
|
||||
&mut [],
|
||||
tag,
|
||||
) {
|
||||
return Err(byzantine_fault!(FailedAuth, true));
|
||||
}
|
||||
let (_, c) = from_nonce(&n);
|
||||
|
||||
Reference in New Issue
Block a user