Explorar el Código

Fix protocols

Remove report_init function, fix race condition with Resolve cred
Vecna hace 6 meses
padre
commit
8315ad5ceb

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
crates/lox-distributor/db_test_file.json


+ 12 - 54
crates/lox-distributor/src/lox_context.rs

@@ -15,10 +15,10 @@ use lox_extensions::{
     migration_table::EncMigrationTable,
     proto::{
         blockage_migration, check_blockage, errors::CredentialError, issue_invite, level_up,
-        migration, open_invite, redeem_invite, report_init, report_resolve, report_status,
-        report_submit, trust_promotion, update_cred, update_invite,
+        migration, open_invite, redeem_invite, report_resolve, report_status, report_submit,
+        trust_promotion, update_cred, update_invite,
     },
-    report_table::{CountryCode, ReportStatus},
+    report_table::CountryCode,
     BridgeAuth, BridgeDb, OpenInvitationError,
 };
 
@@ -547,28 +547,11 @@ impl LoxServerContext {
         self.ba.lock().unwrap().handle_update_invite(old_key, req)
     }
 
-    // Returns a valid report_init::Response if the report_init::Request is valid
-    fn report_init(
-        &self,
-        req: report_init::report_init::Request,
-        cc: CountryCode,
-        D: G,
-    ) -> Result<report_init::report_init::Reply, CredentialError> {
-        let mut ba_obj = self.ba.lock().unwrap();
-        ba_obj.handle_report_init(req, cc, D)
-    }
-
     // Returns a valid report_status::Response if the report_status::Request is valid
     fn report_status(
         &self,
         req: report_status::report_status::Request,
-    ) -> Result<
-        (
-            report_status::report_status::Reply,
-            [ReportStatus; MAX_BRIDGES_PER_BUCKET],
-        ),
-        CredentialError,
-    > {
+    ) -> Result<report_status::report_status::Reply, CredentialError> {
         let mut ba_obj = self.ba.lock().unwrap();
         ba_obj.handle_report_status(req)
     }
@@ -577,10 +560,12 @@ impl LoxServerContext {
     fn report_submit(
         &self,
         req: report_submit::report_submit::Request,
+        cc: CountryCode,
+        D: G,
         bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
     ) -> Result<report_submit::report_submit::Reply, CredentialError> {
         let mut ba_obj = self.ba.lock().unwrap();
-        ba_obj.handle_report_submit(req, bridges_being_reported)
+        ba_obj.handle_report_submit(req, cc, D, bridges_being_reported)
     }
 
     // Returns a valid report_resolve::Response if the report_resolve::Request is valid
@@ -909,35 +894,6 @@ impl LoxServerContext {
         }
     }
 
