Repository: novifinancial/opaque-ke
Pull request #95 (2020-12-04 12:23:51)

Adding VOPRF draft's test vectors
This ensures that our oprf implementation is compatible with [the VOPRF draft](https://github.com/cfrg/draft-irtf-cfrg-voprf/blob/master/draft-irtf-cfrg-voprf.md), currently version 06.

Test vectors are copied for the "base mode" of VOPRF for Ristretto255-SHA512.


Patch:
diff --git a/benches/oprf.rs b/benches/oprf.rs
index f3b8682..b3fbfe7 100644
--- a/benches/oprf.rs
+++ b/benches/oprf.rs
@@ -14,7 +14,7 @@ use opaque_ke::{
     oprf::{blind_shim, evaluate_shim, unblind_and_finalize_shim},
 };
 use rand::{prelude::ThreadRng, thread_rng};
-use sha2::Sha256;
+use sha2::{Sha256, Sha512};
 
 fn oprf1(c: &mut Criterion) {
     let mut csprng: ThreadRng = thread_rng();
@@ -22,7 +22,7 @@ fn oprf1(c: &mut Criterion) {
 
     c.bench_function("blind with Ristretto", move |b| {
         b.iter(|| {
-            blind_shim::<_, RistrettoPoint>(&input[..], &mut csprng).unwrap();
+            blind_shim::<_, RistrettoPoint, Sha512>(&input[..], &mut csprng).unwrap();
         })
     });
 }
@@ -33,7 +33,7 @@ fn oprf1_edwards(c: &mut Criterion) {
 
     c.bench_function("blind with Edwards", move |b| {
         b.iter(|| {
-            blind_shim::<_, EdwardsPoint>(&input[..], &mut csprng).unwrap();
+            blind_shim::<_, EdwardsPoint, Sha256>(&input[..], &mut csprng).unwrap();
         })
     });
 }
@@ -42,7 +42,7 @@ fn oprf2(c: &mut Criterion) {
     let mut csprng: ThreadRng = thread_rng();
     let input = b"hunter2";
 
-    let (_, alpha) = blind_shim::<_, RistrettoPoint>(&input[..], &mut csprng).unwrap();
+    let (_, alpha) = blind_shim::<_, RistrettoPoint, Sha512>(&input[..], &mut csprng).unwrap();
     let salt_bytes = arr![
         u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
         24, 25, 26, 27, 28, 29, 30, 31, 32,
@@ -60,7 +60,7 @@ fn oprf2_edwards(c: &mut Criterion) {
     let mut csprng: ThreadRng = thread_rng();
     let input = b"hunter2";
 
-    let (_, alpha) = blind_shim::<_, EdwardsPoint>(&input[..], &mut csprng).unwrap();
+    let (_, alpha) = blind_shim::<_, EdwardsPoint, Sha256>(&input[..], &mut csprng).unwrap();
     let salt_bytes = arr![
         u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
         24, 25, 26, 27, 28, 29, 30, 31, 32,
@@ -78,7 +78,7 @@ fn oprf3(c: &mut Criterion) {
     let mut csprng: ThreadRng = thread_rng();
     let input = b"hunter2";
 
-    let (token, alpha) = blind_shim::<_, RistrettoPoint>(&input[..], &mut csprng).unwrap();
+    let (token, alpha) = blind_shim::<_, RistrettoPoint, Sha512>(&input[..], &mut csprng).unwrap();
     let salt_bytes = arr![
         u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
         24, 25, 26, 27, 28, 29, 30, 31, 32,
@@ -88,7 +88,7 @@ fn oprf3(c: &mut Criterion) {
 
     c.bench_function("unblind_and_finalize with Ristretto", move |b| {
         b.iter(|| {
-            let _res = unblind_and_finalize_shim::<RistrettoPoint, Sha256>(&token, beta).unwrap();
+            let _res = unblind_and_finalize_shim::<RistrettoPoint, Sha512>(&token, beta).unwrap();
         })
     });
 }
@@ -97,7 +97,7 @@ fn oprf3_edwards(c: &mut Criterion) {
     let mut csprng: ThreadRng = thread_rng();
     let input = b"hunter2";
 
-    let (token, alpha) = blind_shim::<_, EdwardsPoint>(&input[..], &mut csprng).unwrap();
+    let (token, alpha) = blind_shim::<_, EdwardsPoint, Sha256>(&input[..], &mut csprng).unwrap();
     let salt_bytes = arr![
         u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
         24, 25, 26, 27, 28, 29, 30, 31, 32,
diff --git a/src/group.rs b/src/group.rs
index a04e812..78e3f61 100644
--- a/src/group.rs
+++ b/src/group.rs
@@ -29,7 +29,7 @@ pub trait Group: Sized + for<'a> Mul<&'a <Self as Group>::Scalar, Output = Self>
     type Scalar: Zeroize;
     /// The byte length necessary to represent scalars
     type ScalarLen: ArrayLength<u8>;
-    /// Return a scalat from its fixed-length bytes representation
+    /// Return a scalar from its fixed-length bytes representation
     fn from_scalar_slice(
         scalar_bits: &GenericArray<u8, Self::ScalarLen>,
     ) -> Result<Self::Scalar, InternalPakeError>;
diff --git a/src/map_to_curve.rs b/src/map_to_curve.rs
index 41e8e54..20655a8 100644
--- a/src/map_to_curve.rs
+++ b/src/map_to_curve.rs
@@ -15,7 +15,6 @@ use digest::{BlockInput, Digest};
 use generic_array::typenum::Unsigned;
 use generic_array::GenericArray;
 use hkdf::Hkdf;
-use sha2::Sha256;
 
 /// A subtrait of Group specifying how to hash a password into a point
 pub trait GroupWithMapToCurve: Group {
@@ -24,7 +23,7 @@ pub trait GroupWithMapToCurve: Group {
     const SUITE_ID: usize;
 
     /// transforms a password and domain separation tag (DST) into a curve point
-    fn map_to_curve(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError>;
+    fn map_to_curve<H: Hash>(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError>;
 
     /// Generates the contextString parameter as defined in
     /// https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-05.txt
@@ -34,12 +33,12 @@ pub trait GroupWithMapToCurve: Group {
 }
 
 impl GroupWithMapToCurve for RistrettoPoint {
-    const SUITE_ID: usize = 0x01;
+    const SUITE_ID: usize = 0x0001;
 
     // Implements the hash_to_ristretto255() function from
     // https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-10.txt
-    fn map_to_curve(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError> {
-        let uniform_bytes = expand_message_xmd::<Sha256>(msg, dst, 64)?;
+    fn map_to_curve<H: Hash>(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError> {
+        let uniform_bytes = expand_message_xmd::<H>(msg, dst, 64)?;
         Ok(<Self as Group>::hash_to_curve(
             &GenericArray::clone_from_slice(&uniform_bytes[..]),
         ))
@@ -47,10 +46,12 @@ impl GroupWithMapToCurve for RistrettoPoint {
 }
 
 impl GroupWithMapToCurve for EdwardsPoint {
-    const SUITE_ID: usize = 0x09; // FIXME, seemingly unsupported by VOPRF RFC?
-    fn map_to_curve(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError> {
-        let (hashed_input, _) = Hkdf::<Sha256>::extract(Some(dst), msg);
-        Ok(<Self as Group>::hash_to_curve(&hashed_input))
+    const SUITE_ID: usize = 0x0009; // FIXME, seemingly unsupported by VOPRF RFC?
+    fn map_to_curve<H: Hash>(msg: &[u8], dst: &[u8]) -> Result<Self, InternalPakeError> {
+        let (hashed_input, _) = Hkdf::<H>::extract(Some(dst), msg);
+        Ok(<Self as Group>::hash_to_curve(
+            &GenericArray::clone_from_slice(&hashed_input[..]),
+        ))
     }
 }
 
diff --git a/src/opaque.rs b/src/opaque.rs
index 37a0d87..4a89edf 100644
--- a/src/opaque.rs
+++ b/src/opaque.rs
@@ -137,7 +137,7 @@ impl<CS: CipherSuite> ClientRegistration<CS> {
             ClientRegistrationStartParameters::WithIdentifiers(id_u, id_s) => (id_u, id_s),
         };
 
-        let (token, alpha) = oprf::blind::<R, CS::Group>(
+        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(
             &password,
             blinding_factor_rng,
             #[cfg(test)]
@@ -584,7 +584,7 @@ impl<CS: CipherSuite> ClientLogin<CS> {
             }
         };
 
-        let (token, alpha) = oprf::blind::<R, CS::Group>(
+        let (token, alpha) = oprf::blind::<R, CS::Group, CS::Hash>(
             &password,
             rng,
             #[cfg(test)]
diff --git a/src/oprf.rs b/src/oprf.rs
index 29a68f7..0cac7a2 100644
--- a/src/oprf.rs
+++ b/src/oprf.rs
@@ -17,21 +17,21 @@ pub struct Token<Grp: Group> {
     pub(crate) blind: Grp::Scalar,
 }
 
-static STR_VOPRF: &[u8] = b"VOPRF05";
-static STR_VOPRF_FINALIZE: &[u8] = b"VOPRF05-Finalize-";
-static MODE_BASE: u8 = 0x01;
+static STR_VOPRF: &[u8] = b"VOPRF06-HashToGroup-";
+static STR_VOPRF_FINALIZE: &[u8] = b"VOPRF06-Finalize-";
+static MODE_BASE: u8 = 0x00;
 
 /// Computes the first step for the multiplicative blinding version of DH-OPRF. This
 /// message is sent from the client (who holds the input) to the server (who holds the OPRF key).
 /// The client can also pass in an optional "pepper" string to be mixed in with the input through
 /// an HKDF computation.
-pub(crate) fn blind<R: RngCore + CryptoRng, G: GroupWithMapToCurve>(
+pub(crate) fn blind<R: RngCore + CryptoRng, G: GroupWithMapToCurve, H: Hash>(
     input: &[u8],
     blinding_factor_rng: &mut R,
     #[cfg(test)] postprocess: fn(G::Scalar) -> G::Scalar,
 ) -> Result<(Token<G>, G), InternalPakeError> {
     let dst = [STR_VOPRF, &G::get_context_string(MODE_BASE)].concat();
-    let mapped_point = G::map_to_curve(input, &dst)?;
+    let mapped_point = G::map_to_curve::<H>(input, &dst)?;
     let blinding_factor = G::random_scalar(blinding_factor_rng);
     #[cfg(test)]
     let blind = postprocess(blinding_factor);
@@ -84,11 +84,11 @@ pub(crate) fn finalize<G: GroupWithMapToCurve, H: Hash>(
 #[cfg(feature = "bench")]
 #[doc(hidden)]
 #[inline]
-pub fn blind_shim<R: RngCore + CryptoRng, G: GroupWithMapToCurve>(
+pub fn blind_shim<R: RngCore + CryptoRng, G: GroupWithMapToCurve, H: Hash>(
     input: &[u8],
     blinding_factor_rng: &mut R,
 ) -> Result<(Token<G>, G), InternalPakeError> {
-    blind(
+    blind::<R, G, H>(
         input,
         blinding_factor_rng,
         #[cfg(test)]
@@ -129,13 +129,14 @@ mod tests {
     use curve25519_dalek::ristretto::RistrettoPoint;
     use generic_array::{arr, GenericArray};
     use rand_core::OsRng;
+    use sha2::Sha256;
 
     fn prf(
         input: &[u8],
         oprf_key: &[u8; 32],
     ) -> GenericArray<u8, <RistrettoPoint as Group>::ElemLen> {
         let dst = [STR_VOPRF, &RistrettoPoint::get_context_string(MODE_BASE)].concat();
-        let point = RistrettoPoint::map_to_curve(input, &dst).unwrap();
+        let point = RistrettoPoint::map_to_curve::<Sha256>(input, &dst).unwrap();
         let scalar =
             RistrettoPoint::from_scalar_slice(GenericArray::from_slice(&oprf_key[..])).unwrap();
         let res = point * scalar;
@@ -148,7 +149,7 @@ mod tests {
         let input = b"hunter2";
         let mut rng = OsRng;
         let (token, alpha) =
-            blind::<_, RistrettoPoint>(&input[..], &mut rng, std::convert::identity)?;
+            blind::<_, RistrettoPoint, Sha256>(&input[..], &mut rng, std::convert::identity)?;
         let oprf_key_bytes = arr![
             u8; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
             24, 25, 26, 27, 28, 29, 30, 31, 32,
@@ -168,12 +169,12 @@ mod tests {
         let mut input = vec![0u8; 64];
         rng.fill_bytes(&mut input);
         let (token, alpha) =
-            blind::<_, RistrettoPoint>(&input, &mut rng, std::convert::identity).unwrap();
+            blind::<_, RistrettoPoint, Sha256>(&input, &mut rng, std::convert::identity).unwrap();
         let res =
             finalize::<RistrettoPoint, sha2::Sha256>(&token.data, &unblind(&token, alpha), b"");
 
         let dst = [STR_VOPRF, &RistrettoPoint::get_context_string(MODE_BASE)].concat();
-        let point = RistrettoPoint::map_to_curve(&input, &dst).unwrap();
+        let point = RistrettoPoint::map_to_curve::<Sha256>(&input, &dst).unwrap();
         let res2 = finalize::<RistrettoPoint, sha2::Sha256>(&input, &point.to_arr().to_vec(), b"");
 
         assert_eq!(res, res2);


Test patch:
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 1449398..8c642ba 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -5,3 +5,4 @@
 
 pub mod mock_rng;
 mod opaque_ke_test;
+mod voprf_test_vectors;
diff --git a/src/tests/opaque_ke_test.rs b/src/tests/opaque_ke_test.rs
index c606f3c..3272d3b 100644
--- a/src/tests/opaque_ke_test.rs
+++ b/src/tests/opaque_ke_test.rs
@@ -72,40 +72,40 @@ pub struct TestVectorParameters {
 
 static TEST_VECTOR: &str = r#"
 {
-    "client_s_pk": "9e1a7d68007f9db6f2cc515c9e42ce52eee78a6e4d6bca5a16d49bbfa4cd462e",
-    "client_s_sk": "101719337a212b3e55872a0c25874f420bb4510e445c527159ec3e23afe13c7c",
-    "client_e_pk": "5abe58c49c4fe4b3c82f6313857969cc9a2bc913865026ff39b0235636434844",
-    "client_e_sk": "78a41d08c2cb8e1dd3818a494136f5ee321ea2478ba161f0bcbc282c68467a5a",
-    "server_s_pk": "96118ab2892cb29ab29af9e8f0cf7d4d336e757e4f56bbe6afde887714118e12",
-    "server_s_sk": "70240d3fb3f49a289488f0b89b5f956276766efd5c1254317e72c4cb4d452840",
-    "server_e_pk": "9fe1377a64db22c3ae9ebe7d6016b5cbb8060448d8b5582548566f41202f7f33",
-    "server_e_sk": "2021fce09d946296e0a0671555d7552c7f1f69ed9d116cc3b6605449abcf6951",
+    "client_s_pk": "5320b55752ad2061c1804050f8a225a4ab1184bc17d8fa6b3c86470c33d9ce42",
+    "client_s_sk": "e8719d2f7aca0ab85a991ac4221ff7fde7665b9bd17658777c5417ef5c59796a",
+    "client_e_pk": "77fc3fefb8178ae08461756b54364c4f2d1363d5ce3187af128a3f84a6c5722a",
+    "client_e_sk": "48b452f6d0b28387cfa98245bda9230b6df215ed5b820bffb8511ec6802d8075",
+    "server_s_pk": "b8c5defc933aaf3640d13f217c392e06fe5fd41fae8571204fcbb804a566cd2e",
+    "server_s_sk": "60f5ab9ff1a3ba9cf6242812be91635db16e8e1826bf06fb97893d7cf82a2656",
+    "server_e_pk": "5a314dc389c12bf041b14e131fadebcc98e0fc33d3cd996ad9392c7ae6bff468",
+    "server_e_sk": "606034b9be54759ea5802a2dd71e4413e98d52c46e60f2d0868642ad9e6a9b46",
     "id_u": "696455",
     "id_s": "696453",
     "password": "70617373776f7264",
-    "blinding_factor": "2d33e5917ed8ed3b4318dc15d6844fa7fae0b84210ac588350fccf2aa7e26b03",
-    "oprf_key": "90f32248c84a99c303dd97f5a6b80106a7324dd27cf1a9bdec725ce5be11f205",
-    "envelope_nonce": "491dc86966491b649bab099c1dd3a7927ea0ecb74b88d5d08c7267f151e9bf67",
-    "client_nonce": "741fbafaf51abbeab80434c1d1afccc81059b356b84ab704447d9632d5696d91",
-    "server_nonce": "745d196eac5679020332a2c237e63f56310741dbceb714cad4ea28680e5e52c9",
+    "blinding_factor": "c0accb2010d728cfce827d4cb3769000c8b42ac341db8ba196fbe75809d91300",
+    "oprf_key": "7f1d2a048a7aad9d1cf2b96473e3adbb1e0626eafe0abdfd09f1f700beda9d0f",
+    "envelope_nonce": "abb706633ac7092e2aa63dedd4b456d7d99870f099c4f2c51ba75da0f20db8e8",
+    "client_nonce": "1a92d39b2f9acdbef96dcc586b35ae056a085ede41b05f9f81801f69558d44c7",
+    "server_nonce": "e04e9f1a4d35882c0ce435e3f467f601f26fe246e5f60ee9b600d9a63b91c0ad",
     "info1": "696e666f31",
     "info2": "696e666f32",
     "einfo2": "65696e666f32",
     "info3": "696e666f33",
     "einfo3": "65696e666f33",
-    "r1": "00204e61b9b1f2efbc4da7f114b9b8585ece25b3faec9c45e49ff60f310f61586417",
-    "r2": "0020fe43bff51e3868ecc913c97774e070e68acf5532f1862da20e554450f43856a2002096118ab2892cb29ab29af9e8f0cf7d4d336e757e4f56bbe6afde887714118e1201010103",
-    "r3": "491dc86966491b649bab099c1dd3a7927ea0ecb74b88d5d08c7267f151e9bf67002351e93c57b3c307d92ae0d38cbc9c66636511a3fa97bb34947e85c79dbafd670a22152a002303002096118ab2892cb29ab29af9e8f0cf7d4d336e757e4f56bbe6afde887714118e120020626884cb268cfbb7af1edd23ebe12c0cc995a95dffcf225ad9db330185846e9400209e1a7d68007f9db6f2cc515c9e42ce52eee78a6e4d6bca5a16d49bbfa4cd462e",
-    "l1": "00204e61b9b1f2efbc4da7f114b9b8585ece25b3faec9c45e49ff60f310f61586417741fbafaf51abbeab80434c1d1afccc81059b356b84ab704447d9632d5696d910005696e666f315abe58c49c4fe4b3c82f6313857969cc9a2bc913865026ff39b0235636434844",
-    "l2": "0020fe43bff51e3868ecc913c97774e070e68acf5532f1862da20e554450f43856a2491dc86966491b649bab099c1dd3a7927ea0ecb74b88d5d08c7267f151e9bf67002351e93c57b3c307d92ae0d38cbc9c66636511a3fa97bb34947e85c79dbafd670a22152a002303002096118ab2892cb29ab29af9e8f0cf7d4d336e757e4f56bbe6afde887714118e120020626884cb268cfbb7af1edd23ebe12c0cc995a95dffcf225ad9db330185846e942021fce09d946296e0a0671555d7552c7f1f69ed9d116cc3b6605449abcf69510005696e666f329fe1377a64db22c3ae9ebe7d6016b5cbb8060448d8b5582548566f41202f7f33000665696e666f32317af5691eb732163f54e2e497b5a883598baafce9a3a87a710f374b611aa3d5",
-    "l3": "0005696e666f33000665696e666f339ca8f1957db30ea986d9d29d9efc458476959dde2efeb4308bcde0e807152987",
-    "client_registration_state": "000369645500036964532d33e5917ed8ed3b4318dc15d6844fa7fae0b84210ac588350fccf2aa7e26b0370617373776f7264",
-    "client_login_state": "000369645500036964532d33e5917ed8ed3b4318dc15d6844fa7fae0b84210ac588350fccf2aa7e26b0378a41d08c2cb8e1dd3818a494136f5ee321ea2478ba161f0bcbc282c68467a5a741fbafaf51abbeab80434c1d1afccc81059b356b84ab704447d9632d5696d91c346939f310305b1a7e4a3d0958c3c8ecb62ba654f99a3f291aa17f665db721070617373776f7264",
-    "server_registration_state": "90f32248c84a99c303dd97f5a6b80106a7324dd27cf1a9bdec725ce5be11f205",
-    "server_login_state": "6b4d09544d05b91e31dca9fc27da533acd02bd7aafd10a715b479c7de2aedcdc5bf8effba8cc499e32894279cd7b70774a264c7813ad97e0360697b0a81ce1adcfa865f87aa285b8938e46b53f497852f2d88cf4c9302bd27debb02f582fb392",
-    "password_file": "90f32248c84a99c303dd97f5a6b80106a7324dd27cf1a9bdec725ce5be11f2059e1a7d68007f9db6f2cc515c9e42ce52eee78a6e4d6bca5a16d49bbfa4cd462e491dc86966491b649bab099c1dd3a7927ea0ecb74b88d5d08c7267f151e9bf67002351e93c57b3c307d92ae0d38cbc9c66636511a3fa97bb34947e85c79dbafd670a22152a002303002096118ab2892cb29ab29af9e8f0cf7d4d336e757e4f56bbe6afde887714118e120020626884cb268cfbb7af1edd23ebe12c0cc995a95dffcf225ad9db330185846e94",
-    "export_key": "a8a7ecd9f8e4fe53fd883655fb7eb1b52df11b5956b066dca30af60288520611",
-    "shared_secret": "cfa865f87aa285b8938e46b53f497852f2d88cf4c9302bd27debb02f582fb392"
+    "r1": "0020c4ad91692b704470e0613850b9bfcc5b265d43f9ba03dd6ff028f0fb6b365957",
+    "r2": "00205cbfd7b74c7fe6088f8117e7e63000675762920b75e0f4630ed67d4d960c7aa80020b8c5defc933aaf3640d13f217c392e06fe5fd41fae8571204fcbb804a566cd2e01010103",
+    "r3": "abb706633ac7092e2aa63dedd4b456d7d99870f099c4f2c51ba75da0f20db8e80023e76da9b9fbf400fea214ecdab24966e4d3fea9d0c3d5d672597e1e702eb30547495ada0023030020b8c5defc933aaf3640d13f217c392e06fe5fd41fae8571204fcbb804a566cd2e00204e0e5754fbbd48efcdab43cee37f6455f357f3edc54316cb69da8f8e92f5ed8900205320b55752ad2061c1804050f8a225a4ab1184bc17d8fa6b3c86470c33d9ce42",
+    "l1": "0020c4ad91692b704470e0613850b9bfcc5b265d43f9ba03dd6ff028f0fb6b3659571a92d39b2f9acdbef96dcc586b35ae056a085ede41b05f9f81801f69558d44c70005696e666f3177fc3fefb8178ae08461756b54364c4f2d1363d5ce3187af128a3f84a6c5722a",
+    "l2": "00205cbfd7b74c7fe6088f8117e7e63000675762920b75e0f4630ed67d4d960c7aa8abb706633ac7092e2aa63dedd4b456d7d99870f099c4f2c51ba75da0f20db8e80023e76da9b9fbf400fea214ecdab24966e4d3fea9d0c3d5d672597e1e702eb30547495ada0023030020b8c5defc933aaf3640d13f217c392e06fe5fd41fae8571204fcbb804a566cd2e00204e0e5754fbbd48efcdab43cee37f6455f357f3edc54316cb69da8f8e92f5ed89606034b9be54759ea5802a2dd71e4413e98d52c46e60f2d0868642ad9e6a9b460005696e666f325a314dc389c12bf041b14e131fadebcc98e0fc33d3cd996ad9392c7ae6bff468000665696e666f32661aa1d3e579032df30a78312d38a4c3d8d8a0f1b9bb7bbdbff040e0881a248e",
+    "l3": "0005696e666f33000665696e666f3317090781af5a220941ddd6db7d4f2af33d9b316a48aee163647131970bae50ba",
+    "client_registration_state": "00036964550003696453c0accb2010d728cfce827d4cb3769000c8b42ac341db8ba196fbe75809d9130070617373776f7264",
+    "client_login_state": "00036964550003696453c0accb2010d728cfce827d4cb3769000c8b42ac341db8ba196fbe75809d9130048b452f6d0b28387cfa98245bda9230b6df215ed5b820bffb8511ec6802d80751a92d39b2f9acdbef96dcc586b35ae056a085ede41b05f9f81801f69558d44c7288b735853d9e1d8ab699b7c4aef54680f036c56140dc0a4991f6d02a95babc970617373776f7264",
+    "server_registration_state": "7f1d2a048a7aad9d1cf2b96473e3adbb1e0626eafe0abdfd09f1f700beda9d0f",
+    "server_login_state": "2bfb97200c110fa3a9a020920829ce9e7e74a9e843ae86949e16300dc6e25b09e7e0522de623f021a7ba5d150ab8cba2eca4be458d3c22ae6109813a215a03d88f757f3390c767cd9433007c1abf9226c9e0da8e5d1a3d5722773c070b05f44b",
+    "password_file": "7f1d2a048a7aad9d1cf2b96473e3adbb1e0626eafe0abdfd09f1f700beda9d0f5320b55752ad2061c1804050f8a225a4ab1184bc17d8fa6b3c86470c33d9ce42abb706633ac7092e2aa63dedd4b456d7d99870f099c4f2c51ba75da0f20db8e80023e76da9b9fbf400fea214ecdab24966e4d3fea9d0c3d5d672597e1e702eb30547495ada0023030020b8c5defc933aaf3640d13f217c392e06fe5fd41fae8571204fcbb804a566cd2e00204e0e5754fbbd48efcdab43cee37f6455f357f3edc54316cb69da8f8e92f5ed89",
+    "export_key": "139998e9d44e2fa629689d8bef9f900a60278e2acd55f4e59906255b10494d58",
+    "shared_secret": "8f757f3390c767cd9433007c1abf9226c9e0da8e5d1a3d5722773c070b05f44b"
 }
 "#;
 
diff --git a/src/tests/voprf_test_vectors.rs b/src/tests/voprf_test_vectors.rs
new file mode 100644
index 0000000..6fe1b32
--- /dev/null
+++ b/src/tests/voprf_test_vectors.rs
@@ -0,0 +1,182 @@
+// Copyright (c) Facebook, Inc. and its affiliates.
+//
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+use crate::{errors::*, group::Group, oprf};
+use curve25519_dalek::ristretto::RistrettoPoint;
+use generic_array::GenericArray;
+use rand_core::OsRng;
+use serde_json::Value;
+use sha2::Sha512;
+
+struct VOPRFTestVectorParameters {
+    sksm: Vec<u8>,
+    input: Vec<u8>,
+    blind: Vec<u8>,
+    blinded_element: Vec<u8>,
+    evaluation_element: Vec<u8>,
+    unblinded_element: Vec<u8>,
+    info: Vec<u8>,
+    output: Vec<u8>,
+}
+
+// Taken from https://github.com/cfrg/draft-irtf-cfrg-voprf/blob/master/draft-irtf-cfrg-voprf.md
+// in base mode
+static OPRF_RISTRETTO255_SHA512: &'static [&str] = &[
+    r#"
+    {
+        "sksm": "c604c785ada70d77a5256ae21767de8c3304115237d262134f5e46e512cf8e03",
+        "input": "00",
+        "blind": "5ed895206bfc53316d307b23e46ecc6623afb3086da74189a416012be037e50b",
+        "blinded_element": "5cccd309ec729aebe398c53e19c0ab09c24a29f01036960bdad109852e7bdb44",
+        "evaluation_element": "86bd5eeabf29a87cb4a5c7207cb3ade5297e65f9b74c979bd3551891f4b21515",
+        "unblinded_element": "3c7f2d901c0d4f245503a186086fbdf5d8b4408432b25c5163e8b5a19c258348",
+        "info": "4f505246207465737420766563746f7273",
+        "output": "0bb570873cc0402ca38f1a2c395301f2a3627616e305f2bc54bb08c3f6ea9871eb71074e52e36b90778ba7c3e3429ef7170245c9e01647f3827fdef84d3ba930"
+    }
+    "#,
+    r#"
+    {
+        "sksm": "c604c785ada70d77a5256ae21767de8c3304115237d262134f5e46e512cf8e03",
+        "input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+        "blind": "ed8366feb6b1d05d1f46acb727061e43aadfafe9c10e5a64e7518d63e3263503",
+        "blinded_element": "227d63ca69e93bd062193c1e97fff3d5ebf628f646009d77c4e22ba6429be154",
+        "evaluation_element": "063b91a12e7cbb98dfeb75d8a7eeb83aacf9fd6df7e0b4197466fb77a27fa631",
+        "unblinded_element": "804ec6774764ed50a0bbad0a5f477aa04df7323acab8f98ca6e468b7790bca4c",
+        "info": "4f505246207465737420766563746f7273",
+        "output": "af7cc264dbc96a6b898ba0fa33bfa9e1407bf1dcfbf8772204d470d4458b8f047806679dbfa251f656b906edf9fa638e268adf979bd0e2380a092047d61f9db9"
+    }
+    "#,
+];
+
+fn decode(values: &Value, key: &str) -> Option<Vec<u8>> {
+    values[key]
+        .as_str()
+        .and_then(|s| hex::decode(&s.to_string()).ok())
+}
+
+fn populate_test_vectors(values: &Value) -> VOPRFTestVectorParameters {
+    VOPRFTestVectorParameters {
+        sksm: decode(&values, "sksm").unwrap(),
+        input: decode(&values, "input").unwrap(),
+        blind: decode(&values, "blind").unwrap(),
+        blinded_element: decode(&values, "blinded_element").unwrap(),
+        evaluation_element: decode(&values, "evaluation_element").unwrap(),
+        unblinded_element: decode(&values, "unblinded_element").unwrap(),
+        info: decode(&values, "info").unwrap(),
+        output: decode(&values, "output").unwrap(),
+    }
+}
+
+// For fixing the blinding factor
+
+fn postprocess_blinding_factor_oprf_ristretto255_sha512_0<G: Group>(_: G::Scalar) -> G::Scalar {
+    let parameters =
+        populate_test_vectors(&serde_json::from_str(OPRF_RISTRETTO255_SHA512[0]).unwrap());
+    G::from_scalar_slice(GenericArray::from_slice(&parameters.blind[..])).unwrap()
+}
+
+fn postprocess_blinding_factor_oprf_ristretto255_sha512_1<G: Group>(_: G::Scalar) -> G::Scalar {
+    let parameters =
+        populate_test_vectors(&serde_json::from_str(OPRF_RISTRETTO255_SHA512[1]).unwrap());
+    G::from_scalar_slice(GenericArray::from_slice(&parameters.blind[..])).unwrap()
+}
+
+// Tests input -> blind, blinded_element
+#[test]
+fn test_blind() -> Result<(), PakeError> {
+    for (i, tv) in OPRF_RISTRETTO255_SHA512.iter().enumerate() {
+        let parameters = populate_test_vectors(&serde_json::from_str(tv).unwrap());
+        let mut rng = OsRng;
+
+        let postprocess_fn: fn(
+            <RistrettoPoint as Group>::Scalar,
+        ) -> <RistrettoPoint as Group>::Scalar = match i {
+            0 => postprocess_blinding_factor_oprf_ristretto255_sha512_0::<RistrettoPoint>,
+            1 => postprocess_blinding_factor_oprf_ristretto255_sha512_1::<RistrettoPoint>,
+            _ => panic!("Need to cover each test vector"),
+        };
+
+        let (token, blinded_element) = oprf::blind::<OsRng, RistrettoPoint, Sha512>(
+            &parameters.input,
+            &mut rng,
+            postprocess_fn,
+        )?;
+
+        assert_eq!(
+            &parameters.blind,
+            &RistrettoPoint::scalar_as_bytes(&token.blind).to_vec()
+        );
+        assert_eq!(
+            &parameters.blinded_element,
+            &blinded_element.to_arr().to_vec()
+        );
+    }
+    Ok(())
+}
+
+// Tests sksm, blinded_element -> evaluation_element
+#[test]
+fn test_evaluate() -> Result<(), PakeError> {
+    for tv in OPRF_RISTRETTO255_SHA512 {
+        let parameters = populate_test_vectors(&serde_json::from_str(tv).unwrap());
+        let evaluation_element = oprf::evaluate::<RistrettoPoint>(
+            RistrettoPoint::from_element_slice(GenericArray::from_slice(
+                &parameters.blinded_element,
+            ))
+            .unwrap(),
+            &RistrettoPoint::from_scalar_slice(GenericArray::from_slice(&parameters.sksm)).unwrap(),
+        );
+
+        assert_eq!(
+            &parameters.evaluation_element,
+            &evaluation_element.to_arr().to_vec()
+        );
+    }
+    Ok(())
+}
+
+// Tests sksm, evaluation_element -> evaluation_element
+#[test]
+fn test_unblind() -> Result<(), PakeError> {
+    for tv in OPRF_RISTRETTO255_SHA512 {
+        let parameters = populate_test_vectors(&serde_json::from_str(tv).unwrap());
+
+        let token = oprf::Token {
+            data: parameters.input,
+            blind: RistrettoPoint::from_scalar_slice(GenericArray::from_slice(
+                &parameters.blind[..],
+            ))
+            .unwrap(),
+        };
+
+        let unblinded_element = oprf::unblind::<RistrettoPoint>(
+            &token,
+            RistrettoPoint::from_element_slice(GenericArray::from_slice(
+                &parameters.evaluation_element,
+            ))
+            .unwrap(),
+        );
+
+        assert_eq!(&parameters.unblinded_element, &unblinded_element);
+    }
+    Ok(())
+}
+
+// Tests input, unblinded_element, info -> output
+#[test]
+fn test_finalize() -> Result<(), PakeError> {
+    for tv in OPRF_RISTRETTO255_SHA512 {
+        let parameters = populate_test_vectors(&serde_json::from_str(tv).unwrap());
+
+        let output = oprf::finalize::<RistrettoPoint, Sha512>(
+            &parameters.input,
+            &parameters.unblinded_element,
+            &parameters.info,
+        );
+
+        assert_eq!(&parameters.output, &output.to_vec());
+    }
+    Ok(())
+}