Repository: novifinancial/opaque-ke (rust)

Issue:
Declare a trait for `serialize`/`deserialize`
Many structures in this crate can be serialized/deserialized with the same interface. However, it's all just ad-hoc type methods. It would allow writing generic code if the methods were extracted to a trait.

Even better, implementing the `serde` interface is the standard way to serialize and deserialize code.

Pull request:
Implement serde serialization and deserialization
Provide a standard interface for serialization and deserialization using serde.

This makes it much easier to pass messages as part of protocols.

Fixes #166 

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c8b6896..b2576a9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -72,6 +72,20 @@ jobs:
       - uses: hecrj/setup-rust-action@v1
       - run: cargo test --verbose --features slow-hash --no-default-features --features ${{ matrix.backend_feature }}
 
+  serde-test:
+    name: Test on ${{ matrix.target }} with serde support
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        backend_feature:
+          - u64_backend
+          - u32_backend
+    steps:
+      - uses: actions/checkout@v2
+      - uses: hecrj/setup-rust-action@v1
+      - run: cargo test --verbose --features serialize --no-default-features --features ${{ matrix.backend_feature }}
+
 
   simple-login-test:
     runs-on: ubuntu-latest
diff --git a/Cargo.lock b/Cargo.lock
index fbeb2af..10b95e9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -40,6 +40,15 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "bit-set"
 version = "0.5.2"