-    // Verify the report_init request and return the result as an HTTP response
-    pub fn verify_and_send_report_init(
-        self,
-        request: Bytes,
-    ) -> Response<BoxBody<Bytes, Infallible>> {
-        let (req, cc, D): (report_init::report_init::Request, CountryCode, G) =
-            match serde_json::from_slice(&request) {
-                Ok((req, cc, D)) => (req, cc, D),
-                Err(e) => {
-                    let response = json!({"error": e.to_string()});
-                    let val = serde_json::to_string(&response).unwrap();
-                    return prepare_header(val);
-                }
-            };
-        match self.report_init(req, cc, D) {
-            Ok(resp) => {
-                let response = serde_json::to_string(&resp).unwrap();
-                self.metrics.report_init_count.inc();
-                prepare_header(response)
-            }
-            Err(e) => {
-                println!("Invalid Report Init request, Proof Error: {:?}", e);
-                let response = json!({"error": e.to_string()});
-                let val = serde_json::to_string(&response).unwrap();
-                prepare_header(val)
-            }
-        }
-    }
-
     // Verify the report_status request and return the result as an HTTP response
     pub fn verify_and_send_report_status(
         self,
@@ -971,18 +927,20 @@ impl LoxServerContext {
         self,
         request: Bytes,
     ) -> Response<BoxBody<Bytes, Infallible>> {
-        let (req, bridges_being_reported): (
+        let (req, cc, D, bridges_being_reported): (
             report_submit::report_submit::Request,
+            CountryCode,
+            G,
             [bool; MAX_BRIDGES_PER_BUCKET],
         ) = match serde_json::from_slice(&request) {
-            Ok((req, bridges_being_reported)) => (req, bridges_being_reported),
+            Ok((req, cc, D, bridges_being_reported)) => (req, cc, D, bridges_being_reported),
             Err(e) => {
                 let response = json!({"error": e.to_string()});
                 let val = serde_json::to_string(&response).unwrap();
                 return prepare_header(val);
             }
         };
-        match self.report_submit(req, bridges_being_reported) {
+        match self.report_submit(req, cc, D, bridges_being_reported) {
             Ok(resp) => {
                 let response = serde_json::to_string(&resp).unwrap();
                 self.metrics.report_submit_count.inc();

+ 65 - 93
crates/lox-distributor/src/request_handler.rs

@@ -96,10 +96,6 @@ where
                 let bytes = req.into_body().collect().await.unwrap().to_bytes();
                 cloned_context.verify_and_send_update_invite(bytes)
             }),
-            (&Method::POST, "/reportinit") => Ok::<_, Infallible>({
-                let bytes = req.into_body().collect().await.unwrap().to_bytes();
-                cloned_context.verify_and_send_report_init(bytes)
-            }),
             (&Method::POST, "/reportstatus") => Ok::<_, Infallible>({
                 let bytes = req.into_body().collect().await.unwrap().to_bytes();
                 cloned_context.verify_and_send_report_status(bytes)
@@ -137,11 +133,11 @@ mod tests {
     use hyper::Response;
     use julianday::JulianDay;
     use lox_extensions::{
-        lox_creds::{BucketReachability, Invitation, Lox, Migration},
+        lox_creds::{BucketReachability, Invitation, Lox, Migration, Resolve},
         proto::{
             blockage_migration, check_blockage, issue_invite, level_up, migration, open_invite,
-            redeem_invite, report_init, report_resolve, report_status, report_submit,
-            trust_promotion, update_cred, update_invite,
+            redeem_invite, report_resolve, report_status, report_submit, trust_promotion,
+            update_cred, update_invite,
         },
         report_table::CountryCode,
     };
@@ -170,16 +166,12 @@ mod tests {
         ) -> Request<B>;
         fn updatecred(&self, request: lox_utils::UpdateCredReq) -> Request<B>;
         fn updateinvite(&self, request: lox_utils::UpdateInviteReq) -> Request<B>;
-        fn reportinit(
-            &self,
-            request: report_init::report_init::Request,
-            cc: CountryCode,
-            D: G,
-        ) -> Request<B>;
         fn reportstatus(&self, request: report_status::report_status::Request) -> Request<B>;
         fn reportsubmit(
             &self,
             request: report_submit::report_submit::Request,
+            cc: CountryCode,
+            D: G,
             bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
         ) -> Request<B>;
         fn reportresolve(&self, request: report_resolve::report_resolve::Request) -> Request<B>;
@@ -332,21 +324,6 @@ mod tests {
                 .unwrap()
         }
 
-        fn reportinit(
-            &self,
-            request: report_init::report_init::Request,
-            cc: CountryCode,
-            D: G,
-        ) -> Request<BoxedBody> {
-            let req_str = serde_json::to_string(&(request, cc, D)).unwrap();
-            Request::builder()
-                .header("Content-Type", "application/json")
-                .method("POST")
-                .uri("http://localhost/reportinit")
-                .body(full(req_str))
-                .unwrap()
-        }
-
         fn reportstatus(
             &self,
             request: report_status::report_status::Request,
@@ -363,9 +340,11 @@ mod tests {
         fn reportsubmit(
             &self,
             request: report_submit::report_submit::Request,
+            cc: CountryCode,
+            D: G,
             bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
         ) -> Request<BoxedBody> {
-            let req_str = serde_json::to_string(&(request, bridges_being_reported)).unwrap();
+            let req_str = serde_json::to_string(&(request, cc, D, bridges_being_reported)).unwrap();
             Request::builder()
                 .header("Content-Type", "application/json")
                 .method("POST")
@@ -1123,49 +1102,35 @@ mod tests {
         .0
     }
 
-    async fn get_report_init(
-        context: LoxServerContext,
-        cred: Lox,
-        cc: CountryCode,
-    ) -> Result<Lox, CMZError> {
-        let rng = &mut rand::thread_rng();
-        let lc = LoxClientMock {};
-        let ((request, state), cc, D) = report_init::request(rng, cred, cc).unwrap();
-        let report_init_request = lc.reportinit(request, cc, D);
-        let report_init_response = handle(context, report_init_request).await.unwrap();
-        assert_eq!(report_init_response.status(), StatusCode::OK);
-        let report_init_resp = body_to_string(report_init_response).await;
-        let report_init_response_obj = serde_json::from_str(&report_init_resp).unwrap();
-        report_init::handle_response(state, report_init_response_obj)
-    }
-
     async fn get_report_status(
         context: LoxServerContext,
         cred: Lox,
-    ) -> Result<(Lox, [ReportStatus; MAX_BRIDGES_PER_BUCKET]), CMZError> {
+    ) -> Result<(Lox, Resolve), CMZError> {
         let rng = &mut rand::thread_rng();
         let lc = LoxClientMock {};
-        let (request, state) = report_status::request(rng, cred).unwrap();
+        let (request, state) =
+            report_status::request(rng, cred, context.ba.lock().unwrap().resolve_pub.clone())
+                .unwrap();
         let report_status_request = lc.reportstatus(request);
         let report_status_response = handle(context, report_status_request).await.unwrap();
         assert_eq!(report_status_response.status(), StatusCode::OK);
         let report_status_resp = body_to_string(report_status_response).await;
-        let (report_status_response_obj, report_statuses) =
-            serde_json::from_str(&report_status_resp).unwrap();
-        report_status::handle_response(state, report_status_response_obj, report_statuses)
+        let report_status_response_obj = serde_json::from_str(&report_status_resp).unwrap();
+        report_status::handle_response(state, report_status_response_obj)
     }
 
     async fn get_report_submit(
         context: LoxServerContext,
         cred: Lox,
-        new_false_reports: u32,
+        resolve_cred: Resolve,
+        cc: CountryCode,
         bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
     ) -> Result<Lox, CMZError> {
         let rng = &mut rand::thread_rng();
         let lc = LoxClientMock {};
-        let ((request, state), bridges_being_reported) =
-            report_submit::request(rng, cred, new_false_reports, bridges_being_reported).unwrap();
-        let report_submit_request = lc.reportsubmit(request, bridges_being_reported);
+        let ((request, state), cc_server, D, bridges_being_reported) =
+            report_submit::request(rng, cred, resolve_cred, cc, bridges_being_reported).unwrap();
+        let report_submit_request = lc.reportsubmit(request, cc_server, D, bridges_being_reported);
         let report_submit_response = handle(context, report_submit_request).await.unwrap();
         assert_eq!(report_submit_response.status(), StatusCode::OK);
         let report_submit_resp = body_to_string(report_submit_response).await;
@@ -1176,11 +1141,11 @@ mod tests {
     async fn get_report_resolve(
         context: LoxServerContext,
         cred: Lox,
-        new_false_reports: u32,
+        resolve_cred: Resolve,
     ) -> Result<Lox, CMZError> {
         let rng = &mut rand::thread_rng();
         let lc = LoxClientMock {};
-        let (request, state) = report_resolve::request(rng, cred, new_false_reports).unwrap();
+        let (request, state) = report_resolve::request(rng, cred, resolve_cred).unwrap();
         let report_resolve_request = lc.reportresolve(request);
         let report_resolve_response = handle(context, report_resolve_request).await.unwrap();
         assert_eq!(report_resolve_response.status(), StatusCode::OK);
@@ -1191,7 +1156,10 @@ mod tests {
 
     #[tokio::test]
     async fn test_handle_report_protocols() {
+        let rng = &mut rand::thread_rng();
         let th = TestHarness::new();
+        let cc = CountryCode::RU;
+
         // Get level 0 credential
         let invite_response = get_invite(th.context.clone()).await.unwrap();
         let token = get_token(invite_response).await;
@@ -1203,37 +1171,34 @@ mod tests {
         // Get bucket info
         let bucket = get_bucket(th.context.clone(), lox_cred.clone()).await;
 
-        // Report init
-        let lox_cred = get_report_init(th.context.clone(), lox_cred.clone(), CountryCode::RU)
-            .await
-            .unwrap();
-        let pending1 = lox_cred.pending.unwrap();
-        assert!(pending1 != Scalar::ZERO);
+        // Submit a report
+        let resolve_cred_pending_0 = th
+            .context
+            .ba
+            .lock()
+            .unwrap()
+            .get_resolve_cred_for_new_session(rng);
+        let lox_cred = get_report_submit(
+            th.context.clone(),
+            lox_cred,
+            resolve_cred_pending_0,
+            cc,
+            [true, false, false],
+        )
+        .await
+        .unwrap();
 
-        // Get report status
-        let (lox_cred, report_statuses) = get_report_status(th.context.clone(), lox_cred)
+        // Check status
+        let (lox_cred, resolve_cred) = get_report_status(th.context.clone(), lox_cred)
             .await
             .unwrap();
-        assert_eq!(report_statuses, [ReportStatus::NoReport; 3]);
+        assert_eq!(resolve_cred.bridge_0_pending.unwrap(), Scalar::ONE);
 
-        // Submit a report
-        let lox_cred = get_report_submit(th.context.clone(), lox_cred, 0, [true, false, false])
+        // Resolve, returning to pending state
+        let lox_cred = get_report_resolve(th.context.clone(), lox_cred, resolve_cred)
             .await
             .unwrap();
 
-        // Check status
-        let (lox_cred, report_statuses) = get_report_status(th.context.clone(), lox_cred)
-            .await
-            .unwrap();
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Pending,
-                ReportStatus::NoReport,
-                ReportStatus::NoReport
-            ]
-        );
-
         // Scan returns bridge is not blocked
         th.context.ba.lock().unwrap().process_scan_result(
             bucket[0].get_hashed_fingerprint(),
@@ -1243,22 +1208,22 @@ mod tests {
         );
 
         // Check status
-        let (lox_cred, report_statuses) = get_report_status(th.context.clone(), lox_cred)
+        let (lox_cred, resolve_cred) = get_report_status(th.context.clone(), lox_cred)
             .await
             .unwrap();
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::NotBlocked,
-                ReportStatus::NoReport,
-                ReportStatus::NoReport
-            ]
-        );
+        assert_eq!(resolve_cred.new_false_reports.unwrap(), Scalar::ONE);
+        assert_eq!(resolve_cred.bridge_0_pending.unwrap(), Scalar::ZERO);
 
         // Submit a new report, adding 1 to our false_reports count
-        let lox_cred = get_report_submit(th.context.clone(), lox_cred, 1, [true, false, false])
-            .await
-            .unwrap();
+        let lox_cred = get_report_submit(
+            th.context.clone(),
+            lox_cred,
+            resolve_cred,
+            cc,
+            [true, false, false],
+        )
+        .await
+        .unwrap();
         assert_eq!(lox_cred.false_reports.unwrap(), Scalar::ONE);
 
         // Let report time out
@@ -1272,8 +1237,15 @@ mod tests {
                 SystemTime::now(),
             );
 
+        // Check status
+        let (lox_cred, resolve_cred) = get_report_status(th.context.clone(), lox_cred)
+            .await
+            .unwrap();
+        assert_eq!(resolve_cred.new_false_reports.unwrap(), Scalar::ZERO);
+        assert_eq!(resolve_cred.bridge_0_pending.unwrap(), Scalar::ZERO);
+
         // Resolve
-        let _lox_cred = get_report_resolve(th.context.clone(), lox_cred, 0)
+        let _lox_cred = get_report_resolve(th.context.clone(), lox_cred, resolve_cred)
             .await
             .unwrap();
     }

+ 28 - 2
crates/lox-extensions/src/lib.rs

@@ -46,7 +46,7 @@ pub mod proto {
     pub mod migration;
     pub mod open_invite;
     pub mod redeem_invite;
-    pub mod report_init;
+    //    pub mod report_init;
     pub mod report_resolve;
     pub mod report_status;
     pub mod report_submit;
@@ -359,6 +359,10 @@ pub struct BridgeAuth {
     invitation_priv: CMZPrivkey<G>,
     /// The public key for invitation credentials
     pub invitation_pub: CMZPubkey<G>,
+    /// The private key for resolve credentials
+    resolve_priv: CMZPrivkey<G>,
+    /// The public key for resolve credentials
+    pub resolve_pub: CMZPubkey<G>,
 
     /// The public key of the BridgeDb issuing open invitations
     pub bridgedb_pub: VerifyingKey,
@@ -404,6 +408,7 @@ impl BridgeAuth {
         let (migrationkey_priv, migrationkey_pub) = MigrationKey::gen_keys(rng, true);
         let (reachability_priv, reachability_pub) = BucketReachability::gen_keys(rng, true);
         let (invitation_priv, invitation_pub) = Invitation::gen_keys(rng, true);
+        let (resolve_priv, resolve_pub) = Resolve::gen_keys(rng, true);
         Self {
             lox_priv,
             lox_pub,
@@ -415,6 +420,8 @@ impl BridgeAuth {
             reachability_pub,
             invitation_priv,
             invitation_pub,
+            resolve_priv,
+            resolve_pub,
             bridgedb_pub,
             bridge_table: Default::default(),
             report_table: Default::default(),
@@ -662,7 +669,7 @@ impl BridgeAuth {
             .iter()
             .position(|x| x == bridge)
         {
-            Some(index) => Some({ self.bridge_table.unallocated_bridges.swap_remove(index) }),
+            Some(index) => Some(self.bridge_table.unallocated_bridges.swap_remove(index)),
             None => None,
         }
     }
@@ -1018,6 +1025,25 @@ impl BridgeAuth {
         }
     }
 
+    // Current implementation requires a resolve credential to submit
+    // reports, even if there is not already an active session. This
+    // function provides such a resolve credential that can be used when
+    // pending == 0.
+    #[cfg(feature = "bridgeauth")]
+    pub fn get_resolve_cred_for_new_session(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+    ) -> Resolve {
+        let mut R = Resolve::using_privkey(&self.resolve_priv);
+        R.pending = Some(Scalar::ZERO);
+        R.new_false_reports = Some(Scalar::ZERO);
+        R.bridge_0_pending = Some(Scalar::ZERO);
+        R.bridge_1_pending = Some(Scalar::ZERO);
+        R.bridge_2_pending = Some(Scalar::ZERO);
+        let _ = R.create_MAC(rng, &self.resolve_priv);
+        R
+    }
+
     // For testing only: manually advance the day by the given number
     // of days
     #[cfg(feature = "test")]

+ 21 - 0
crates/lox-extensions/src/lox_creds.rs

@@ -89,3 +89,24 @@ CMZ! { Invitation:
     blockages,
     false_reports
 }
+
+// The Resolve credential.
+//
+// These credentials are given to Lox users who have previously
+// submitted one or more reports, and they allow them to either submit
+// more reports or resolve their pending credential. The credential
+// indicates the number of new false reports that must be added to the
+// Lox credential's false_reports value when the report_submit or
+// report_resolve protocol is performed and which bridges in the user's
+// bucket have pending reports. The user cannot submit new reports for
+// bridges that already have pending reports, and they cannot reset
+// their credential's pending value to 0 unless they have no pending
+// reports.
+// Annotated to "R"
+CMZ! { Resolve:
+    pending,
+    new_false_reports,
+    bridge_0_pending,
+    bridge_1_pending,
+    bridge_2_pending
+}

+ 55 - 42
crates/lox-extensions/src/mock_auth.rs

@@ -2,18 +2,14 @@
 use super::proto::{
     blockage_migration, check_blockage, issue_invite,
     level_up::{self, LEVEL_INTERVAL},
-    migration, open_invite, report_init, report_resolve, report_status, report_submit,
+    migration, open_invite, report_resolve, report_status, report_submit,
     trust_promotion::{self, UNTRUSTED_INTERVAL},
 };
 #[cfg(all(test, feature = "bridgeauth"))]
 use super::*;
 
 #[cfg(all(test, feature = "bridgeauth"))]
-use crate::{
-    bridge_table::BridgeLine,
-    lox_creds::BucketReachability,
-    report_table::{CountryCode, ReportStatus},
-};
+use crate::{bridge_table::BridgeLine, lox_creds::BucketReachability, report_table::CountryCode};
 
 #[cfg(all(test, feature = "bridgeauth"))]
 use rand::RngCore;
@@ -96,6 +92,14 @@ impl TestHarness {
         );
     }
 
+    /// Verify the MAC on a Resolve credential
+    pub fn verify_resolve(&mut self, cred: &lox_creds::Resolve) {
+        assert!(
+            cred.verify_MAC(&self.ba.resolve_priv).is_ok(),
+            "Resolve cred's MAC should verify"
+        );
+    }
+
     pub fn open_invite(
         &mut self,
         rng: &mut (impl CryptoRng + RngCore),
@@ -312,35 +316,37 @@ impl TestHarness {
         self.ba.report_table.process_scan_result(bridge, cc, status);
     }
 
-    pub fn report_init(
-        &mut self,
-        rng: &mut (impl CryptoRng + RngCore),
-        cred: Lox,
-        cc: CountryCode,
-    ) -> Lox {
-        let report_init_request = report_init::request(rng, cred, cc);
-        assert!(
-            report_init_request.is_ok(),
-            "Report init request should succeed"
-        );
-        let ((ri_request, ri_client_state), cc, D) = report_init_request.unwrap();
-        let report_init_response = self.ba.handle_report_init(ri_request, cc, D);
-        assert!(
-            report_init_response.is_ok(),
-            "Report init response from server should succeed"
-        );
-        let response = report_init_response.unwrap();
-        let new_cred = report_init::handle_response(ri_client_state, response);
-        assert!(new_cred.is_ok(), "Handle response should succeed");
-        new_cred.unwrap()
-    }
+    /*
+        pub fn report_init(
+            &mut self,
+            rng: &mut (impl CryptoRng + RngCore),
+            cred: Lox,
+            cc: CountryCode,
+        ) -> Lox {
+            let report_init_request = report_init::request(rng, cred, cc);
+            assert!(
+                report_init_request.is_ok(),
+                "Report init request should succeed"
+            );
+            let ((ri_request, ri_client_state), cc, D) = report_init_request.unwrap();
+            let report_init_response = self.ba.handle_report_init(ri_request, cc, D);
+            assert!(
+                report_init_response.is_ok(),
+                "Report init response from server should succeed"
+            );
+            let response = report_init_response.unwrap();
+            let new_cred = report_init::handle_response(ri_client_state, response);
+            assert!(new_cred.is_ok(), "Handle response should succeed");
+            new_cred.unwrap()
+        }
+    */
 
     pub fn report_status(
         &mut self,
         rng: &mut (impl CryptoRng + RngCore),
         cred: Lox,
-    ) -> (Lox, [ReportStatus; MAX_BRIDGES_PER_BUCKET]) {
-        let report_status_request = report_status::request(rng, cred);
+    ) -> (Lox, Resolve) {
+        let report_status_request = report_status::request(rng, cred, self.ba.resolve_pub.clone());
         assert!(
             report_status_request.is_ok(),
             "Report status request should succeed"
@@ -351,30 +357,37 @@ impl TestHarness {
             report_status_response.is_ok(),
             "Report status response from server should succeed"
         );
-        let (response, report_statuses) = report_status_response.unwrap();
-        let (new_cred, report_statuses) =
-            report_status::handle_response(rs_client_state, response, report_statuses).unwrap();
-        (new_cred, report_statuses)
+        let response = report_status_response.unwrap();
+        let (new_cred, resolve_cred) =
+            report_status::handle_response(rs_client_state, response).unwrap();
+        (new_cred, resolve_cred)
     }
 
     pub fn report_submit(
         &mut self,
         rng: &mut (impl CryptoRng + RngCore),
         cred: Lox,
-        new_false_reports: u32,
+        resolve_cred: Option<Resolve>,
+        cc: CountryCode,
         bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
     ) -> Lox {
+        let resolve_cred = match resolve_cred {
+            Some(v) => v,
+            None => self.ba.get_resolve_cred_for_new_session(rng),
+        };
+        self.verify_resolve(&resolve_cred);
+
         let report_submit_request =
-            report_submit::request(rng, cred, new_false_reports, bridges_being_reported);
+            report_submit::request(rng, cred, resolve_cred, cc, bridges_being_reported);
         assert!(
             report_submit_request.is_ok(),
             "Report submit request should succeed"
         );
-        let ((rs_request, rs_client_state), bridges_being_reported_server) =
+        let ((rs_request, rs_client_state), cc_server, D, bridges_being_reported_server) =
             report_submit_request.unwrap();
-        let report_submit_response = self
-            .ba
-            .handle_report_submit(rs_request, bridges_being_reported_server);
+        let report_submit_response =
+            self.ba
+                .handle_report_submit(rs_request, cc_server, D, bridges_being_reported_server);
         assert!(
             report_submit_response.is_ok(),
             "Report submit response from server should succeed"
@@ -389,9 +402,9 @@ impl TestHarness {
         &mut self,
         rng: &mut (impl CryptoRng + RngCore),
         cred: Lox,
-        new_false_reports: u32,
+        resolve_cred: Resolve,
     ) -> Lox {
-        let report_resolve_request = report_resolve::request(rng, cred, new_false_reports);
+        let report_resolve_request = report_resolve::request(rng, cred, resolve_cred);
         assert!(
             report_resolve_request.is_ok(),
             "Report resolve request should succeed"

+ 0 - 196
crates/lox-extensions/src/proto/report_init.rs

@@ -1,196 +0,0 @@
-/*! A module for the protocol for the user to enter a pending state so
-that they can submit reports.
-
-This is a prerequisite to other report_* protocols and an anti-requisite
-to block_migration, issue_invite, migration, and trust_promotion. In
-order to perform these protocols, the user must wait for their reports
-to resolve and then perform the report_resolve protocol, which restores
-their pending status to 0.
-
-This protocol cannot be performed if the user already has more
-false_reports than are allowed. This is not a technical requirement, but
-it would only hurt the user to become pending if they are not allowed to
-submit reports in the first place.
-
-The user presents their current Lox credential:
-- id: revealed
-- bucket: blinded
-- trust_level: blinded
-- level_since: blinded
-- invites_remaining: blinded
-- blockages: blinded
-- false_reports: blinded but proved in ZK to be less than or equal to
-  MAX_FALSE_REPORTS
-- pending: revealed to be 0
-
-and D = bucket * H to indicate which bucket they have
-
-and cc, their country code
-
-and a new Lox credential to be issued:
-
-- id: jointly chosen by the user and BA
-- bucket: blinded, but proved in ZK that it's the same as in the Lox
-  credential above
-- trust_level: blinded, but proved in ZK that it's the same as in the
-  Lox credential above
-- level_since: blinded, but proved in ZK that it's the same as in the
-  Lox credential above
-- invites_remaining: blinded, but proved in ZK that it's the same as in
-  the Lox credential above
-- blockages: blinded, but proved in ZK that it's the same as in the Lox
-  credential above
-- false_reports: blinded, but proved in ZK that it's the same as in the
-  Lox credential above
-- pending: chosen randomly by the BA
-
-The BridgeAuthority associates their pending value with their bucket and
-country code, so the user does not need to re-send this information in
-future report_* protocols. This also means that if the user moves to a
-different country, they should report_resolve and then report_init
-again. Also, users must report_resolve before they can perform any
-action that changes their bucket.
-
-*/
-#[cfg(feature = "bridgeauth")]
-use super::super::dup_filter::SeenType;
-#[cfg(feature = "bridgeauth")]
-use super::super::BridgeAuth;
-use super::super::{
-    get_H,
-    report_table::{CountryCode, MAX_FALSE_REPORTS},
-    Scalar, G,
-};
-use super::errors::CredentialError;
-use crate::lox_creds::Lox;
-use cmz::*;
-use group::Group;
-use rand::{CryptoRng, RngCore};
-use sha2::Sha512;
-
-const SESSION_ID: &[u8] = b"report_init";
-
-muCMZProtocol! { report_init<max_false_reports, bucket>,
-    L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R },
-    N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: S },
-    N.bucket = L.bucket,
-    N.trust_level = L.trust_level,
-    N.level_since = L.level_since,
-    N.invites_remaining = L.invites_remaining,
-    N.blockages = L.blockages,
-    N.false_reports = L.false_reports,
-    (0..=max_false_reports).contains(L.false_reports),
-
-    // Before checking this proof, we need to determine (from D =
-    // bucket*H submitted in the report) which bucket the user is
-    // claiming to have in their credential. Check that the user's
-    // claimed bucket is correct.
-    L.bucket = bucket,
-}
-
-pub fn request(
-    rng: &mut (impl CryptoRng + RngCore),
-    L: Lox,
-    cc: CountryCode,
-) -> Result<
-    (
-        (report_init::Request, report_init::ClientState),
-        CountryCode,
-        G,
-    ),
-    CredentialError,
-> {
-    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
-
-    // Ensure the credential can be correctly shown; that is, the
-    // pending value is 0
-    if L.pending.is_some_and(|p| p != Scalar::ZERO) {
-        return Err(CredentialError::CredentialPending);
-    }
-
-    let H = get_H();
-    let D = L.bucket.unwrap() * H;
-
-    let mut N = Lox::using_pubkey(L.get_pubkey());
-    N.bucket = L.bucket;
-    N.trust_level = L.trust_level;
-    N.level_since = L.level_since;
-    N.invites_remaining = L.invites_remaining;
-    N.blockages = L.blockages;
-    N.false_reports = L.false_reports;
-
-    let params = report_init::Params {
-        max_false_reports: MAX_FALSE_REPORTS.into(),
-        bucket: L.bucket.unwrap(),
-    };
-    match report_init::prepare(rng, SESSION_ID, &L, N, &params) {
-        Ok(req_state) => Ok((req_state, cc, D)),
-        Err(e) => Err(CredentialError::CMZError(e)),
-    }
-}
-
-#[cfg(feature = "bridgeauth")]
-impl BridgeAuth {
-    pub fn handle_report_init(
-        &mut self,
-        req: report_init::Request,
-        cc: CountryCode,
-        D: G,
-    ) -> Result<report_init::Reply, CredentialError> {
-        let mut rng = rand::thread_rng();
-        let reqbytes = req.as_bytes();
-        let recvreq = report_init::Request::try_from(&reqbytes[..]).unwrap();
-
-        let (bucket, _bridges) = match self.bridge_table.get_bucket_and_bridges(D) {
-            Ok((bucket, bridges)) => (bucket, bridges),
-            Err(e) => {
-                return Err(e);
-            }
-        };
-
-        let pending = Scalar::random(&mut rng);
-
-        match report_init::handle(
-            &mut rng,
-            SESSION_ID,
-            recvreq,
-            |L: &mut Lox, N: &mut Lox| {
-                L.set_privkey(&self.lox_priv);
-                N.set_privkey(&self.lox_priv);
-
-                N.pending = Some(pending);
-
-                Ok(report_init::Params {
-                    max_false_reports: MAX_FALSE_REPORTS.into(),
-                    bucket: bucket,
-                })
-            },
-            |L: &Lox, _N: &Lox| {
-                if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
-                    return Err(CMZError::RevealAttrMissing("id", ""));
-                }
-                Ok(())
-            },
-        ) {
-            Ok((response, (_L_issuer, _N_issuer))) => {
-                self.report_table.add_user(pending, bucket, cc);
-                Ok(response)
-            }
-            Err(e) => Err(CredentialError::CMZError(e)),
-        }
-    }
-}
-
-pub fn handle_response(
-    state: report_init::ClientState,
-    rep: report_init::Reply,
-) -> Result<Lox, CMZError> {
-    let replybytes = rep.as_bytes();
-    let recvreply = report_init::Reply::try_from(&replybytes[..]).unwrap();
-    match state.finalize(recvreply) {
-        Ok(cred) => Ok(cred),
-        Err(_e) => Err(CMZError::Unknown),
-    }
-}
-
-// This function is implicitly tested by report_submit tests.

+ 58 - 104
crates/lox-extensions/src/proto/report_resolve.rs

@@ -9,8 +9,11 @@ credential. The number of reported bridges that were found to be
 accessible is added to the user's new Lox credential's false_reports
 value.
 
-The BridgeAuthority knows the number of "false reports" associated with
-the pending value. We will call this new_false_reports.
+If the user has performed report_status and still had pending reports at
+that time, they cannot return their credential's pending value back to
+0. They can, however, perform report_resolve to aggregate any false
+reports into their Lox credential's count, and then they can perform
+report_status again.
 
 The user presents their current Lox credential:
 - id: revealed
@@ -22,6 +25,13 @@ The user presents their current Lox credential:
 - false_reports: blinded
 - pending: revealed
 
+and a Resolve credential:
+- pending: revealed to be the same as in the Lox credential above
+- new_false_report: revealed
+- bridge_0_pending: revealed
+- bridge_1_pending: revealed
+- bridge_2_pending: revealed
+
 and a new Lox credential to be issued:
 
 - id: jointly chosen by the user and BA
@@ -37,16 +47,18 @@ and a new Lox credential to be issued:
   credential above
 - false_reports: blinded, but proved in ZK that it's new_false_reports
   more than the value in the Lox credential above
-- pending: 0
+- pending: revealed; if the user still has pending reports, the value is
+  the same as in the Lox credential above, and if the user does not have
+  pending reports, the value is set to 0
 
 */
 #[cfg(feature = "bridgeauth")]
 use super::super::dup_filter::SeenType;
 #[cfg(feature = "bridgeauth")]
-use super::super::{report_table::ReportStatus, BridgeAuth, MAX_BRIDGES_PER_BUCKET};
+use super::super::BridgeAuth;
 use super::super::{Scalar, G};
 use super::errors::CredentialError;
-use crate::lox_creds::Lox;
+use crate::lox_creds::{Lox, Resolve};
 use cmz::*;
 use group::Group;
 use rand::{CryptoRng, RngCore};
@@ -54,21 +66,26 @@ use sha2::Sha512;
 
 const SESSION_ID: &[u8] = b"report_resolve";
 
-muCMZProtocol! { report_resolve<new_false_reports, bucket>,
-    L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R },
+// We might be able to do fancier things to hide the bridge_*_pending
+// values in the resolve credential, but the BridgeAuthority knows this
+// information, associated with the pending value, so that wouldn't be
+// useful.
+muCMZProtocol! { report_resolve,
+    [ L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R }, R: Resolve { pending: R, new_false_reports: R, bridge_0_pending: R, bridge_1_pending: R, bridge_2_pending: R } ],
     N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: I },
+    R.pending = L.pending,
     N.bucket = L.bucket,
     N.trust_level = L.trust_level,
     N.level_since = L.level_since,
     N.invites_remaining = L.invites_remaining,
     N.blockages = L.blockages,
-    N.false_reports = L.false_reports + new_false_reports,
+    N.false_reports = L.false_reports + R.new_false_reports,
 }
 
 pub fn request(
     rng: &mut (impl CryptoRng + RngCore),
     L: Lox,
-    new_false_reports: u32, // 0, 1, 2, or 3
+    R: Resolve,
 ) -> Result<(report_resolve::Request, report_resolve::ClientState), CredentialError> {
     cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
 
@@ -78,20 +95,27 @@ pub fn request(
         return Err(CredentialError::CredentialNotPending);
     }
 
+    if L.pending != R.pending {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
     let mut N = Lox::using_pubkey(L.get_pubkey());
     N.bucket = L.bucket;
     N.trust_level = L.trust_level;
     N.level_since = L.level_since;
     N.invites_remaining = L.invites_remaining;
     N.blockages = L.blockages;
-    N.false_reports = Some(L.false_reports.unwrap() + Scalar::from(new_false_reports));
-    N.pending = Some(Scalar::ZERO);
-
-    let params = report_resolve::Params {
-        new_false_reports: new_false_reports.into(),
-        bucket: L.bucket.unwrap(),
+    N.false_reports = Some(L.false_reports.unwrap() + R.new_false_reports.unwrap());
+    N.pending = if R.bridge_0_pending == Some(Scalar::ZERO)
+        && R.bridge_1_pending == Some(Scalar::ZERO)
+        && R.bridge_2_pending == Some(Scalar::ZERO)
+    {
+        Some(Scalar::ZERO)
+    } else {
+        L.pending
     };
-    match report_resolve::prepare(rng, SESSION_ID, &L, N, &params) {
+
+    match report_resolve::prepare(rng, SESSION_ID, &L, &R, N) {
         Ok(req_state) => Ok(req_state),
         Err(e) => Err(CredentialError::CMZError(e)),
     }
@@ -107,114 +131,44 @@ impl BridgeAuth {
         let reqbytes = req.as_bytes();
         let recvreq = report_resolve::Request::try_from(&reqbytes[..]).unwrap();
 
-        let mut bridges_with_old_reports = Vec::<[u8; 20]>::new();
-
         match report_resolve::handle(
             &mut rng,
             SESSION_ID,
             recvreq,
-            |L: &mut Lox, N: &mut Lox| {
+            |L: &mut Lox, R: &mut Resolve, N: &mut Lox| {
                 L.set_privkey(&self.lox_priv);
+                R.set_privkey(&self.resolve_priv);
                 N.set_privkey(&self.lox_priv);
 
-                let bucket = match self
-                    .report_table
-                    .get_user_bucket(L.pending.unwrap().clone())
+                N.pending = if R.bridge_0_pending == Some(Scalar::ZERO)
+                    && R.bridge_1_pending == Some(Scalar::ZERO)
+                    && R.bridge_2_pending == Some(Scalar::ZERO)
                 {
-                    Some(bucket) => bucket,
-                    None => {
-                        return Err(CMZError::RevealAttrMissing(
-                            "pending",
-                            "Pending value is incorrect",
-                        ));
-                    }
-                };
-
-                let bridges = match self.bridge_table.get_bridge_hashed_fingerprints(bucket) {
-                    Ok(bridges) => bridges,
-                    Err(_) => {
-                        return Err(CMZError::RevealAttrMissing(
-                            "bucket",
-                            "Bucket value is incorrect",
-                        ));
-                    }
+                    Some(Scalar::ZERO)
+                } else {
+                    L.pending
                 };
 
-                let mut new_false_reports: u32 = 0;
-
-                for i in 0..MAX_BRIDGES_PER_BUCKET {
-                    let status = self
-                        .report_table
-                        .get_report_status(&bridges[i], L.pending.unwrap().clone());
-
-                    // Check for old reports
-                    if status == ReportStatus::NotBlocked
-                        || status == ReportStatus::Blocked
-                        || status == ReportStatus::Timeout
-                    {
-                        // We have an old report that will need to be cleaned up afterwards
-                        bridges_with_old_reports.push(bridges[i]);
-                    }
-
-                    if status == ReportStatus::Pending {
-                        // TODO: Improve this error message/type
-                        return Err(CMZError::RevealAttrMissing(
-                            "pending",
-                            "There are still pending reports",
-                        ));
-                    } else if status == ReportStatus::NotBlocked {
-                        new_false_reports += 1;
-                    }
-                }
-
-                N.pending = Some(Scalar::ZERO);
-                Ok(report_resolve::Params {
-                    new_false_reports: new_false_reports.into(),
-                    bucket: bucket,
-                })
+                Ok(())
             },
-            |L: &Lox, _N: &Lox| {
+            |L: &Lox, _R: &Resolve, _N: &Lox| {
                 if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
                     return Err(CMZError::RevealAttrMissing("id", ""));
                 }
                 Ok(())
             },
         ) {
-            Ok((response, (L_issuer, _N_issuer))) => {
-                // Get bucket and bridge hashed fingerprints again
-                let bucket = match self
-                    .report_table
-                    .get_user_bucket(L_issuer.pending.unwrap().clone())
-                {
-                    Some(bucket) => bucket,
-                    None => {
-                        return Err(CredentialError::InvalidField(
-                            String::from("pending"),
-                            String::from("Pending value is incorrect"),
-                        ));
-                    }
-                };
-                let bridges = match self.bridge_table.get_bridge_hashed_fingerprints(bucket) {
-                    Ok(bridges) => bridges,
-                    Err(e) => {
-                        return Err(e);
-                    }
-                };
-
-                // Only if everything went okay, remove old reports from this user
-                if self
-                    .report_table
-                    .remove_old_reports(&bridges, L_issuer.pending.unwrap())
-                {
+            Ok((response, (L_issuer, _R_issuer, N_issuer))) => {
+                if N_issuer.pending == Some(Scalar::ZERO) {
+                    // Remove this user's previous pending value
                     self.report_table.remove_user(L_issuer.pending.unwrap());
-                    Ok(response)
                 } else {
-                    // TODO: Fix this error message/type
-                    Err(CredentialError::InvalidField(
-                        "pending".to_string(),
-                        "Failed to remove old reports".to_string(),
-                    ))
+                    // If everything went okay, remove any existing resolve
+                    // credential for this user
+                    self.report_table
+                        .set_user_resolve_cred(N_issuer.pending.unwrap(), None);
                 }
+                Ok(response)
             }
             Err(e) => Err(CredentialError::CMZError(e)),
         }

+ 119 - 36
crates/lox-extensions/src/proto/report_status.rs

@@ -34,17 +34,34 @@ and a new Lox credential to be issued:
   credential above
 - false_reports: blinded, but proved in ZK that it's new_false_reports
   more than the value in the Lox credential above
-- pending: revealed to be the same as the pending value in the Lox
+- pending: revealed to be one more than the pending value in the Lox
   credential above
 
+and a new Resolve credential to be issued:
+
+- pending: revealed to be the same as the pending value in the new Lox
+  credential (that is, one more than the pending value in the presented
+  Lox credential)
+- new_false_reports: revealed
+- bucket_0_pending: revealed to be 0 if there is not a pending report
+  for bucket 0, or 1 if there is
+- bucket_1_pending: revealed to be 0 if there is not a pending report
+  for bucket 1, or 1 if there is
+- bucket_2_pending: revealed to be 0 if there is not a pending report
+  for bucket 2, or 1 if there is
+
 */
 #[cfg(feature = "bridgeauth")]
 use super::super::dup_filter::SeenType;
 #[cfg(feature = "bridgeauth")]
-use super::super::BridgeAuth;
-use super::super::{bridge_table::MAX_BRIDGES_PER_BUCKET, report_table::ReportStatus, Scalar, G};
+use super::super::{
+    bridge_table::MAX_BRIDGES_PER_BUCKET,
+    report_table::{ReportStatus, MAX_REPORT_ACTIONS_IN_SESSION},
+    BridgeAuth,
+};
+use super::super::{Scalar, G};
 use super::errors::CredentialError;
-use crate::lox_creds::Lox;
+use crate::lox_creds::{Lox, Resolve};
 use cmz::*;
 use group::Group;
 use rand::{CryptoRng, RngCore};
@@ -52,20 +69,24 @@ use sha2::Sha512;
 
 const SESSION_ID: &[u8] = b"report_status";
 
-muCMZProtocol! { report_status<bucket>,
+muCMZProtocol! { report_status,
     L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R },
-    N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: I },
+    [ N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: I },
+    R: Resolve { pending: I, new_false_reports: S, bridge_0_pending: S, bridge_1_pending: S, bridge_2_pending: S } ],
     N.bucket = L.bucket,
     N.trust_level = L.trust_level,
     N.level_since = L.level_since,
     N.invites_remaining = L.invites_remaining,
     N.blockages = L.blockages,
     N.false_reports = L.false_reports,
+    N.pending = L.pending + 1,
+    R.pending = L.pending + 1,
 }
 
 pub fn request(
     rng: &mut (impl CryptoRng + RngCore),
     L: Lox,
+    resolve_pub: CMZPubkey<G>,
 ) -> Result<(report_status::Request, report_status::ClientState), CredentialError> {
     cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
 
@@ -75,6 +96,9 @@ pub fn request(
         return Err(CredentialError::CredentialNotPending);
     }
 
+    let mut R = Resolve::using_pubkey(&resolve_pub);
+    R.pending = Some(L.pending.unwrap() + Scalar::ONE);
+
     let mut N = Lox::using_pubkey(L.get_pubkey());
     N.bucket = L.bucket;
     N.trust_level = L.trust_level;
@@ -82,12 +106,9 @@ pub fn request(
     N.invites_remaining = L.invites_remaining;
     N.blockages = L.blockages;
     N.false_reports = L.false_reports;
-    N.pending = L.pending;
+    N.pending = Some(L.pending.unwrap() + Scalar::ONE);
 
-    let params = report_status::Params {
-        bucket: L.bucket.unwrap(),
-    };
-    match report_status::prepare(rng, SESSION_ID, &L, N, &params) {
+    match report_status::prepare(rng, SESSION_ID, &L, N, R) {
         Ok(req_state) => Ok(req_state),
         Err(e) => Err(CredentialError::CMZError(e)),
     }
@@ -98,36 +119,54 @@ impl BridgeAuth {
     pub fn handle_report_status(
         &mut self,
         req: report_status::Request,
-    ) -> Result<(report_status::Reply, [ReportStatus; MAX_BRIDGES_PER_BUCKET]), CredentialError>
-    {
+    ) -> Result<report_status::Reply, CredentialError> {
         let mut rng = rand::thread_rng();
         let reqbytes = req.as_bytes();
         let recvreq = report_status::Request::try_from(&reqbytes[..]).unwrap();
 
-        let mut result = [ReportStatus::NoReport; 3];
+        // TODO: Re-send resolve credential if user re-requests it
+
+        let mut bridges_with_old_reports = Vec::<[u8; 20]>::new();
 
         match report_status::handle(
             &mut rng,
             SESSION_ID,
             recvreq,
-            |L: &mut Lox, N: &mut Lox| {
+            |L: &mut Lox, N: &mut Lox, R: &mut Resolve| {
                 L.set_privkey(&self.lox_priv);
+                R.set_privkey(&self.resolve_priv);
                 N.set_privkey(&self.lox_priv);
 
-                let bucket = match self
-                    .report_table
-                    .get_user_bucket(L.pending.unwrap().clone())
-                {
-                    Some(bucket) => bucket,
-                    None => {
-                        return Err(CMZError::RevealAttrMissing(
-                            "pending",
-                            "Pending value is incorrect",
-                        ));
-                    }
+                let Some(user_info) = self.report_table.get_user_info(L.pending.unwrap().clone())
+                else {
+                    return Err(CMZError::RevealAttrMissing(
+                        "pending",
+                        "Pending value is incorrect",
+                    ));
                 };
 
-                let bridges = match self.bridge_table.get_bridge_hashed_fingerprints(bucket) {
+                if L.pending.unwrap() != user_info.current_pending_value {
+                    return Err(CMZError::RevealAttrMissing(
+                        "pending",
+                        "Pending value is incorrect",
+                    ));
+                }
+
+                // For now, it is the client's responsibility to ensure
+                // that this number does not get too high. It might be
+                // nice to give some kind of warning when the number
+                // gets close.
+                if L.pending.unwrap().as_bytes()[0] >= MAX_REPORT_ACTIONS_IN_SESSION {
+                    return Err(CMZError::RevealAttrMissing(
+                        "pending",
+                        "Pending value is too high",
+                    ));
+                }
+
+                let bridges = match self
+                    .bridge_table
+                    .get_bridge_hashed_fingerprints(user_info.bucket)
+                {
                     Ok(bridges) => bridges,
                     Err(_) => {
                         return Err(CMZError::RevealAttrMissing(
@@ -137,23 +176,68 @@ impl BridgeAuth {
                     }
                 };
 
+                let mut new_false_reports: u32 = 0;
+
+                let mut reports_pending = [false; MAX_BRIDGES_PER_BUCKET];
+
+                // For each bridge, get pending status
                 for i in 0..MAX_BRIDGES_PER_BUCKET {
-                    result[i] = self
+                    let report_status = self
                         .report_table
-                        .get_report_status(&bridges[i], L.pending.unwrap().clone());
+                        .get_report_status(&bridges[i], L.pending.unwrap());
+
+                    if report_status == ReportStatus::Pending {
+                        reports_pending[i] = true;
+                    } else {
+                        if report_status == ReportStatus::NotBlocked {
+                            new_false_reports += 1;
+                        }
+
+                        if report_status != ReportStatus::NoReport {
+                            bridges_with_old_reports.push(bridges[i]);
+                        }
+                    }
                 }
 
-                N.pending = L.pending;
-                Ok(report_status::Params { bucket: bucket })
+                // Set R fields accordingly
+                R.pending = Some(L.pending.unwrap() + Scalar::ONE);
+                R.new_false_reports = Some(Scalar::from(new_false_reports));
+                R.bridge_0_pending = if reports_pending[0] {
+                    Some(Scalar::ONE)
+                } else {
+                    Some(Scalar::ZERO)
+                };
+                R.bridge_1_pending = if reports_pending[1] {
+                    Some(Scalar::ONE)
+                } else {
+                    Some(Scalar::ZERO)
+                };
+                R.bridge_2_pending = if reports_pending[2] {
+                    Some(Scalar::ONE)
+                } else {
+                    Some(Scalar::ZERO)
+                };
+
+                N.pending = Some(L.pending.unwrap() + Scalar::ONE);
+                Ok(())
             },
-            |L: &Lox, _N: &Lox| {
+            |L: &Lox, _N: &Lox, _R: &Resolve| {
                 if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
                     return Err(CMZError::RevealAttrMissing("id", ""));
                 }
                 Ok(())
             },
         ) {
-            Ok((response, (_L_issuer, _N_issuer))) => Ok((response, result)),
+            Ok((response, (L_issuer, N_issuer, R_issuer))) => {
+                // Set server state and store Resolve credential in case user re-requests it
+                self.report_table
+                    .set_user_pending(N_issuer.pending.unwrap());
+                self.report_table
+                    .set_user_resolve_cred(N_issuer.pending.unwrap(), Some(R_issuer));
+                self.report_table
+                    .remove_old_reports(&bridges_with_old_reports, L_issuer.pending.unwrap());
+                Ok(response)
+            }
             Err(e) => Err(CredentialError::CMZError(e)),
         }
     }
@@ -162,12 +246,11 @@ impl BridgeAuth {
 pub fn handle_response(
     state: report_status::ClientState,
     rep: report_status::Reply,
-    report_statuses: [ReportStatus; MAX_BRIDGES_PER_BUCKET],
-) -> Result<(Lox, [ReportStatus; MAX_BRIDGES_PER_BUCKET]), CMZError> {
+) -> Result<(Lox, Resolve), CMZError> {
     let replybytes = rep.as_bytes();
     let recvreply = report_status::Reply::try_from(&replybytes[..]).unwrap();
     match state.finalize(recvreply) {
-        Ok(cred) => Ok((cred, report_statuses)),
+        Ok((cred, resolve_cred)) => Ok((cred, resolve_cred)),
         Err(_e) => Err(CMZError::Unknown),
     }
 }

+ 305 - 246
crates/lox-extensions/src/proto/report_submit.rs

@@ -1,9 +1,6 @@
 /*! A module for the protocol for the user to submit one or more
 reports.
 
-The user's credential must be in a "pending" state. (If it is not, they
-should run report_init first.)
-
 The user may submit up to 3 reports at a time, but the protocol will
 only succeed if the user's reports do not conflict with any pending
 reports (i.e., the user may not submit a report for a bridge if they
@@ -28,6 +25,13 @@ The user presents their current Lox credential:
 - false_reports: blinded
 - pending: revealed
 
+and a Resolve credential (which may have all fields 0):
+- pending: revealed to be the same as in the above Lox credential
+- new_false_reports: revealed
+- bridge_0_pending: revealed
+- bridge_1_pending: revealed
+- bridge_2_pending: revealed
+
 and a new Lox credential to be issued:
 
 - id: jointly chosen by the user and BA
@@ -44,18 +48,22 @@ and a new Lox credential to be issued:
 - false_reports: blinded, but proved in ZK that it's new_false_reports
   more than the value in the Lox credential above
 - pending: revealed to be the same as the pending value in the Lox
-  credential above
+  credential above if that value is 0 OR a new, random pending value
+  from the server with the low byte 0
 
 */
 #[cfg(feature = "bridgeauth")]
 use super::super::dup_filter::SeenType;
 use super::super::{
-    bridge_table::MAX_BRIDGES_PER_BUCKET, report_table::MAX_FALSE_REPORTS, scalar_u32, Scalar, G,
+    bridge_table::MAX_BRIDGES_PER_BUCKET,
+    get_H,
+    report_table::{CountryCode, MAX_FALSE_REPORTS},
+    scalar_u32, Scalar, G,
 };
 #[cfg(feature = "bridgeauth")]
-use super::super::{report_table::ReportStatus, BridgeAuth, BridgeLine};
+use super::super::{BridgeAuth, BridgeLine};
 use super::errors::CredentialError;
-use crate::lox_creds::Lox;
+use crate::lox_creds::{Lox, Resolve};
 use cmz::*;
 use group::Group;
 use rand::{CryptoRng, RngCore};
@@ -63,36 +71,46 @@ use sha2::Sha512;
 
 const SESSION_ID: &[u8] = b"report_submit";
 
-muCMZProtocol! { report_submit<max_false_reports, new_false_reports>,
-    L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R },
-    N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: I },
+muCMZProtocol! { report_submit<max_false_reports, bucket>,
+    [ L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: R },
+    R: Resolve { pending: R, new_false_reports: R, bridge_0_pending: R, bridge_1_pending: R, bridge_2_pending: R } ],
+    N: Lox { id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H, false_reports: H, pending: S },
+    L.bucket = bucket,
     N.bucket = L.bucket,
     N.trust_level = L.trust_level,
     N.level_since = L.level_since,
     N.invites_remaining = L.invites_remaining,
     N.blockages = L.blockages,
-    (0..=max_false_reports).contains(L.false_reports + new_false_reports),
-    N.false_reports = L.false_reports + new_false_reports,
+    (0..=max_false_reports).contains(L.false_reports + R.new_false_reports),
+    N.false_reports = L.false_reports + R.new_false_reports,
 }
 
 pub fn request(
     rng: &mut (impl CryptoRng + RngCore),
     L: Lox,
-    new_false_reports: u32, // 0, 1, 2, or 3
+    R: Resolve,
+    cc: CountryCode,
     bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
 ) -> Result<
     (
         (report_submit::Request, report_submit::ClientState),
+        CountryCode,
+        G,
         [bool; MAX_BRIDGES_PER_BUCKET],
     ),
     CredentialError,
 > {
     cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
 
-    // Ensure the credential can be correctly shown; that is, the
-    // pending value is not 0
-    if L.pending.is_some_and(|p| p == Scalar::ZERO) {
-        return Err(CredentialError::CredentialNotPending);
+    // For simplicity, always have a resolve credential, and let user
+    // have empty one if not already pending
+    // TODO: We might want this to implemented the other way (where the
+    // logic is different) in the future. I'm prioritizing simplicity of
+    // the code and not repeating myself rather than most
+    // straightforward logic.
+
+    if L.pending != R.pending {
+        return Err(CredentialError::CredentialMismatch);
     }
 
     let false_reports: u32 = match scalar_u32(&L.false_reports.unwrap()) {
@@ -104,6 +122,15 @@ pub fn request(
             ))
         }
     };
+    let new_false_reports: u32 = match scalar_u32(&R.new_false_reports.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("new_false_reports"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
     if false_reports + new_false_reports > MAX_FALSE_REPORTS {
         return Err(CredentialError::InvalidField(
             String::from("false_reports"),
@@ -125,6 +152,8 @@ pub fn request(
         return Err(CredentialError::NoBridgesReported);
     }
 
+    let D = L.bucket.unwrap() * get_H();
+
     let mut N = Lox::using_pubkey(L.get_pubkey());
     N.bucket = L.bucket;
     N.trust_level = L.trust_level;
@@ -132,14 +161,13 @@ pub fn request(
     N.invites_remaining = L.invites_remaining;
     N.blockages = L.blockages;
     N.false_reports = Some(Scalar::from(false_reports + new_false_reports));
-    N.pending = L.pending;
 
     let params = report_submit::Params {
         max_false_reports: MAX_FALSE_REPORTS.into(),
-        new_false_reports: new_false_reports.into(),
+        bucket: L.bucket.unwrap(),
     };
-    match report_submit::prepare(rng, SESSION_ID, &L, N, &params) {
-        Ok(req_state) => Ok((req_state, bridges_being_reported)),
+    match report_submit::prepare(rng, SESSION_ID, &L, &R, N, &params) {
+        Ok(req_state) => Ok((req_state, cc, D, bridges_being_reported)),
         Err(e) => Err(CredentialError::CMZError(e)),
     }
 }
@@ -149,130 +177,146 @@ impl BridgeAuth {
     pub fn handle_report_submit(
         &mut self,
         req: report_submit::Request,
+        cc: CountryCode,
+        D: G,
         bridges_being_reported: [bool; MAX_BRIDGES_PER_BUCKET],
     ) -> Result<report_submit::Reply, CredentialError> {
         let mut rng = rand::thread_rng();
         let reqbytes = req.as_bytes();
         let recvreq = report_submit::Request::try_from(&reqbytes[..]).unwrap();
 
-        let mut bridges_with_old_reports = Vec::<[u8; 20]>::new();
+        let (bucket, bridges) = match self.bridge_table.get_bucket_and_bridges(D) {
+            Ok((bucket, bridges)) => (bucket, bridges),
+            Err(_) => {
+                return Err(CredentialError::InvalidField(
+                    String::from("D"),
+                    String::from("Submitted D does not match an existing bucket"),
+                ));
+            }
+        };
+
+        let mut bridges_with_new_reports = Vec::<[u8; 20]>::new();
 
         match report_submit::handle(
             &mut rng,
             SESSION_ID,
             recvreq,
-            |L: &mut Lox, N: &mut Lox| {
+            |L: &mut Lox, R: &mut Resolve, N: &mut Lox| {
                 L.set_privkey(&self.lox_priv);
+                R.set_privkey(&self.resolve_priv);
                 N.set_privkey(&self.lox_priv);
 
-                let bucket = match self
-                    .report_table
-                    .get_user_bucket(L.pending.unwrap().clone())
-                {
-                    Some(bucket) => bucket,
-                    None => {
-                        return Err(CMZError::RevealAttrMissing(
-                            "pending",
-                            "Pending value is incorrect",
-                        ));
-                    }
-                };
+                if L.pending != R.pending {
+                    return Err(CMZError::RevealAttrMissing(
+                        "pending",
+                        "Lox credential pending value does not match resolve credential pending value"
+                    ));
+                }
 
-                let bridges = match self.bridge_table.get_bridge_hashed_fingerprints(bucket) {
-                    Ok(bridges) => bridges,
-                    Err(_) => {
-                        return Err(CMZError::RevealAttrMissing(
-                            "bucket",
-                            "Bucket value is incorrect",
-                        ));
+                let pending = if L.pending == Some(Scalar::ZERO) {
+                    // Get random scalar with low bits 0
+                    let mut rng = rand::rngs::OsRng;
+                    let p = Scalar::random(&mut rng);
+                    let mut p_bytes = *p.as_bytes();
+                    p_bytes[0] = 0u8;
+                    Scalar::from_bytes_mod_order(p_bytes)
+                } else {
+                    match self.report_table.get_user_info(L.pending.unwrap()) {
+                        Some(user_info) => {
+                            // Check that submitted data matches previously submitted data
+                            if user_info.bucket * get_H() != D {
+                                return Err(CMZError::RevealAttrMissing(
+                                    "D",
+                                    "D does not equal bucket * H",
+                                ));
+                            }
+                            if user_info.cc != cc {
+                                return Err(CMZError::RevealAttrMissing(
+                                    "cc",
+                                    "cc does not equal previously used cc",
+                                ));
+                            }
+                            if user_info.current_pending_value != L.pending.unwrap() {
+                                return Err(CMZError::RevealAttrMissing(
+                                    "pending",
+                                    "incorrect pending value",
+                                ));
+                            }
+
+                            L.pending.unwrap()
+                        }
+                        None => {
+                            return Err(CMZError::RevealAttrMissing(
+                                "pending",
+                                "incorrect pending value",
+                            ));
+                        }
                     }
                 };
 
                 // Check that user indicated at least one non-default bridge to
                 // submit a report for
-                let mut reporting_a_bridge = false;
+                let bridges_pending = [R.bridge_0_pending, R.bridge_1_pending, R.bridge_2_pending];
                 for i in 0..MAX_BRIDGES_PER_BUCKET {
                     if bridges_being_reported[i] {
-                        if bridges[i] == BridgeLine::default().get_hashed_fingerprint() {
+                        if bridges_pending[i] != Some(Scalar::ZERO) {
+                            return Err(CMZError::RevealAttrMissing(
+                                "bridges_being_reported",
+                                "Report submitted for a bridge that already has a pending report",
+                            ));
+                        } else if bridges[i] == BridgeLine::default().get_hashed_fingerprint() {
                             return Err(CMZError::RevealAttrMissing(
                                 "bridges_being_reported",
                                 "Report submitted for a bridge that does not exist",
                             ));
+                        } else {
+                            bridges_with_new_reports.push(bridges[i]);
                         }
-                        reporting_a_bridge = true;
                     }
                 }
-                if !reporting_a_bridge {
+                if bridges_with_new_reports.len() == 0 {
                     return Err(CMZError::RevealAttrMissing(
                         "bridges_being_reported",
                         "No bridges are being reported",
                     ));
                 }
 
-                let mut new_false_reports: u32 = 0;
-
-                for i in 0..MAX_BRIDGES_PER_BUCKET {
-                    let status = self
-                        .report_table
-                        .get_report_status(&bridges[i], L.pending.unwrap().clone());
-
-                    // Check for old reports
-                    if status == ReportStatus::NotBlocked
-                        || status == ReportStatus::Blocked
-                        || status == ReportStatus::Timeout
-                    {
-                        // We have an old report that will need to be cleaned up afterwards
-                        bridges_with_old_reports.push(bridges[i]);
-                    }
-
-                    if status == ReportStatus::Pending && bridges_being_reported[i] {
-                        // TODO: Fix this error
-                        return Err(CMZError::RevealAttrMissing(
-                            "pending",
-                            "Attempted to submit a report while a pending report exists",
-                        ));
-                    } else if status == ReportStatus::NotBlocked {
-                        // Report allowed for this bridge, but we have to add to false_reports
-                        new_false_reports += 1;
-                    }
-                    // Implicitly, other cases are fine
-                }
-
-                // Add the reports to the table
-                for i in 0..MAX_BRIDGES_PER_BUCKET {
-                    if bridges_being_reported[i] {
-                        self.report_table
-                            .process_new_report(bridges[i], L.pending.unwrap());
-                    }
-                }
+                N.pending = Some(pending);
 
-                N.pending = L.pending;
                 Ok(report_submit::Params {
                     max_false_reports: MAX_FALSE_REPORTS.into(),
-                    new_false_reports: new_false_reports.into(),
+                    bucket: bucket,
                 })
             },
-            |L: &Lox, _N: &Lox| {
+            |L: &Lox, _R: &Resolve, _N: &Lox| {
                 if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
                     return Err(CMZError::RevealAttrMissing("id", ""));
                 }
                 Ok(())
             },
         ) {
-            Ok((response, (L_issuer, _N_issuer))) => {
-                // Only if everything went okay, remove old reports from this user
-                if self
-                    .report_table
-                    .remove_old_reports(&bridges_with_old_reports, L_issuer.pending.unwrap())
-                {
-                    Ok(response)
-                } else {
-                    // TODO: Fix this error message/type
-                    Err(CredentialError::InvalidField(
-                        "pending".to_string(),
-                        "Failed to remove old reports".to_string(),
-                    ))
+            Ok((response, (_L_issuer, _R_issuer, N_issuer))) => {
+                // Ensure correct user data in table
+                match self.report_table.get_user_info(N_issuer.pending.unwrap()) {
+                    Some(_user_info) => {
+                        // If everything went okay, remove any existing resolve
+                        // credential for this user
+                        self.report_table
+                            .set_user_resolve_cred(N_issuer.pending.unwrap(), None);
+                    }
+                    None => {
+                        // Add new user's data
+                        self.report_table
+                            .add_user(N_issuer.pending.unwrap(), bucket, cc);
+                    }
                 }
+
+                // Add new reports to the table
+                for bridge in bridges_with_new_reports {
+                    self.report_table
+                        .process_new_report(bridge, N_issuer.pending.unwrap());
+                }
+                Ok(response)
             }
             Err(e) => Err(CredentialError::CMZError(e)),
         }
@@ -298,7 +342,7 @@ mod tests {
     use crate::proto::*;
     use crate::{
         report_table::{CountryCode, ReportStatus},
-        BridgeLine, Scalar, MAX_BRIDGES_PER_BUCKET,
+        Scalar,
     };
     use std::time::SystemTime;
 
@@ -313,71 +357,88 @@ mod tests {
         th.verify_lox(&lox_cred);
         assert_eq!(lox_cred.pending.unwrap(), Scalar::ZERO);
 
-        // Enter pending state
-        lox_cred = th.report_init(rng, lox_cred.clone(), cc);
-        th.verify_lox(&lox_cred);
-        let pending_1 = lox_cred.pending.unwrap();
-        assert!(pending_1 != Scalar::ZERO);
-
-        // Figure out which bridge is non-default
+        // Get bridges
         let (bucket, _reach_cred) = th.get_bucket(lox_cred.clone());
-        let mut bridges_to_report = [false; MAX_BRIDGES_PER_BUCKET];
-        for i in 0..bucket.len() {
-            if bucket[i].get_hashed_fingerprint() != BridgeLine::default().get_hashed_fingerprint()
-            {
-                bridges_to_report[i] = true;
-            }
-        }
 
-        // Only bridge 0 should be non-default
-        assert_eq!(bridges_to_report, [true, false, false]);
+        // Check we start in non-pending state
+        assert_eq!(lox_cred.pending, Some(Scalar::ZERO));
+
+        // Scalar with low byte 0
+        let scalar = Scalar::random(rng);
+        let mut scalar_bytes = scalar.as_bytes().clone();
+        scalar_bytes[0] = 0u8;
+        let scalar = Scalar::from_bytes_mod_order(scalar_bytes);
+        assert_eq!(scalar.as_bytes()[0], 0u8);
 
         // Only bridge 0 exists. We should not be able to submit a
         // report for bridge 1 or 2.
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 0, [false, true, false]);
-        let ((rs_request, _rs_client_state), bridges_being_reported_server) =
+        let mut resolve_cred = th.ba.get_resolve_cred_for_new_session(rng);
+
+        assert_eq!(resolve_cred.pending, Some(Scalar::ZERO));
+
+        let report_submit_request = report_submit::request(
+            rng,
+            lox_cred.clone(),
+            resolve_cred,
+            cc,
+            [false, true, false],
+        );
+        let ((rs_request, _rs_client_state), cc_server, D, bridges_being_reported_server) =
             report_submit_request.unwrap();
-        let report_submit_response = th
-            .ba
-            .handle_report_submit(rs_request, bridges_being_reported_server);
+        let report_submit_response =
+            th.ba
+                .handle_report_submit(rs_request, cc_server, D, bridges_being_reported_server);
         assert!(report_submit_response.is_err());
 
+        assert_eq!(lox_cred.pending, Some(Scalar::ZERO));
+
         // Submit a report for bridge 0
-        lox_cred = th.report_submit(rng, lox_cred.clone(), 0, [true, false, false]);
+        lox_cred = th.report_submit(rng, lox_cred.clone(), None, cc, [true, false, false]);
         th.verify_lox(&lox_cred);
-        assert_eq!(pending_1, lox_cred.pending.unwrap());
+        let pending_1 = lox_cred.pending.unwrap();
+        assert_eq!(lox_cred.pending.unwrap().as_bytes()[0], 0u8);
 
         // Get report status
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
         th.verify_lox(&lox_cred);
-        assert_eq!(pending_1, lox_cred.pending.unwrap());
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Pending,
-                ReportStatus::NoReport,
-                ReportStatus::NoReport
-            ]
-        );
+        let pending_2 = lox_cred.pending.unwrap();
+        assert_eq!(pending_1 + Scalar::ONE, pending_2);
+        assert_eq!(resolve_cred.pending.unwrap(), pending_2);
+        assert_eq!(resolve_cred.new_false_reports, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_0_pending, Some(Scalar::ONE));
+        assert_eq!(resolve_cred.bridge_1_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_2_pending, Some(Scalar::ZERO));
+
+        // Resolve (stay in pending state because we have a pending report)
+        lox_cred = th.report_resolve(rng, lox_cred.clone(), resolve_cred.clone());
+        th.verify_lox(&lox_cred);
+        assert_eq!(lox_cred.pending.unwrap(), pending_2);
 
-        // We should be unable to resolve pending state due to pending
-        // report
-        let report_resolve_request = report_resolve::request(rng, lox_cred.clone(), 0);
-        let (rr_request, _rr_client_state) = report_resolve_request.unwrap();
-        let report_resolve_response = th.ba.handle_report_resolve(rr_request);
-        assert!(report_resolve_response.is_err());
+        // Get report status
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
+        th.verify_lox(&lox_cred);
+        let pending_3 = lox_cred.pending.unwrap();
+        assert_eq!(pending_2 + Scalar::ONE, pending_3);
+        assert_eq!(resolve_cred.pending.unwrap(), pending_3);
+        assert_eq!(resolve_cred.new_false_reports, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_0_pending, Some(Scalar::ONE));
+        assert_eq!(resolve_cred.bridge_1_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_2_pending, Some(Scalar::ZERO));
 
         // We should be unable to submit another report for bridge 0 due
         // to pending report for bridge 0
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 0, [true, false, false]);
-        let ((rs_request, _rs_client_state), bridges_being_reported_server) =
+        let report_submit_request = report_submit::request(
+            rng,
+            lox_cred.clone(),
+            resolve_cred.clone(),
+            cc,
+            [true, false, false],
+        );
+        let ((rs_request, _rs_client_state), cc_server, D, bridges_being_reported_server) =
             report_submit_request.unwrap();
-        let report_submit_response = th
-            .ba
-            .handle_report_submit(rs_request, bridges_being_reported_server);
+        let report_submit_response =
+            th.ba
+                .handle_report_submit(rs_request, cc_server, D, bridges_being_reported_server);
         assert!(report_submit_response.is_err());
 
         // Let the report time out
@@ -387,10 +448,27 @@ mod tests {
             SystemTime::now(),
         );
 
+        // Get report status
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
+        th.verify_lox(&lox_cred);
+        let pending_4 = lox_cred.pending.unwrap();
+        assert_eq!(pending_3 + Scalar::ONE, pending_4);
+        assert_eq!(resolve_cred.pending.unwrap(), pending_4);
+        assert_eq!(resolve_cred.new_false_reports, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_0_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_1_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_2_pending, Some(Scalar::ZERO));
+
         // Submit another report for bridge 0
-        lox_cred = th.report_submit(rng, lox_cred.clone(), 0, [true, false, false]);
+        lox_cred = th.report_submit(
+            rng,
+            lox_cred.clone(),
+            Some(resolve_cred.clone()),
+            cc,
+            [true, false, false],
+        );
         th.verify_lox(&lox_cred);
-        assert_eq!(pending_1, lox_cred.pending.unwrap());
+        assert_eq!(pending_4, lox_cred.pending.unwrap());
 
         // Bridge was not blocked
         th.ba.report_table.process_scan_result(
@@ -400,37 +478,27 @@ mod tests {
         );
 
         // Get report status
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
         th.verify_lox(&lox_cred);
-        assert_eq!(pending_1, lox_cred.pending.unwrap());
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::NotBlocked,
-                ReportStatus::NoReport,
-                ReportStatus::NoReport
-            ]
-        );
+        let pending_5 = lox_cred.pending.unwrap();
+        assert_eq!(pending_4 + Scalar::ONE, pending_5);
+        assert_eq!(resolve_cred.pending.unwrap(), pending_5);
+        assert_eq!(resolve_cred.new_false_reports, Some(Scalar::ONE));
+        assert_eq!(resolve_cred.bridge_0_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_1_pending, Some(Scalar::ZERO));
+        assert_eq!(resolve_cred.bridge_2_pending, Some(Scalar::ZERO));
 
         // Submit a new report. We need to increase false_reports.
-        lox_cred = th.report_submit(rng, lox_cred.clone(), 1, [true, false, false]);
+        lox_cred = th.report_submit(
+            rng,
+            lox_cred.clone(),
+            Some(resolve_cred.clone()),
+            cc,
+            [true, false, false],
+        );
         th.verify_lox(&lox_cred);
         assert_eq!(Scalar::ONE, lox_cred.false_reports.unwrap());
 
-        // Get report status
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
-        th.verify_lox(&lox_cred);
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Pending,
-                ReportStatus::NoReport,
-                ReportStatus::NoReport
-            ]
-        );
-
         // Time out
         th.ba.report_table.mark_old_reports_simulated_time(
             &bucket[0].get_hashed_fingerprint(),
@@ -438,10 +506,12 @@ mod tests {
             SystemTime::now(),
         );
 
-        // Resolve so we can trust promote
-        lox_cred = th.report_resolve(rng, lox_cred.clone(), 0);
-        assert_eq!(Scalar::ZERO, lox_cred.pending.unwrap());
-        assert_eq!(Scalar::ONE, lox_cred.false_reports.unwrap());
+        // Get status
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
+
+        // Resolve so we can trust promote (exit pending state)
+        lox_cred = th.report_resolve(rng, lox_cred.clone(), resolve_cred.clone());
+        assert_eq!(lox_cred.pending, Some(Scalar::ZERO));
 
         // Promote/migrate to level 1
         let mig_cred = th.trust_promotion(rng, lox_cred.clone());
@@ -453,28 +523,11 @@ mod tests {
         // Get our new bucket
         let (bucket, _reach_cred) = th.get_bucket(lox_cred.clone());
 
-        // Try to submit reports for bridges 1 and 2 (fails because
-        // we are not in pending state)
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 0, [false, true, true]);
-        assert!(report_submit_request.is_err());
-
-        // Enter pending state
-        lox_cred = th.report_init(rng, lox_cred.clone(), cc);
-
         // Submit reports for bridges 0 and 2
-        lox_cred = th.report_submit(rng, lox_cred.clone(), 0, [true, false, true]);
-
-        // Try to submit reports for bridges 1 and 2 (fails because
-        // there is a pending report for bridge 2)
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 0, [false, true, true]);
-        let ((rs_request, _rs_client_state), bridges_being_reported_server) =
-            report_submit_request.unwrap();
-        let report_submit_response = th
-            .ba
-            .handle_report_submit(rs_request, bridges_being_reported_server);
-        assert!(report_submit_response.is_err());
+        lox_cred = th.report_submit(rng, lox_cred.clone(), None, cc, [true, false, true]);
+        // Pending should be new random scalar with low byte 0
+        assert!(lox_cred.pending.unwrap() != pending_1);
+        assert_eq!(lox_cred.pending.unwrap().as_bytes()[0], 0u8);
 
         // Bridge 2 is not blocked
         th.ba.report_table.process_scan_result(
@@ -483,34 +536,32 @@ mod tests {
             ReportStatus::NotBlocked,
         );
 
-        // Get report statuses
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
-        th.verify_lox(&lox_cred);
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Pending,
-                ReportStatus::NoReport,
-                ReportStatus::NotBlocked
-            ]
+        // Get status
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
+
+        // Try to submit reports for a different country (should not work)
+        let report_submit_request = report_submit::request(
+            rng,
+            lox_cred.clone(),
+            resolve_cred.clone(),
+            CountryCode::BY,
+            [false, true, false],
         );
+        let ((rs_request, _rs_client_state), cc_server, D, bridges_being_reported_server) =
+            report_submit_request.unwrap();
+        let report_submit_response =
+            th.ba
+                .handle_report_submit(rs_request, cc_server, D, bridges_being_reported_server);
+        assert!(report_submit_response.is_err());
 
         // Submit reports for bridges 1 and 2 (must increase
         // false_reports by 1)
-        lox_cred = th.report_submit(rng, lox_cred.clone(), 1, [false, true, true]);
-
-        // Get report statuses
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
-        th.verify_lox(&lox_cred);
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Pending,
-                ReportStatus::Pending,
-                ReportStatus::Pending
-            ]
+        lox_cred = th.report_submit(
+            rng,
+            lox_cred.clone(),
+            Some(resolve_cred),
+            cc,
+            [false, true, true],
         );
 
         // Two of them are accessible, and one is blocked
@@ -531,38 +582,46 @@ mod tests {
             ReportStatus::NotBlocked,
         );
 
+        // Level up
+        lox_cred = th.level_up(rng, lox_cred.clone());
+
         // Get report statuses
-        let report_statuses;
-        (lox_cred, report_statuses) = th.report_status(rng, lox_cred.clone());
-        th.verify_lox(&lox_cred);
-        assert_eq!(
-            report_statuses,
-            [
-                ReportStatus::Blocked,
-                ReportStatus::NotBlocked,
-                ReportStatus::NotBlocked
-            ]
-        );
+        (lox_cred, resolve_cred) = th.report_status(rng, lox_cred.clone());
 
         // Can't submit more reports because we've already submitted too
         // many false reports
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 2, [false, true, true]);
+        let report_submit_request = report_submit::request(
+            rng,
+            lox_cred.clone(),
+            resolve_cred.clone(),
+            cc,
+            [false, true, true],
+        );
         assert!(report_submit_request.is_err());
 
-        // Can't just lie about the number of false reports
-        let report_submit_request =
-            report_submit::request(rng, lox_cred.clone(), 0, [false, true, true]);
-        let ((rs_request, _rs_client_state), bridges_being_reported_server) =
-            report_submit_request.unwrap();
-        let report_submit_response = th
-            .ba
-            .handle_report_submit(rs_request, bridges_being_reported_server);
-        assert!(report_submit_response.is_err());
-
         // Resolve
-        let lox_cred = th.report_resolve(rng, lox_cred.clone(), 2);
+        lox_cred = th.report_resolve(rng, lox_cred.clone(), resolve_cred.clone());
         th.verify_lox(&lox_cred);
         assert_eq!(Scalar::ZERO, lox_cred.pending.unwrap());
+
+        // Invite a friend
+        let invitation;
+        (invitation, lox_cred) = th.issue_invite(rng, lox_cred.clone());
+        let redeem_invite_request =
+            redeem_invite::request(rng, invitation, th.ba.lox_pub.clone(), th.ba.today());
+        let (redeem_invite_request, redeem_invite_client_state) = redeem_invite_request.unwrap();
+        let redeem_invite_response = th.ba.handle_redeem_invite(redeem_invite_request);
+        assert!(
+            redeem_invite_response.is_ok(),
+            "Redeem Invite response from server should succeed"
+        );
+        let response = redeem_invite_response.unwrap();
+        let r_cred = redeem_invite::handle_response(redeem_invite_client_state, response);
+        assert!(r_cred.is_ok(), "Handle response should succeed");
+        let friend_cred = r_cred.unwrap();
+        th.verify_lox(&friend_cred);
+
+        // Invited friend inherits false_reports
+        assert_eq!(lox_cred.false_reports, friend_cred.false_reports);
     }
 }

+ 119 - 32
crates/lox-extensions/src/report_table.rs

@@ -1,7 +1,7 @@
 /*! Table of information about reports that have been submitted. */
 
 #[cfg(feature = "bridgeauth")]
-use crate::{BridgeLine, Scalar};
+use crate::{lox_creds::Resolve, BridgeLine, Scalar};
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "bridgeauth")]
 use std::collections::HashMap;
@@ -17,6 +17,9 @@ const TIMEOUT_PERIOD: Duration = Duration::new(60 * 60 * 12, 0);
 // A user who submits more than this many false reports should not be
 // allowed to submit more reports.
 pub const MAX_FALSE_REPORTS: u32 = 2;
+// Users are not allowed to do more than this many report actions before
+// resolving their credential to pending = 0.
+pub const MAX_REPORT_ACTIONS_IN_SESSION: u8 = 100;
 
 // Country codes present in Tor's GeoIP database
 // List generated with: `cat /usr/share/tor/geoip{,6} | grep -v ^# | grep -o ..$ | sort | uniq | while read cc; do echo "    ${cc},";done | tail -n+2`
@@ -296,15 +299,21 @@ pub enum ReportStatus {
     Timeout = 2,
     Pending = 3,
     NoReport = 4,
+    BridgeDown = 5,
 }
 
 #[derive(Default, Debug, Serialize, Deserialize)]
 #[cfg(feature = "bridgeauth")]
 struct BridgeReportInfo {
-    new_reports: Vec<(SystemTime, Scalar)>,
-    old_reports: HashMap<Scalar, ReportStatus>,
+    // Vector of timestamps and pending byte arrays (with low byte 0)
+    new_reports: Vec<(SystemTime, [u8; 32])>,
+    // Map of pending byte arrays (with low byte 0) to report statuses
+    old_reports: HashMap<[u8; 32], ReportStatus>,
+    // Timestamp of the last scan, if one happened
     last_scan_timestamp: Option<SystemTime>,
+    // Result of the last scan, if one happened
     last_scan_result: Option<ReportStatus>,
+    // Is there currently a scan in progress?
     scan_in_progress: bool,
 }
 
@@ -333,7 +342,7 @@ impl BridgeReportInfo {
         let current_time = SystemTime::now();
         // new_reports should be ordered by timestamps, so it might be
         // possible to abort early for better efficiency
-        let mut new_reports = Vec::<(SystemTime, Scalar)>::new();
+        let mut new_reports = Vec::<(SystemTime, [u8; 32])>::new();
 
         for (time, pending) in self.new_reports.iter() {
             if let Ok(duration) = current_time.duration_since(*time) {
@@ -350,8 +359,10 @@ impl BridgeReportInfo {
     // Returns true if there is a pending report for this user, false
     // otherwise.
     fn contains_pending(&self, pending: Scalar) -> bool {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
         for (_time, existing_pending) in self.new_reports.iter() {
-            if *existing_pending == pending {
+            if *existing_pending == pending_bytes {
                 return true;
             }
         }
@@ -362,6 +373,9 @@ impl BridgeReportInfo {
     // report to the list, and launch a scan if applicable. Returns true
     // if successful, false otherwise.
     fn process_new_report(&mut self, pending: Scalar) -> bool {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+
         let current_time = SystemTime::now();
 
         // First, remove any old reports from the list
@@ -373,7 +387,7 @@ impl BridgeReportInfo {
         }
 
         // Add this new report to the list
-        self.new_reports.push((current_time, pending));
+        self.new_reports.push((current_time, pending_bytes));
 
         // Scan if applicable
         if !self.scan_in_progress && self.new_reports.len() >= NUM_REPORTS_TO_SCAN {
@@ -403,8 +417,11 @@ impl BridgeReportInfo {
     // Removes a report from the old_reports list. Returns true if
     // successful, false otherwise.
     fn remove_report(&mut self, pending: Scalar) -> bool {
-        if self.old_reports.contains_key(&pending) {
-            self.old_reports.remove(&pending).unwrap();
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+
+        if self.old_reports.contains_key(&pending_bytes) {
+            self.old_reports.remove(&pending_bytes).unwrap();
             true
         } else {
             false
@@ -412,6 +429,22 @@ impl BridgeReportInfo {
     }
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+#[cfg(feature = "bridgeauth")]
+pub struct UserInfo {
+    // User's bucket scalar
+    pub bucket: Scalar,
+    // User's country code
+    pub cc: CountryCode,
+    // The current pending value for the user. This increments when the
+    // user performs the report_status action.
+    pub current_pending_value: Scalar,
+    // If the user most recently performed the report_status action, we
+    // store their resolve credential so if they perform report_status
+    // again, we can serve it to them again.
+    current_resolve_cred: Option<Resolve>,
+}
+
 #[derive(Default, Debug, Serialize, Deserialize)]
 #[cfg(feature = "bridgeauth")]
 pub struct ReportTable {
@@ -419,7 +452,7 @@ pub struct ReportTable {
     // reports for the bridge from that country
     data: HashMap<([u8; 20], CountryCode), BridgeReportInfo>,
     // Map of pending user scalars to their buckets and country codes
-    pending_users: HashMap<Scalar, (Scalar, CountryCode)>,
+    pending_users: HashMap<[u8; 32], UserInfo>,
 }
 
 // Public interface
@@ -427,24 +460,68 @@ pub struct ReportTable {
 impl ReportTable {
     // When the user does report_init, add them to pending_users
     pub fn add_user(&mut self, pending: Scalar, bucket: Scalar, cc: CountryCode) {
-        self.pending_users.insert(pending, (bucket, cc));
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        self.pending_users.insert(
+            pending_bytes,
+            UserInfo {
+                bucket: bucket,
+                cc: cc,
+                current_pending_value: pending,
+                current_resolve_cred: None,
+            },
+        );
     }
 
-    // When the user does report_resolve, remove them from pending_users
+    // When the user does report_resolve with no pending reports, remove
+    // them from pending_users
     pub fn remove_user(&mut self, pending: Scalar) {
-        let result = self.pending_users.remove(&pending);
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        let result = self.pending_users.remove(&pending_bytes);
         if result.is_none() {
             // If the user doesn't exist, just print a message about it
             eprintln!("Tried to remove a user that didn't exist");
         }
     }
 
-    // Get the bucket for a given pending user
-    pub fn get_user_bucket(&self, pending: Scalar) -> Option<Scalar> {
-        if let Some(user_data) = self.pending_users.get(&pending) {
-            Some(user_data.0)
-        } else {
-            None
+    // Get the bucket and country code for a given pending user
+    pub fn get_user_info(&self, pending: Scalar) -> Option<&UserInfo> {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        self.pending_users.get(&pending_bytes)
+    }
+
+    // Set a user's current pending scalar
+    pub fn set_user_pending(&mut self, pending: Scalar) {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        match self.pending_users.get_mut(&pending_bytes) {
+            Some(user_info) => {
+                if pending != user_info.current_pending_value + Scalar::ONE {
+                    eprintln!("User's new pending value is not one more than previous pending value. Something probably went wrong.");
+                }
+                user_info.current_pending_value = pending;
+            }
+            None => {
+                // If the user doesn't exist, just print a message about it
+                eprintln!("Tried to set resolve cred for user who didn't exist");
+            }
+        }
+    }
+
+    // Set a user's current resolve credential
+    pub fn set_user_resolve_cred(&mut self, pending: Scalar, resolve_cred: Option<Resolve>) {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        match self.pending_users.get_mut(&pending_bytes) {
+            Some(user_info) => {
+                user_info.current_resolve_cred = resolve_cred;
+            }
+            None => {
+                // If the user doesn't exist, just print a message about it
+                eprintln!("Tried to set resolve cred for user who didn't exist");
+            }
         }
     }
 
@@ -455,16 +532,22 @@ impl ReportTable {
             return false;
         }
 
-        let Some((_bucket, cc)) = self.pending_users.get(&pending) else {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+
+        let Some(user_info) = self.pending_users.get(&pending_bytes) else {
             return false;
         };
 
-        if !self.data.contains_key(&(hashed_fingerprint, *cc)) {
+        if !self.data.contains_key(&(hashed_fingerprint, user_info.cc)) {
             let new_bridge_report_info = BridgeReportInfo::default();
             self.data
-                .insert((hashed_fingerprint, *cc), new_bridge_report_info);
+                .insert((hashed_fingerprint, user_info.cc), new_bridge_report_info);
         }
-        let bridge_report_info = self.data.get_mut(&(hashed_fingerprint, *cc)).unwrap();
+        let bridge_report_info = self
+            .data
+            .get_mut(&(hashed_fingerprint, user_info.cc))
+            .unwrap();
 
         bridge_report_info.process_new_report(pending)
     }
@@ -473,13 +556,15 @@ impl ReportTable {
     // the given user for those bridges. Return true if successful,
     // false otherwise.
     pub fn remove_old_reports(&mut self, bridges: &[[u8; 20]], pending: Scalar) -> bool {
-        let Some((_bucket, cc)) = self.pending_users.get(&pending) else {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        let Some(user_info) = self.pending_users.get(&pending_bytes) else {
             return false;
         };
 
         for bridge in bridges {
             if *bridge != BridgeLine::default().get_hashed_fingerprint() {
-                if let Some(bridge_data) = self.data.get_mut(&(*bridge, *cc)) {
+                if let Some(bridge_data) = self.data.get_mut(&(*bridge, user_info.cc)) {
                     if !bridge_data.remove_report(pending) {
                         return false;
                     }
@@ -491,18 +576,20 @@ impl ReportTable {
 
     // Get the status of user's report for a bridge
     pub fn get_report_status(&self, bridge: &[u8; 20], pending: Scalar) -> ReportStatus {
-        let Some((_bucket, cc)) = self.pending_users.get(&pending) else {
+        let mut pending_bytes = pending.as_bytes().clone();
+        pending_bytes[0] = 0u8;
+        let Some(user_info) = self.pending_users.get(&pending_bytes) else {
             return ReportStatus::NoReport;
         };
 
         if *bridge != BridgeLine::default().get_hashed_fingerprint() {
-            if let Some(bridge_data) = self.data.get(&(*bridge, *cc)) {
-                if bridge_data.old_reports.contains_key(&pending) {
-                    return *bridge_data.old_reports.get(&pending).unwrap();
+            if let Some(bridge_data) = self.data.get(&(*bridge, user_info.cc)) {
+                if bridge_data.old_reports.contains_key(&pending_bytes) {
+                    return *bridge_data.old_reports.get(&pending_bytes).unwrap();
                 }
 
                 for (_time, existing_pending) in bridge_data.new_reports.iter() {
-                    if *existing_pending == pending {
+                    if *existing_pending == pending_bytes {
                         return ReportStatus::Pending;
                     }
                 }
@@ -542,8 +629,8 @@ impl ReportTable {
 
         if let Some(bridge_data) = self.data.get_mut(&(*bridge, cc)) {
             for (_time, pending) in &bridge_data.new_reports {
-                if self.pending_users.contains_key(&pending) {
-                    bucket = Some(self.pending_users.get(&pending).unwrap().0);
+                if self.pending_users.contains_key(pending) {
+                    bucket = Some(self.pending_users.get(pending).unwrap().bucket);
                     break;
                 }
             }
@@ -571,7 +658,7 @@ impl ReportTable {
 
             // new_reports should be ordered by timestamps, so it might be
             // possible to abort early for better efficiency
-            let mut new_reports = Vec::<(SystemTime, Scalar)>::new();
+            let mut new_reports = Vec::<(SystemTime, [u8; 32])>::new();
 
             for (time, pending) in bridge_data.new_reports.iter() {
                 if let Ok(duration) = current_time.duration_since(*time) {

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio