diff --git a/crates/lox-distributor/config.json b/crates/lox-distributor/config.json index 438cab1..90538e2 100644 --- a/crates/lox-distributor/config.json +++ b/crates/lox-distributor/config.json @@ -9,10 +9,10 @@ }, "rtype": { "endpoint": "http://127.0.0.1:7100/resources", - "name": "https", - "token": "HttpsApiTokenPlaceholder", + "name": "lox", + "token": "LoxApiTokenPlaceholder", "types": [ - "obfs2", + "obfs4", "scramblesuit" ] } diff --git a/crates/lox-library/README.md b/crates/lox-library/README.md index e05f6fa..c8be9fc 100644 --- a/crates/lox-library/README.md +++ b/crates/lox-library/README.md @@ -1,8 +1,40 @@ # Lox Lox is a reputation-based bridge distribution system that provides privacy protection to users and their social graph and is open to all users. -Lox is written in rust and requires `cargo` to test. [Install Rust](https://www.rust-lang.org/tools/install). We used Rust version 1.56.0. -Note that this implementation is coded such that the reachability certificate expires at 00:00 UTC. In reality, if the bucket is still reachable, a user could simply request a new reachability token if their request fails for this reason (a new certificate should be available prior to the outdated certificate expiring). +The protocols in the Lox-library are consistent with the Lox system described +in [Tulloch and Goldberg](https://petsymposium.org/popets/2023/popets-2023-0029.php) (and in greater detail [here](https://uwspace.uwaterloo.ca/handle/10012/18333)). However, this implementation may diverge from the theory over time as the system is deployed and its limitations are better illuminated. The [original version of this library](https://git-crysp.uwaterloo.ca/iang/lox) will remain a more precise implementation of the theory. +Lox is written in rust and requires `cargo` to test. [Install Rust](https://www.rust-lang.org/tools/install). We used Rust version 1.65.0. +## Notable Changes from the original repository + +Some changes have been made to integrate the existing Lox protocols with Tor's +bridge distributor [rdsys](https://gitlab.torproject.org/tpo/anti-censorship/rdsys), +but so far, these have not affected the Lox protocols themselves. + +These changes are necessary to keep the consistentcy of bridges in buckets that Lox requires while working with the reality of how rdsys/Tor currently receives and distributes information about bridges. The changes to Lox are: +1. Add a `uid_fingerprint` field to the BridgeLine which helps with bridge lookup and + corresponds (roughly) to the unique fingerprint rdsys gives to each bridge + (made up of a hash of the IP and pluggable transport type) +2. Allow for the details of a bridge to be updated. This has been added to + [`crates/lox-library/src/lib.rs`](https://gitlab.torproject.org/tpo/anti-censorship/lox-rs/-/blob/main/crates/lox-library/src/lib.rs) and accounts for the fact that some details + of an existing bridge (i.e., that has a matching fingerprint) may be updated + from time to time. +3. Allow for a bridge to be replaced without penalty. This has also been added to + [`crates/lox-library/src/lib.rs`](https://gitlab.torproject.org/tpo/anti-censorship/lox-rs/-/blob/main/crates/lox-library/src/lib.rs) + and accounts for the fact that Tor currently does not have a robust way of + [knowing that a bridge is blocked](https://gitlab.torproject.org/tpo/anti-censorship/censorship-analysis/-/issues/40035), but does have some tests (namely, + [bridgestrap](https://gitlab.torproject.org/tpo/anti-censorship/bridgestrap) and [onbasca](https://gitlab.torproject.org/tpo/network-health/onbasca)) that help to determine if a + bridge should not be distributed. Since we do not know if the results of + these tests indicate a blocking event, we are allowing for bridges that + rdsys marks as unsuitable for distribution to be updated without penalty in the Lox library. +4. The vectors within `bridge_table.rs` have been refactored into HashMaps that use a unique `u32` for lookup. This has led to a +number of changes around how bridges are inserted/removed from the bridge table but does not impact the overall functionality of the Lox system. +5. The `DupFilter` has been changed from a `HashMap` to a `HashSet`, primarily because this is easier to Serialize/Deserialize when storing the state of the Lox system to recover from failure or to be able to roll back to a previous state. +6. The [`dalek-cryptography`](https://dalek.rs/) libraries have been updated to their most recent versions and the `zkp` library has been forked (until/unless this is fixed in one of the existing upstream repos) to fix a bug that appears when a public attribute is set to 0 (previously impacting only the blockage migration protocol when a user's invitations are set to 0 after migrating). The fork of `zkp` also includes similar updates to `dalek-cryptography` dependencies and some others such as `rand`. +7. Many tests that were used to create the Lox paper/thesis and measure the performance of the system were removed from this repository as they are unnecessary in a deployment scenario. They are still available in the [original repository](https://git-crysp.uwaterloo.ca/iang/lox). + +### Other important Notes + +As with the original implementation, this implementation is coded such that the reachability certificate expires at 00:00 UTC. Therefore, if an unlucky user requests a reachability certificate just before the 00:00 UTC and tries to use it just after, the request will fail. If the bucket is still reachable, a user can simply request a new reachability token if their request fails for this reason (a new certificate should be available prior to the outdated certificate expiring). diff --git a/crates/lox-library/src/bridge_table.rs b/crates/lox-library/src/bridge_table.rs index 43e248b..4ec1ce4 100644 --- a/crates/lox-library/src/bridge_table.rs +++ b/crates/lox-library/src/bridge_table.rs @@ -240,41 +240,46 @@ struct K { /// A BridgeTable is the internal structure holding the buckets /// containing the bridges, the keys used to encrypt the buckets, and -/// the encrypted buckets. The encrypted buckets will be exposed to the +/// the encrypted buckets. The encrypted buckets will be exposed to the /// users of the system, and each user credential will contain the /// decryption key for one bucket. #[serde_as] #[derive(Debug, Default, Serialize, Deserialize)] pub struct BridgeTable { - // All structures in the bridgetable are indexed by counter + /// All structures in the bridgetable are indexed by counter pub counter: u32, + /// The keys of all buckets, indexed by counter, that are still part of the bridge table. pub keys: HashMap, + /// All buckets, indexed by counter corresponding to the key above, that are + /// part of the bridge table. pub buckets: HashMap, pub encbuckets: HashMap, - /// Individual bridges that are reachable + /// Individual bridges that are reachable. #[serde_as(as = "HashMap")] pub reachable: HashMap>, - /// bucket ids of "hot spare" buckets. These buckets are not handed + /// Bucket ids of "hot spare" buckets. These buckets are not handed /// to users, nor do they have any Migration credentials pointing to - /// them. When a new Migration credential is needed, a bucket is + /// them. When a new Migration credential is needed, a bucket is /// removed from this set and used for that purpose. pub spares: HashSet, - /// In some instances a single bridge may need to be added to a bucket - /// In that case, a spare bucket will be removed from the set of spare bridges. One + /// In some instances a single bridge may need to be added to a bucket as a replacement + /// or otherwise. In that case, a spare bucket will be removed from the set of spares, one + /// bridge will be used as the replacement and the left over bridges will be appended to + /// unallocated_bridges. pub unallocated_bridges: Vec, - // To prevent issues with a counter for the hashmap keys, we keep a list of keys that - // no longer match any buckets that can be used before increasing the counter + // To prevent issues with the counter for the hashmap keys, keep a list of keys that + // no longer match any buckets that can be used before increasing the counter. pub recycleable_keys: Vec, - // We maintain a list of keys that have been blocked (bucket_id: u32), as well as the + // A list of keys that have been blocked (bucket_id: u32), as well as the // time (julian_date: u32) of their blocking so that they can be repurposed with new - // buckets after the EXPIRY_DATE + // buckets after the EXPIRY_DATE. pub blocked_keys: Vec<(u32, u32)>, - // Similarly, we maintain a list of open entry buckets (bucket_id: u32) and the time they were - // created (julian_date: u32) so they will be listed as expired after the EXPIRY_DATE + // Similarly, a list of open entry buckets (bucket_id: u32) and the time they were + // created (julian_date: u32) so they will be listed as expired after the EXPIRY_DATE. // TODO: add open entry buckets to the open_inv_keys only once they have been distributed pub open_inv_keys: Vec<(u32, u32)>, /// The date the buckets were last encrypted to make the encbucket. - /// The encbucket must be rebuilt each day so that the Bucket + /// The encbucket must be rebuilt at least each day so that the Bucket /// Reachability credentials in the buckets can be refreshed. pub date_last_enc: u32, } @@ -288,7 +293,7 @@ impl BridgeTable { self.buckets.len() } - /// Append a new bucket to the bridge table, returning its index + /// Insert a new bucket into the bridge table, returning its index pub fn new_bucket(&mut self, index: u32, bucket: &[BridgeLine; MAX_BRIDGES_PER_BUCKET]) { // Pick a random key to encrypt this bucket let mut rng = rand::thread_rng(); @@ -311,7 +316,7 @@ impl BridgeTable { } /// Create the vector of encrypted buckets from the keys and buckets - /// in the BridgeTable. All of the entries will be (randomly) + /// in the BridgeTable. All of the entries will be (randomly) /// re-encrypted, so it will be hidden whether any individual bucket /// has changed (except for entirely new buckets, of course). /// Bucket Reachability credentials are added to the buckets when diff --git a/crates/lox-library/src/cred.rs b/crates/lox-library/src/cred.rs index 9f0fc57..04d450e 100644 --- a/crates/lox-library/src/cred.rs +++ b/crates/lox-library/src/cred.rs @@ -1,8 +1,8 @@ /*! The various credentials used by the system. -In each case, (P,Q) forms the MAC on the credential. This MAC is +In each case, (P,Q) forms the MAC on the credential. This MAC is verifiable only by the issuing party, or if the issuing party issues a -zero-knowledge proof of its correctness (as it does at issuing time). */ +zero-knowledge proof of its correctness (as it does at issuing time).*/ use curve25519_dalek::ristretto::RistrettoPoint; use curve25519_dalek::scalar::Scalar; @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; /// A migration credential. /// /// This credential authorizes the holder of the Lox credential with the -/// given id to switch from bucket from_bucket to bucket to_bucket. The +/// given id to switch from bucket from_bucket to bucket to_bucket. The /// migration_type attribute is 0 for trust upgrade migrations (moving /// from a 1-bridge untrusted bucket to a 3-bridge trusted bucket) and 1 /// for blockage migrations (moving buckets because the from_bucket has @@ -29,7 +29,7 @@ pub struct Migration { /// The main user credential in the Lox system. /// /// Its id is jointly generated by the user and the BA (bridge -/// authority), but known only to the user. The level_since date is the +/// authority), but known only to the user. The level_since date is the /// Julian date of when this user was changed to the current trust /// level. #[derive(Debug, Serialize, Deserialize)] @@ -46,13 +46,13 @@ pub struct Lox { /// The migration key credential. /// -/// This credential is never actually instantiated. It is an implicit -/// credential on attributes lox_id and from_bucket. This credential -/// type does have an associated private and public key, however. The +/// This credential is never actually instantiated. It is an implicit +/// credential on attributes lox_id and from_bucket. This credential +/// type does have an associated private and public key, however. The /// idea is that if a user proves (in zero knowledge) that their Lox /// credential entitles them to migrate from one bucket to another, the /// BA will issue a (blinded, so the BA will not know the values of the -/// attributes or of Q) MAC on this implicit credential. The Q value +/// attributes or of Q) MAC on this implicit credential. The Q value /// will then be used (actually, a hash of lox_id, from_bucket, and Q) /// to encrypt the to_bucket, P, and Q fields of a Migration credential. /// That way, people entitled to migrate buckets can receive a Migration @@ -70,7 +70,7 @@ pub struct MigrationKey { /// /// Each day, a credential of this type is put in each bucket that has /// at least a (configurable) threshold number of bridges that have not -/// been blocked as of the given date. Users can present this +/// been blocked as of the given date. Users can present this /// credential (in zero knowledge) with today's date to prove that the /// bridges in their bucket have not been blocked, in order to gain a /// trust level. @@ -86,7 +86,7 @@ pub struct BucketReachability { /// /// These credentials allow a Lox user (the inviter) of sufficient trust /// (level 2 or higher) to invite someone else (the invitee) to join the -/// system. The invitee ends up at trust level 1, in the _same bucket_ +/// system. The invitee ends up at trust level 1, in the _same bucket_ /// as the inviter, and inherits the inviter's blockages count (so that /// you can't clear your blockages count simply by inviting yourself). /// Invitations expire after some amount of time. diff --git a/crates/lox-library/src/dup_filter.rs b/crates/lox-library/src/dup_filter.rs index 2bd747a..5326562 100644 --- a/crates/lox-library/src/dup_filter.rs +++ b/crates/lox-library/src/dup_filter.rs @@ -10,7 +10,7 @@ use std::hash::Hash; use serde::{Deserialize, Serialize}; /// Each instance of DupFilter maintains its own independent table of -/// seen ids. IdType will typically be Scalar. +/// seen ids. IdType will typically be Scalar. #[derive(Default, Debug, Serialize, Deserialize)] pub struct DupFilter { seen_table: HashSet, diff --git a/crates/lox-library/src/lib.rs b/crates/lox-library/src/lib.rs index ee75858..cbbae94 100644 --- a/crates/lox-library/src/lib.rs +++ b/crates/lox-library/src/lib.rs @@ -8,7 +8,7 @@ Keyed-Verification Anonymous Credentials" (Chase, Meiklejohn, and Zaverucha, CCS 2014) The notation follows that of the paper "Hyphae: Social Secret Sharing" -(Lovecruft and de Valence, 2017), Section 4. */ +(Lovecruft and de Valence, 2017), Section 4. */ // We really want points to be capital letters and scalars to be // lowercase letters @@ -62,10 +62,12 @@ lazy_static! { } // EXPIRY_DATE is set to EXPIRY_DATE days for open-entry and blocked buckets in order to match -// the expiry date for Lox credentials. This particular value (EXPIRY_DATE) is chosen because +// the expiry date for Lox credentials.This particular value (EXPIRY_DATE) is chosen because // values that are 2^k − 1 make range proofs more efficient, but this can be changed to any value pub const EXPIRY_DATE: u32 = 511; +/// ReplaceSuccess sends a signal to the lox-distributor to inform +/// whether or not a bridge was successfully replaced #[derive(PartialEq, Eq)] pub enum ReplaceSuccess { NotFound = 0, @@ -73,18 +75,23 @@ pub enum ReplaceSuccess { Replaced = 2, } +/// This error is thrown if the number of buckets/keys in the bridge table +/// exceeds u32 MAX.It is unlikely this error will ever occur. #[derive(Error, Debug)] pub enum NoAvailableIDError { #[error("Find key exhausted with no available index found!")] ExhaustedIndexer, } +/// This error is thrown after the MAX_DAILY_BRIDGES threshold for bridges +/// distributed in a day has been reached #[derive(Error, Debug)] pub enum ExceededMaxBridgesError { #[error("The maximum number of bridges has already been distributed today, please try again tomorrow!")] ExceededMaxBridges, } +/// Private Key of the Issuer #[derive(Clone, Debug, Serialize, Deserialize)] pub struct IssuerPrivKey { x0tilde: Scalar, @@ -106,11 +113,13 @@ impl IssuerPrivKey { } } + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct IssuerPubKey { X: Vec, } +/// Public Key of the Issuer impl IssuerPubKey { /// Create an IssuerPubKey from the corresponding IssuerPrivKey pub fn new(privkey: &IssuerPrivKey) -> IssuerPubKey { @@ -130,11 +139,11 @@ impl IssuerPubKey { } } -// Number of times a given invitation is ditributed +/// Number of times a given invitation is ditributed pub const OPENINV_K: u32 = 10; -// TODO: Decide on maximum daily number of invitations to be distributed +/// TODO: Decide on maximum daily number of invitations to be distributed pub const MAX_DAILY_BRIDGES: u32 = 100; -/// The BridgeDb. This will typically be a singleton object. The +/// The BridgeDb. This will typically be a singleton object. The /// BridgeDb's role is simply to issue signed "open invitations" to /// people who are not yet part of the system. #[derive(Debug, Serialize, Deserialize)] @@ -188,6 +197,8 @@ impl BridgeDb { self.openinv_buckets.remove(bucket); } + /// Remove open invitation and/or otherwise distributed buckets that have + /// become blocked or are expired to free up the index for a new bucket pub fn remove_blocked_or_expired_buckets(&mut self, bucket: &u32) { if self.openinv_buckets.contains(bucket) { println!("Removing a bucket that has not been distributed yet!"); @@ -197,6 +208,7 @@ impl BridgeDb { } } + /// Mark a bucket as distributed pub fn mark_distributed(&mut self, bucket: u32) { self.distributed_buckets.push(bucket); } @@ -239,8 +251,8 @@ impl BridgeDb { } } - /// Verify an open invitation. Returns the invitation id and the - /// bucket number if the signature checked out. It is up to the + /// Verify an open invitation. Returns the invitation id and the + /// bucket number if the signature checked out. It is up to the /// caller to then check that the invitation id has not been used /// before. pub fn verify( @@ -250,7 +262,7 @@ impl BridgeDb { // Pull out the signature and verify it let sig = Signature::try_from(&invitation[(32 + 4)..])?; pubkey.verify(&invitation[0..(32 + 4)], &sig)?; - // The signature passed. Pull out the bucket number and then + // The signature passed. Pull out the bucket number and then // the invitation id let bucket = u32::from_le_bytes(invitation[32..(32 + 4)].try_into().unwrap()); let s = Scalar::from_canonical_bytes(invitation[0..32].try_into().unwrap()); @@ -270,7 +282,7 @@ impl Default for BridgeDb { } } -/// The bridge authority. This will typically be a singleton object. +/// The bridge authority. This will typically be a singleton object. #[derive(Debug, Serialize, Deserialize)] pub struct BridgeAuth { /// The private key for the main Lox credential @@ -363,7 +375,7 @@ impl BridgeAuth { /// Insert a set of open invitation bridges. /// /// Each of the bridges will be given its own open invitation - /// bucket, and the BridgeDb will be informed. A single bucket + /// bucket, and the BridgeDb will be informed. A single bucket /// containing all of the bridges will also be created, with a trust /// upgrade migration from each of the single-bridge buckets. pub fn add_openinv_bridges( @@ -406,6 +418,9 @@ impl BridgeAuth { Ok(()) } + /// When syncing the Lox bridge table with rdsys, this function returns any bridges + /// that are found in the Lox bridge table that are not found in the Vector + /// of bridges received from rdsys through the Lox distributor. pub fn find_and_remove_unaccounted_for_bridges( &mut self, accounted_for_bridges: Vec, @@ -419,6 +434,7 @@ impl BridgeAuth { unaccounted_for } + /// Allocate single left over bridges to an open invitation bucket pub fn allocate_bridges( &mut self, distributor_bridges: &mut Vec, @@ -447,12 +463,10 @@ impl BridgeAuth { // Update the details of a bridge in the bridge table. This assumes that the IP and Port // of a given bridge remains the same and thus can be updated. // First we must retrieve the list of reachable bridges, then we must search for any matching our partial key - // which will include the IP and Port. Then we can replace the original bridge with the updated bridge + // which will include the IP and Port. Finally we can replace the original bridge with the updated bridge. // Returns true if the bridge has successfully updated pub fn bridge_update(&mut self, bridge: &BridgeLine) -> bool { let mut res: bool = false; //default False to assume that update failed - //Needs to be updated since bridge will only match on some fields. - let reachable_bridges = self.bridge_table.reachable.clone(); for reachable_bridge in reachable_bridges { if reachable_bridge.0.uid_fingerprint == bridge.uid_fingerprint { @@ -490,6 +504,8 @@ impl BridgeAuth { res } + /// Attempt to remove a bridge that is failing tests and replace it with a bridge from + /// available_bridge or from a spare bucket pub fn bridge_replace( &mut self, bridge: &BridgeLine, @@ -591,13 +607,13 @@ impl BridgeAuth { /// Mark a bridge as unreachable /// /// This bridge will be removed from each of the buckets that - /// contains it. If any of those are open-invitation buckets, the + /// contains it. If any of those are open-invitation buckets, the /// trust upgrade migration for that bucket will be removed and the - /// BridgeDb will be informed to stop handing out that bridge. If + /// BridgeDb will be informed to stop handing out that bridge. If /// any of those are trusted buckets where the number of reachable /// bridges has fallen below the threshold, a blockage migration /// from that bucket to a spare bucket will be added, and the spare - /// bucket will be removed from the list of hot spares. In + /// bucket will be removed from the list of hot spares. In /// addition, if the blocked bucket was the _target_ of a blockage /// migration, change the target to the new (formerly spare) bucket. /// Returns true if sucessful, or false if it needed a hot spare but @@ -647,9 +663,9 @@ impl BridgeAuth { continue; } - // This bucket is now unreachable. Get a spare bucket + // This bucket is now unreachable. Get a spare bucket if self.bridge_table.spares.is_empty() { - // Uh, oh. No spares available. Just delete any + // Uh, oh. No spares available. Just delete any // migrations leading to this bucket. res = false; self.trustup_migration_table @@ -692,7 +708,7 @@ impl BridgeAuth { } // Since buckets are moved around in the bridge_table, finding a lookup key that - // does not overwrite existing bridges could become an issue. We keep a list + // does not overwrite existing bridges could become an issue.We keep a list // of recycleable lookup keys from buckets that have been removed and prioritize // this list before increasing the counter fn find_next_available_key(&mut self, bdb: &mut BridgeDb) -> Result { @@ -728,6 +744,7 @@ impl BridgeAuth { self.clean_up_open_entry(bdb); } + /// Cleans up exipred blocked buckets fn clean_up_blocked(&mut self) { if !self.bridge_table.blocked_keys.is_empty() && self @@ -754,7 +771,7 @@ impl BridgeAuth { if bridgeline.port > 0 { // Move to unallocated bridges self.bridge_table.unallocated_bridges.push(*bridgeline); - // Check if it's still in the reachable bridges. It should be if we've gotten this far. + // Check if it's still in the reachable bridges.It should be if we've gotten this far. if let Some(_reachable_indexes_for_bridgeline) = self.bridge_table.reachable.get(bridgeline) { @@ -780,6 +797,7 @@ impl BridgeAuth { } } + /// Cleans up expired open invitation buckets fn clean_up_open_entry(&mut self, bdb: &mut BridgeDb) { // First check if there are any open invitation indexes that are old enough to be replaced if !self.bridge_table.open_inv_keys.is_empty() @@ -825,14 +843,14 @@ impl BridgeAuth { self.time_offset += time::Duration::days(1); } - //#[cfg(test)] + ///#[cfg(test)] /// For testing only: manually advance the day by the given number /// of days pub fn advance_days(&mut self, days: u16) { self.time_offset += time::Duration::days(days.into()); } - /// Get today's (real or simulated) date + /// Get today's (real or simulated) date as u32 pub fn today(&self) -> u32 { // We will not encounter negative Julian dates (~6700 years ago) // or ones larger than 32 bits @@ -842,7 +860,7 @@ impl BridgeAuth { .unwrap() } - /// Get today's (real or simulated) date + /// Get today's (real or simulated) date as a DateTime value pub fn today_date(&self) -> DateTime { Utc::now() } @@ -961,13 +979,13 @@ pub fn pt_dbl(P: &RistrettoPoint) -> RistrettoPoint { /// The protocol modules. /// -/// Each protocol lives in a submodule. Each submodule defines structs +/// Each protocol lives in a submodule. Each submodule defines structs /// for Request (the message from the client to the bridge authority), /// State (the state held by the client while waiting for the reply), /// and Response (the message from the bridge authority to the client). /// Each submodule defines functions request, which produces a (Request, /// State) pair, and handle_response, which consumes a State and a -/// Response. It also adds a handle_* function to the BridgeAuth struct +/// Response. It also adds a handle_* function to the BridgeAuth struct /// that consumes a Request and produces a Result. pub mod proto { pub mod blockage_migration; diff --git a/crates/lox-utils/Cargo.toml b/crates/lox-utils/Cargo.toml index c91fe99..d4804af 100644 --- a/crates/lox-utils/Cargo.toml +++ b/crates/lox-utils/Cargo.toml @@ -7,7 +7,6 @@ rust-version = "1.65" homepage = "https://gitlab.torproject.org/tpo/anti-censorship/lox/-/wikis/home" description = "General helpers used by Lox" keywords = ["tor", "lox"] -# We must put *something* here and this will do categories = ["rust-patterns"] repository = "https://gitlab.torproject.org/tpo/anti-censorship/lox.git/" diff --git a/crates/rdsys-backend-api/Cargo.toml b/crates/rdsys-backend-api/Cargo.toml index 4c562b3..916d282 100644 --- a/crates/rdsys-backend-api/Cargo.toml +++ b/crates/rdsys-backend-api/Cargo.toml @@ -10,7 +10,6 @@ categories = ["api-bindings", "encoding"] repository = "https://gitlab.torproject.org/tpo/anti-censorship/lox/-/tree/main/crates/rdsys-backend-api" readme = "https://gitlab.torproject.org/tpo/anti-censorship/lox/-/blob/main/crates/rdsys-backend-api/README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] serde_json = "1"