@@ -562,6 +571,7 @@ version = "0.5.1-pre.1"
 dependencies = [
  "anyhow",
  "base64",
+ "bincode",
  "chacha20poly1305",
  "criterion",
  "curve25519-dalek",
@@ -577,6 +587,7 @@ dependencies = [
  "rand 0.8.3",
  "rustyline",
  "scrypt",
+ "serde",
  "serde_json",
  "sha2",
  "subtle",
diff --git a/Cargo.toml b/Cargo.toml
index ac6248e..5a5b17a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,13 +10,15 @@ edition = "2018"
 readme = "README.md"
 
 [features]
-default = ["u64_backend"]
+default = ["u64_backend", "serialize"]
 slow-hash = ["scrypt"]
 bench = []
 u64_backend = ["curve25519-dalek/u64_backend"]
 u32_backend = ["curve25519-dalek/u32_backend"]
+serialize = ["serde", "base64"]
 
 [dependencies]
+base64 = { version = "0.13", optional = true }
 curve25519-dalek = { version = "3.0.0", default-features = false, features = ["std"] }
 digest = "0.9.0"
 displaydoc = "0.1.7"
@@ -26,6 +28,7 @@ hkdf = "0.10.0"
 hmac = "0.10.1"
 rand = "0.8"
 scrypt = { version = "0.5.0", optional = true }
+serde = { version = "1", optional = true }
 subtle = { version = "2.3.0", default-features = false }
 thiserror = "1.0.22"
 zeroize = { version = "1.1.1", features = ["zeroize_derive"] }
@@ -33,6 +36,7 @@ zeroize = { version = "1.1.1", features = ["zeroize_derive"] }
 [dev-dependencies]
 anyhow = "1.0.35"
 base64 = "0.13.0"
+bincode = "1"
 chacha20poly1305 = "0.7.1"
 criterion = "0.3.3"
 hex = "0.4.2"
diff --git a/src/envelope.rs b/src/envelope.rs
index 50a909b..fcce8aa 100644
--- a/src/envelope.rs
+++ b/src/envelope.rs
@@ -179,7 +179,7 @@ impl<D: Hash> Envelope<D> {
         optional_ids: Option<(Vec<u8>, Vec<u8>)>,
     ) -> Result<(Self, GenericArray<u8, <D as Digest>::OutputSize>), InternalPakeError> {
         let aad = construct_aad(server_s_pk, &optional_ids);
-        Self::seal_raw(rng, key, &client_s_sk, &aad, mode_from_ids(&optional_ids))
+        Self::seal_raw(rng, key, client_s_sk, &aad, mode_from_ids(&optional_ids))
     }
 
     /// Uses a key to convert the plaintext into an envelope, authenticated by the aad field.
@@ -194,7 +194,7 @@ impl<D: Hash> Envelope<D> {
         let mut nonce = vec![0u8; NONCE_LEN];
         rng.fill_bytes(&mut nonce);
 
-        let h = Hkdf::<D>::new(Some(&nonce), &key);
+        let h = Hkdf::<D>::new(Some(&nonce), key);
         let mut xor_key = vec![0u8; plaintext.len()];
         let mut hmac_key = vec![0u8; Self::hmac_key_size()];
         let mut export_key = vec![0u8; Self::export_key_size()];
@@ -221,7 +221,7 @@ impl<D: Hash> Envelope<D> {
         let mut hmac =
             Hmac::<D>::new_varkey(&hmac_key).map_err(|_| InternalPakeError::HmacError)?;
         hmac.update(&inner_envelope.serialize());
-        hmac.update(&aad);
+        hmac.update(aad);
 
         let hmac_bytes = hmac.finalize().into_bytes();
 
@@ -266,7 +266,7 @@ impl<D: Hash> Envelope<D> {
         key: &[u8],
         aad: &[u8],
     ) -> Result<OpenedInnerEnvelope<D>, InternalPakeError> {
-        let h = Hkdf::<D>::new(Some(&self.inner_envelope.nonce), &key);
+        let h = Hkdf::<D>::new(Some(&self.inner_envelope.nonce), key);
         let mut xor_key = vec![0u8; self.inner_envelope.ciphertext.len()];
         let mut hmac_key = vec![0u8; Self::hmac_key_size()];
         let mut export_key = vec![0u8; Self::export_key_size()];
diff --git a/src/key_exchange/tripledh.rs b/src/key_exchange/tripledh.rs
index c3b483d..ec4e629 100644
--- a/src/key_exchange/tripledh.rs
+++ b/src/key_exchange/tripledh.rs
@@ -313,7 +313,7 @@ impl TryFrom<&[u8]> for Ke1Message {
         Ok(Self {
             client_nonce: GenericArray::clone_from_slice(&checked_nonce[..nonce_len]),
             info,
-            client_e_pk: Key::from_bytes(&checked_client_e_pk)?,
+            client_e_pk: Key::from_bytes(checked_client_e_pk)?,
         })
     }
 }
@@ -419,7 +419,7 @@ impl<HashLen: ArrayLength<u8>> TryFrom<&[u8]> for Ke2Message<HashLen> {
             server_nonce: GenericArray::clone_from_slice(&checked_nonce[..nonce_len]),
             server_e_pk: Key::from_bytes(&checked_server_e_pk[..KEY_LEN])?,
             e_info,
-            mac: GenericArray::clone_from_slice(&checked_mac),
+            mac: GenericArray::clone_from_slice(checked_mac),
         })
     }
 }
@@ -459,10 +459,10 @@ impl<HashLen: ArrayLength<u8>> TryFrom<&[u8]> for Ke3Message<HashLen> {
     type Error = PakeError;
 
     fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-        let checked_bytes = check_slice_size(&bytes, HashLen::to_usize(), "ke3_message")?;
+        let checked_bytes = check_slice_size(bytes, HashLen::to_usize(), "ke3_message")?;
 
         Ok(Self {
-            mac: GenericArray::clone_from_slice(&checked_bytes),
+            mac: GenericArray::clone_from_slice(checked_bytes),
         })
     }
 }
@@ -485,30 +485,30 @@ fn derive_3dh_keys<D: Hash, G: Group>(
     let extracted_ikm = Hkdf::<D>::new(None, &ikm);
     let handshake_secret = derive_secrets::<D>(
         &extracted_ikm,
-        &STR_HANDSHAKE_SECRET,
-        &hashed_derivation_transcript,
+        STR_HANDSHAKE_SECRET,
+        hashed_derivation_transcript,
     )?;
     let session_key = derive_secrets::<D>(
         &extracted_ikm,
-        &STR_SESSION_SECRET,
-        &hashed_derivation_transcript,
+        STR_SESSION_SECRET,
+        hashed_derivation_transcript,
     )?;
 
     let km2 = hkdf_expand_label::<D>(
         &handshake_secret,
-        &STR_SERVER_MAC,
+        STR_SERVER_MAC,
         b"",
         <D as Digest>::OutputSize::to_usize(),
     )?;
     let ke2 = hkdf_expand_label::<D>(
         &handshake_secret,
-        &STR_HANDSHAKE_ENC,
+        STR_HANDSHAKE_ENC,
         b"",
         <D as Digest>::OutputSize::to_usize(),
     )?;
     let km3 = hkdf_expand_label::<D>(
         &handshake_secret,
-        &STR_CLIENT_MAC,
+        STR_CLIENT_MAC,
         b"",
         <D as Digest>::OutputSize::to_usize(),
     )?;
@@ -543,11 +543,11 @@ fn hkdf_expand_label_extracted<D: Hash>(
     hkdf_label.extend_from_slice(&length.to_be_bytes()[std::mem::size_of::<usize>() - 2..]);
 
     let mut opaque_label: Vec<u8> = Vec::new();
-    opaque_label.extend_from_slice(&STR_OPAQUE);
-    opaque_label.extend_from_slice(&label);
+    opaque_label.extend_from_slice(STR_OPAQUE);
+    opaque_label.extend_from_slice(label);
     hkdf_label.extend_from_slice(&serialize(&opaque_label, 1));
 
-    hkdf_label.extend_from_slice(&serialize(&context, 1));
+    hkdf_label.extend_from_slice(&serialize(context, 1));
 
     hkdf.expand(&hkdf_label, &mut okm)
         .map_err(|_| InternalPakeError::HkdfError)?;
@@ -562,7 +562,7 @@ fn derive_secrets<D: Hash>(
     hkdf_expand_label_extracted::<D>(
         hkdf,
         label,
-        &hashed_derivation_transcript,
+        hashed_derivation_transcript,
         <D as Digest>::OutputSize::to_usize(),
     )
 }
diff --git a/src/keypair.rs b/src/keypair.rs
index 27f85f1..5940551 100644
--- a/src/keypair.rs
+++ b/src/keypair.rs
@@ -81,7 +81,7 @@ impl<G: Group> KeyPair<G> {
     pub(crate) fn generate_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
         let sk = G::random_scalar(rng);
         let sk_bytes = G::scalar_as_bytes(&sk);
-        let pk = G::base_point().mult_by_slice(&sk_bytes);
+        let pk = G::base_point().mult_by_slice(sk_bytes);
         Self {
             pk: Key(pk.to_arr().to_vec()),
             sk: Key(sk_bytes.to_vec()),
@@ -93,7 +93,7 @@ impl<G: Group> KeyPair<G> {
     /// &public_from_private(self.private()) == self.public()
     pub(crate) fn public_from_private(bytes: &Key) -> Key {
         let bytes_data = GenericArray::<u8, G::ScalarLen>::from_slice(&bytes.0[..]);
-        Key(G::base_point().mult_by_slice(&bytes_data).to_arr().to_vec())
+        Key(G::base_point().mult_by_slice(bytes_data).to_arr().to_vec())
     }
 
     /// Check whether a public key is valid. This is meant to be applied on
@@ -107,14 +107,14 @@ impl<G: Group> KeyPair<G> {
     /// Computes the diffie hellman function on a public key and private key
     pub(crate) fn diffie_hellman(pk: Key, sk: Key) -> Result<Vec<u8>, InternalPakeError> {
         let pk_data = GenericArray::<u8, G::ElemLen>::from_slice(&pk.0[..]);
-        let point = G::from_element_slice(&pk_data)?;
+        let point = G::from_element_slice(pk_data)?;
         let secret_data = GenericArray::<u8, G::ScalarLen>::from_slice(&sk.0[..]);
-        Ok(G::mult_by_slice(&point, &secret_data).to_arr().to_vec())
+        Ok(G::mult_by_slice(&point, secret_data).to_arr().to_vec())
     }
 
     /// Obtains a KeyPair from a slice representing the private key
     pub fn from_private_key_slice(input: &[u8]) -> Result<Self, InternalPakeError> {
-        let sk = Key::from_arr(GenericArray::from_slice(&input))?;
+        let sk = Key::from_arr(GenericArray::from_slice(input))?;
         let pk = Self::public_from_private(&sk);
         Self::new(pk, sk)
     }
diff --git a/src/messages.rs b/src/messages.rs
index 692ec4a..be3e545 100644
--- a/src/messages.rs
+++ b/src/messages.rs
@@ -13,6 +13,7 @@ use crate::{
         PakeError, ProtocolError,
     },
     group::Group,
+    impl_serialize_and_deserialize_for,
     key_exchange::traits::{KeyExchange, ToBytes},
     keypair::{Key, KeyPair, SizedBytesExt},
 };
@@ -38,7 +39,7 @@ impl<CS: CipherSuite> RegistrationRequest<CS> {
     /// Deserialization from bytes
     pub fn deserialize(input: &[u8]) -> Result<Self, ProtocolError> {
         let elem_len = <CS::Group as Group>::ElemLen::to_usize();
-        let checked_slice = check_slice_size(&input, elem_len, "first_message_bytes")?;
+        let checked_slice = check_slice_size(input, elem_len, "first_message_bytes")?;
         // Check that the message is actually containing an element of the
         // correct subgroup
         let arr = GenericArray::from_slice(checked_slice);
@@ -47,6 +48,8 @@ impl<CS: CipherSuite> RegistrationRequest<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(RegistrationRequest);
+
 /// The answer sent by the server to the user, upon reception of the
 /// registration attempt
 pub struct RegistrationResponse<CS: CipherSuite> {
@@ -67,7 +70,7 @@ impl<CS: CipherSuite> RegistrationResponse<CS> {
         let elem_len = <CS::Group as Group>::ElemLen::to_usize();
         let key_len = <Key as SizedBytes>::Len::to_usize();
         let checked_slice =
-            check_slice_size(&input, elem_len + key_len, "registration_response_bytes")?;
+            check_slice_size(input, elem_len + key_len, "registration_response_bytes")?;
 
         // Check that the message is actually containing an element of the
         // correct subgroup
@@ -81,6 +84,8 @@ impl<CS: CipherSuite> RegistrationResponse<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(RegistrationResponse);
+
 /// The final message from the client, containing sealed cryptographic
 /// identifiers
 pub struct RegistrationUpload<CS: CipherSuite> {
@@ -105,7 +110,7 @@ impl<CS: CipherSuite> RegistrationUpload<CS> {
     pub fn deserialize(input: &[u8]) -> Result<Self, ProtocolError> {
         let key_len = <Key as SizedBytes>::Len::to_usize();
 
-        let checked_slice = check_slice_size_atleast(&input, key_len, "registration_upload_bytes")?;
+        let checked_slice = check_slice_size_atleast(input, key_len, "registration_upload_bytes")?;
 
         let (envelope, remainder) = Envelope::<CS::Hash>::deserialize(&checked_slice[key_len..])?;
 
@@ -122,6 +127,8 @@ impl<CS: CipherSuite> RegistrationUpload<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(RegistrationUpload);
+
 /// The message sent by the user to the server, to initiate registration
 pub struct CredentialRequest<CS: CipherSuite> {
     /// blinded password information
@@ -142,8 +149,7 @@ impl<CS: CipherSuite> CredentialRequest<CS> {
     pub fn deserialize(input: &[u8]) -> Result<Self, ProtocolError> {
         let elem_len = <CS::Group as Group>::ElemLen::to_usize();
 
-        let checked_slice =
-            check_slice_size_atleast(&input, elem_len, "login_first_message_bytes")?;
+        let checked_slice = check_slice_size_atleast(input, elem_len, "login_first_message_bytes")?;
 
         // Check that the message is actually containing an element of the
         // correct subgroup
@@ -159,6 +165,8 @@ impl<CS: CipherSuite> CredentialRequest<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(CredentialRequest);
+
 /// The answer sent by the server to the user, upon reception of the
 /// login attempt
 pub struct CredentialResponse<CS: CipherSuite> {
@@ -217,7 +225,7 @@ impl<CS: CipherSuite> CredentialResponse<CS> {
             check_slice_size_atleast(&remainder, ke2_message_size, "login_second_message_bytes")?;
         let ke2_message =
             <CS::KeyExchange as KeyExchange<CS::Hash, CS::Group>>::KE2Message::try_from(
-                &checked_remainder,
+                checked_remainder,
             )?;
 
         Ok(Self {
@@ -229,6 +237,8 @@ impl<CS: CipherSuite> CredentialResponse<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(CredentialResponse);
+
 /// The answer sent by the client to the server, upon reception of the
 /// sealed envelope
 pub struct CredentialFinalization<CS: CipherSuite> {
@@ -248,3 +258,5 @@ impl<CS: CipherSuite> CredentialFinalization<CS> {
         Ok(Self { ke3_message })
     }
 }
+
+impl_serialize_and_deserialize_for!(CredentialFinalization);
diff --git a/src/opaque.rs b/src/opaque.rs
index 35c6f6d..f13a03b 100644
--- a/src/opaque.rs
+++ b/src/opaque.rs
@@ -11,6 +11,7 @@ use crate::{
     errors::{utils::check_slice_size_atleast, InternalPakeError, PakeError, ProtocolError},
     group::Group,
     hash::Hash,
+    impl_serialize_and_deserialize_for,
     key_exchange::traits::{KeyExchange, ToBytesWithPointers},
     keypair::{Key, KeyPair, SizedBytesExt},
     map_to_curve::GroupWithMapToCurve,
@@ -83,6 +84,8 @@ impl<CS: CipherSuite> ClientRegistration<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(ClientRegistration);
+
 /// Optional parameters for client registration finish
 pub enum ClientRegistrationFinishParameters {
     /// Specifying the identifiers idU and idS (corresponding to custom identifier mode)
@@ -133,7 +136,7 @@ impl<CS: CipherSuite> ClientRegistration<CS> {
         blinding_factor_rng: &mut R,
         password: &[u8],
     ) -> Result<ClientRegistrationStartResult<CS>, ProtocolError> {
-        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(&password, blinding_factor_rng)?;
+        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(password, blinding_factor_rng)?;
 
         Ok(ClientRegistrationStartResult {
             message: RegistrationRequest::<CS> { alpha },
@@ -263,7 +266,7 @@ impl<CS: CipherSuite> ServerRegistration<CS> {
         let key_len = <Key as SizedBytes>::Len::to_usize();
 
         let checked_bytes =
-            check_slice_size_atleast(&input, scalar_len + key_len, "server_registration_bytes")?;
+            check_slice_size_atleast(input, scalar_len + key_len, "server_registration_bytes")?;
 
         let oprf_key_bytes = GenericArray::from_slice(&checked_bytes[..scalar_len]);
         let oprf_key = CS::Group::from_scalar_slice(oprf_key_bytes)?;
@@ -385,6 +388,8 @@ impl<CS: CipherSuite> ServerRegistration<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(ServerRegistration);
+
 // Login
 // =====
 
@@ -454,6 +459,8 @@ impl<CS: CipherSuite> ClientLogin<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(ClientLogin);
+
 /// Optional parameters for client login start
 pub enum ClientLoginStartParameters {
     /// Specifying a plaintext info field that will be sent to the server
@@ -536,7 +543,7 @@ impl<CS: CipherSuite> ClientLogin<CS> {
     ) -> Result<ClientLoginStartResult<CS>, ProtocolError> {
         let ClientLoginStartParameters::WithInfo(info) = params;
 
-        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(&password, rng)?;
+        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(password, rng)?;
 
         let (ke1_state, ke1_message) = CS::KeyExchange::generate_ke1(info, rng)?;
 
@@ -782,7 +789,7 @@ impl<CS: CipherSuite> ServerLogin<CS> {
 
         let l1_bytes = &l1.serialize();
         let beta = oprf::evaluate(l1.alpha, &password_file.oprf_key);
-        let server_s_pk = KeyPair::<CS::Group>::public_from_private(&server_s_sk);
+        let server_s_pk = KeyPair::<CS::Group>::public_from_private(server_s_sk);
 
         let credential_response_component =
             CredentialResponse::<CS>::serialize_without_ke(&beta, &server_s_pk, &envelope);
@@ -880,6 +887,8 @@ impl<CS: CipherSuite> ServerLogin<CS> {
     }
 }
 
+impl_serialize_and_deserialize_for!(ServerLogin);
+
 // Zeroize on drop implementations
 
 // This can't be derived because of the use of a phantom parameter
diff --git a/src/serialization/mod.rs b/src/serialization/mod.rs
index e05462b..1e2bc50 100644
--- a/src/serialization/mod.rs
+++ b/src/serialization/mod.rs
@@ -53,5 +53,73 @@ pub(crate) fn tokenize(input: &[u8], size_bytes: usize) -> Result<(Vec<u8>, Vec<
     ))
 }
 
+/// Inner macro used for deriving `serde`'s `Serialize` and `Deserialize` traits.
+#[macro_export]
+macro_rules! impl_serialize_and_deserialize_for {
+    ($t:ident) => {
+        #[cfg(feature = "serialize")]
+        impl<CS: CipherSuite> serde::Serialize for $t<CS> {
+            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: serde::Serializer,
+            {
+                if serializer.is_human_readable() {
+                    serializer.serialize_str(&base64::encode(&self.serialize()))
+                } else {
+                    serializer.serialize_bytes(&self.serialize())
+                }
+            }
+        }
+
+        #[cfg(feature = "serialize")]
+        impl<'de, CS: CipherSuite> serde::Deserialize<'de> for $t<CS> {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: serde::Deserializer<'de>,
+            {
+                if deserializer.is_human_readable() {
+                    let s = <&str>::deserialize(deserializer)?;
+                    $t::<CS>::deserialize(&base64::decode(s).map_err(serde::de::Error::custom)?)
+                        .map_err(serde::de::Error::custom)
+                } else {
+                    struct ByteVisitor<CS: CipherSuite> {
+                        marker: std::marker::PhantomData<CS>,
+                    }
+                    impl<'de, CS: CipherSuite> serde::de::Visitor<'de> for ByteVisitor<CS> {
+                        type Value = $t<CS>;
+                        fn expecting(
+                            &self,
+                            formatter: &mut std::fmt::Formatter,
+                        ) -> std::fmt::Result {
+                            formatter.write_str(std::concat!(
+                                "the byte representation of a ",
+                                std::stringify!($t)
+                            ))
+                        }
+
+                        fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
+                        where
+                            E: serde::de::Error,
+                        {
+                            $t::<CS>::deserialize(value).map_err(|_| {
+                                serde::de::Error::invalid_value(
+                                    serde::de::Unexpected::Bytes(value),
+                                    &std::concat!(
+                                        "invalid byte sequence for ",
+                                        std::stringify!($t)
+                                    ),
+                                )
+                            })
+                        }
+                    }
+                    deserializer.deserialize_bytes(ByteVisitor::<CS> {
+                        marker: std::marker::PhantomData,
+                    })
+                }
+            }
+        }
+    };
+}
+
 #[cfg(test)]
 mod tests;


Test patch:
diff --git a/src/tests/full_test.rs b/src/tests/full_test.rs
index 85fc6f3..55c8355 100644
--- a/src/tests/full_test.rs
+++ b/src/tests/full_test.rs
@@ -450,6 +450,42 @@ fn test_registration_request() -> Result<(), ProtocolError> {
     Ok(())
 }
 
+#[cfg(feature = "serialize")]
+#[test]
+fn test_serialization() -> Result<(), ProtocolError> {
+    let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap());
+    let mut rng = CycleRng::new(parameters.blinding_factor.to_vec());
+    let client_registration_start_result =
+        ClientRegistration::<RistrettoSha5123dhNoSlowHash>::start(&mut rng, &parameters.password)?;
+    {
+        // Test the json serialization (human-readable, base64).
+        let registration_request_json =
+            serde_json::to_string(&client_registration_start_result.message).unwrap();
+        assert_eq!(
+            registration_request_json,
+            r#""FLqG5TAYzlUH0r+y2YrT9g4wLYJr/zQQpexmnI4e8X0=""#
+        );
+        let registration_request: RegistrationRequest<RistrettoSha5123dhNoSlowHash> =
+            serde_json::from_str(&registration_request_json).unwrap();
+        assert_eq!(
+            hex::encode(client_registration_start_result.message.serialize()),
+            hex::encode(registration_request.serialize()),
+        );
+    }
+    {
+        // Test the bincode serialization (binary).
+        let registration_request_bin =
+            bincode::serialize(&client_registration_start_result.message).unwrap();
+        assert_eq!(registration_request_bin.len(), 40);
+        let registration_request: RegistrationRequest<RistrettoSha5123dhNoSlowHash> =
+            bincode::deserialize(&registration_request_bin).unwrap();
+        assert_eq!(
+            hex::encode(client_registration_start_result.message.serialize()),
+            hex::encode(registration_request.serialize()),
+        );
+    }
+    Ok(())
+}
 #[test]
 fn test_registration_response() -> Result<(), ProtocolError> {
     let parameters = populate_test_vectors(&serde_json::from_str(TEST_VECTOR).unwrap());