Browse Source

Add 'crates/lox-extensions/' from commit '42a38f703674bb3a860dfc9011fe98dce45b8f76'

git-subtree-dir: crates/lox-extensions
git-subtree-mainline: 66283f656f3e4caa6702b8b4526121da775b8892
git-subtree-split: 42a38f703674bb3a860dfc9011fe98dce45b8f76
onyinyang 5 months ago
parent
commit
283f56756a

+ 1 - 0
crates/lox-extensions/.gitignore

@@ -0,0 +1 @@
+/target

+ 1514 - 0
crates/lox-extensions/Cargo.lock

@@ -0,0 +1,1514 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addchain"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2e69442aa5628ea6951fa33e24efe8313f4321a91bd729fc2f75bdfc858570"
+dependencies = [
+ "num-bigint 0.3.3",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cc"
+version = "1.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "cmz"
+version = "0.1.0-rc1"
+dependencies = [
+ "cmz-derive",
+ "ff",
+ "generic_static",
+ "group",
+ "hex",
+ "lazy_static",
+ "rand",
+ "serde",
+ "serde_bytes",
+ "serde_with",
+ "sigma-compiler",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "cmz-core"
+version = "0.1.0-rc1"
+dependencies = [
+ "clap",
+ "darling",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "cmz-derive"
+version = "0.1.0-rc1"
+dependencies = [
+ "cmz-core",
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "group",
+ "rand_core",
+ "rustc_version",
+ "serde",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "serde",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core",
+ "serde",
+ "sha2",
+ "subtle",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "bitvec",
+ "byteorder",
+ "ff_derive",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "ff_derive"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60"
+dependencies = [
+ "addchain",
+ "num-bigint 0.3.3",
+ "num-integer",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "generic_static"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28ccff179d8070317671db09aee6d20affc26e88c5394714553b04f509b43a60"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.5",
+ "serde",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "lox-extensions"
+version = "0.1.0"
+dependencies = [
+ "aes-gcm",
+ "base64 0.21.7",
+ "bincode",
+ "chrono",
+ "cmz",
+ "curve25519-dalek",
+ "ed25519-dalek",
+ "ff",
+ "group",
+ "rand",
+ "serde",
+ "serde_bytes",
+ "serde_with",
+ "sha1",
+ "sha2",
+ "subtle",
+ "thiserror 2.0.16",
+ "time",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "num-bigint"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.11.0",
+ "schemars 0.9.0",
+ "schemars 1.0.4",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "sigma-compiler"
+version = "0.1.0-rc1"
+dependencies = [
+ "group",
+ "rand",
+ "sigma-compiler-derive",
+ "sigma-proofs",
+ "subtle",
+]
+
+[[package]]
+name = "sigma-compiler-core"
+version = "0.1.0-rc1"
+dependencies = [
+ "clap",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "sigma-compiler-derive"
+version = "0.1.0-rc1"
+dependencies = [
+ "sigma-compiler-core",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "sigma-proofs"
+version = "0.1.0-sigma"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fe0d134228911458aa039f90582b9e512b06d193aa8fc460c78135367a18388"
+dependencies = [
+ "ahash",
+ "ff",
+ "group",
+ "hashbrown 0.15.5",
+ "keccak",
+ "num-bigint 0.4.6",
+ "num-traits",
+ "rand",
+ "rand_core",
+ "sha3",
+ "subtle",
+ "thiserror 1.0.69",
+ "zerocopy",
+ "zeroize",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
+dependencies = [
+ "thiserror-impl 2.0.16",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "time"
+version = "0.3.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

+ 35 - 0
crates/lox-extensions/Cargo.toml

@@ -0,0 +1,35 @@
+[package]
+name = "lox-extensions"
+version = "0.1.0"
+authors =  ["Ian Goldberg <iang@uwaterloo.ca>, Lindsey Tulloch <onyinyang@torproject.org>"]
+edition = "2021"
+license = "MIT"
+respository = "https://gitlab.torproject.org/tpo/anti-censorship/lox/creates/lox-extensions"
+description = "A reputation-based bridge distribution system that provides privacy protection to users and their social graph while remaining open to all users."
+keywords = ["tor", "anti-censorship", "bridges", "reputation", "anonymity"]
+categories = ["cryptography"]
+readme = "README.md"
+
+[dependencies]
+aes-gcm = { version = "0.10", features = ["aes"] }
+base64 = "0.21.0"
+chrono = { version = "0.4.38", default-features = false, features = ["now"], optional = true }
+curve25519-dalek = {version = "4.1.3", default-features = false, features = ["serde", "group", "rand_core", "digest", "precomputed-tables"] }
+ed25519-dalek = { version = "2.1.1", default-features = false, features = ["serde", "rand_core"] }
+rand = {version = "0.8.5", features = ["std_rng"] }
+serde = "1.0.217"
+serde_with = { version = "3.0.0", features = ["json"] }
+sha1 = "0.10"
+sha2 = "0.10.9"
+subtle = "2.5"
+cmz = "0.1.0"
+group = "0.13"
+ff = "0.13.1"
+bincode = "1"
+thiserror = "2.0.12"
+time = "0.3.36"
+serde_bytes = "0.11.17"
+
+[features]
+bridgeauth = ["chrono"]
+test = []

+ 46 - 0
crates/lox-extensions/README.md

@@ -0,0 +1,46 @@
+# lox-extensions
+
+Lox is a reputation-based bridge distribution system that provides privacy protection to users and their social graph while remaining open to all users. It is 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)). It is currently being maintained by the [Tor Project](https://www.torproject.org/download/)'s [Anti-censorship team](https://gitlab.torproject.org/tpo/anti-censorship/team).
+
+`lox-extensions` is a rust crate that is a re-implementation and replacement of the [`lox-library`](https://crates.io/crates/lox-library) crate, depending on a different set of cryptographic libraries to implement Lox's anonymous credential based reputation system. `lox-extensions` uses the following `sigma-rs` crates: 
+
+- [`cmz`](https://crates.io/crates/cmz) ([`-core`](https://crates.io/crates/cmz-core) and [`-derive`](https://crates.io/crates/cmz-derive)) together facilitate the creation of the Lox system as a series of [10 protocols](src/proto/) described in compact statements using [5 µCMZ credentials](src/lox_creds.rs). The output of our protocols are fed to the `sigma-compiler` crates.
+
+- [`sigma-compiler`](https://crates.io/crates/sigma-compiler) ([`-derive`](https://crates.io/crates/sigma-compiler-derive)) automatically generate the code for sigma zero-knowledge proof protocols from the more complex statements described at the `cmz` layer. These are given to the `sigma-compiler` layer and compiled into statements about linear combinations of points, that can be consumed by the `sigma-proofs` API.
+
+- [`sigma-proofs`](https://crates.io/crates/sigma-proofs) perform zero-knowledge proofs based on the input from the `sigma-compiler`.
+
+Building and testing this crate, requires `cargo`. You can get `cargo` by [installing Rust](https://www.rust-lang.org/tools/install). We used Rust version 1.89.
+
+All Lox tests can be run with `cargo test --features bridgeauth`
+
+## Notable Changes to adapt Lox for Tor
+
+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, no changes have been made to alter the functionality of 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`](src/bridge_table.rs?ref_type=heads#L51) struct 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
+   [`src/lib.rs`](src/lib.rs?ref_type=heads#L557) 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 [`src/lib.rs`](src/lib.rs?ref_type=heads#L668) 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`.~~ has been replaced by the [`sigma-rs`](https://github.com/sigma-rs) libraries.
+This allows us to express protocols using the `cmz` library and prove things expressed in statements with the `sigma-proofs` library. All of the public/private key pairs are of type `CMZPubKey` and `CMZPrivKey` and our credentials are expressed using the cmz credential macro.
+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).
+8. Key rotation protocols were added to allow users to anonymously update existing [Lox](src/proto/update_cred.rs) and [Invitation](src/proto/update_invite.rs) credentials to use new keys.
+
+### 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 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).
+
+##### Note on the abandonment of `lox-library`:
+It was initially unclear how fundamentally the lox code base would need to change to fit as an extension of the generic anonymous credential stack that we envisioned with `sigma-rs`. As such, development of `lox-extension` started as a complete rewrite of each of the Lox protocols with the relatively minor changes to the Lox authority and other elements of the system coming later. To reflect the development process, and the reframing of Lox as an extension, or layer, of a more generic anonymous credentials stack, and to mitigate the many breaking changes involved, creating a new `lox-extension` crate seemed most appropriate.
+
+##### Note on future changes:
+In terms of functionality, the initial version of this crate is fundamentally unchanged from the `lox-library` which is consistent with the Lox system described
+in [Tulloch and Goldberg](https://petsymposium.org/popets/2023/popets-2023-0029.php). However, this implementation may diverge from the theory over time as the system is deployed and its limitations as a censorship circumvention tool are better illuminated.
+

+ 460 - 0
crates/lox-extensions/src/bridge_table.rs

@@ -0,0 +1,460 @@
+/*! The encrypted table of bridges.
+
+The table consists of a number of buckets, each holding some number
+(currently up to 3) of bridges.  Each bucket is individually encrypted
+with a bucket key.  Users will have a credential containing a bucket
+(number, key) combination, and so will be able to read one of the
+buckets.  Users will either download the whole encrypted bucket list or
+use PIR to download a piece of it, so that the bridge authority does not
+learn which bucket the user has access to. */
+use super::lox_creds::BucketReachability;
+use super::{Scalar, G};
+use aes_gcm::aead;
+use aes_gcm::aead::{generic_array::GenericArray, Aead};
+use aes_gcm::{Aes128Gcm, KeyInit};
+#[cfg(feature = "bridgeauth")]
+#[allow(unused_imports)]
+use base64::{engine::general_purpose, Engine as _};
+use cmz::{CMZCredential, CMZPrivkey, CMZPubkey};
+use curve25519_dalek::ristretto::CompressedRistretto;
+#[allow(unused_imports)]
+use rand::{CryptoRng, RngCore};
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr};
+use sha1::{Digest, Sha1};
+use std::collections::{HashMap, HashSet};
+use subtle::ConstantTimeEq;
+
+/// Each bridge information line is serialized into this many bytes
+pub const BRIDGE_BYTES: usize = 250;
+
+/// The bridge info field is this many bytes long
+pub const BRIDGE_INFO_BYTES: usize = BRIDGE_BYTES - 46;
+
+/// The max number of bridges per bucket
+pub const MAX_BRIDGES_PER_BUCKET: usize = 3;
+
+/// The minimum number of bridges in a bucket that must be reachable for
+/// the bucket to get a Bucket Reachability credential that will allow
+/// users of that bucket to gain trust levels (once they are already at
+/// level 1)
+pub const MIN_BUCKET_REACHABILITY: usize = 2;
+
+/// A bridge information line
+#[serde_as]
+#[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq, Debug)]
+pub struct BridgeLine {
+    /// IPv4 or IPv6 address
+    pub addr: [u8; 16],
+    /// port
+    pub port: u16,
+    /// fingerprint
+    #[serde_as(as = "DisplayFromStr")]
+    pub uid_fingerprint: u64,
+    /// unhashed fingerprint (20-byte bridge ID)
+    pub unhashed_fingerprint: [u8; 20], // may be changed to a string later
+    /// other protocol information, including pluggable transport,
+    /// public key, etc.
+    #[serde_as(as = "[_; BRIDGE_INFO_BYTES]")]
+    pub info: [u8; BRIDGE_INFO_BYTES],
+}
+
+impl BridgeLine {
+    pub fn get_hashed_fingerprint(&self) -> [u8; 20] {
+        let mut hasher = Sha1::new();
+        hasher.update(self.unhashed_fingerprint);
+        // If the fingerprint gets changed to a string:
+        //hasher.update(array_bytes::hex2array(&self.fingerprint).unwrap());
+        hasher.finalize().into()
+    }
+}
+
+/// A bucket contains MAX_BRIDGES_PER_BUCKET bridges plus the
+/// information needed to construct a Bucket Reachability credential,
+/// which is a 4-byte date, and a (P,Q) MAC
+type Bucket = (
+    [BridgeLine; MAX_BRIDGES_PER_BUCKET],
+    Option<BucketReachability>,
+);
+
+/// The size of a plaintext bucket
+pub const BUCKET_BYTES: usize = BRIDGE_BYTES * MAX_BRIDGES_PER_BUCKET + 4 + 32 + 32;
+
+/// The size of an encrypted bucket
+pub const ENC_BUCKET_BYTES: usize = BUCKET_BYTES + 12 + 16;
+
+impl Default for BridgeLine {
+    /// An "empty" BridgeLine is represented by all zeros
+    fn default() -> Self {
+        Self {
+            addr: [0; 16],
+            port: 0,
+            uid_fingerprint: 0,
+            unhashed_fingerprint: [0; 20],
+            info: [0; BRIDGE_INFO_BYTES],
+        }
+    }
+}
+
+impl BridgeLine {
+    /// Encode a BridgeLine to a byte array
+    pub fn encode(&self) -> [u8; BRIDGE_BYTES] {
+        let mut res: [u8; BRIDGE_BYTES] = [0; BRIDGE_BYTES];
+        res[0..16].copy_from_slice(&self.addr);
+        res[16..18].copy_from_slice(&self.port.to_be_bytes());
+        res[18..26].copy_from_slice(&self.uid_fingerprint.to_be_bytes());
+        res[26..46].copy_from_slice(&self.unhashed_fingerprint);
+        res[46..].copy_from_slice(&self.info);
+        res
+    }
+    /// Decode a BridgeLine from a byte array
+    pub fn decode(data: &[u8; BRIDGE_BYTES]) -> Self {
+        let mut res: Self = Default::default();
+        res.addr.copy_from_slice(&data[0..16]);
+        res.port = u16::from_be_bytes(data[16..18].try_into().unwrap());
+        res.uid_fingerprint = u64::from_be_bytes(data[18..26].try_into().unwrap());
+        res.unhashed_fingerprint.copy_from_slice(&data[26..46]);
+        res.info.copy_from_slice(&data[46..]);
+        res
+    }
+    /// Encode a bucket to a byte array, including a Bucket Reachability
+    /// credential if appropriate
+    pub fn bucket_encode(
+        rng: &mut (impl CryptoRng + RngCore),
+        bucket: &[BridgeLine; MAX_BRIDGES_PER_BUCKET],
+        reachable: &HashMap<BridgeLine, Vec<(u32, usize)>>,
+        today: u32,
+        bucket_attr: &Scalar,
+        reachability_priv: &CMZPrivkey<G>,
+    ) -> [u8; BUCKET_BYTES] {
+        let mut res: [u8; BUCKET_BYTES] = [0; BUCKET_BYTES];
+        let mut pos: usize = 0;
+        let mut num_reachable: usize = 0;
+        for bridge in bucket {
+            res[pos..pos + BRIDGE_BYTES].copy_from_slice(&bridge.encode());
+            if reachable.contains_key(bridge) {
+                num_reachable += 1;
+            }
+            pos += BRIDGE_BYTES;
+        }
+        if num_reachable >= MIN_BUCKET_REACHABILITY {
+            // Construct a Bucket Reachability credential for this
+            // bucket and today's date
+            let today_attr: Scalar = today.into();
+            let mut B = BucketReachability::using_privkey(reachability_priv);
+            B.date = Some(today_attr);
+            B.bucket = Some(*bucket_attr);
+            let _ = B.create_MAC(rng, reachability_priv);
+            res[pos..pos + 4].copy_from_slice(&today.to_le_bytes());
+            res[pos + 4..pos + 36].copy_from_slice(B.MAC.P.compress().as_bytes());
+            res[pos + 36..].copy_from_slice(B.MAC.Q.compress().as_bytes());
+        }
+        res
+    }
+    /// Decode a bucket from a byte array, yielding the array of
+    /// BridgeLine entries and an optional Bucket Reachability
+    /// credential
+    fn bucket_decode(
+        data: &[u8; BUCKET_BYTES],
+        bucket_attr: &Scalar,
+        reachability_pub: &CMZPubkey<G>,
+    ) -> Bucket {
+        let mut pos: usize = 0;
+        let mut bridges: [BridgeLine; MAX_BRIDGES_PER_BUCKET] = Default::default();
+        for bridge in bridges.iter_mut().take(MAX_BRIDGES_PER_BUCKET) {
+            *bridge = BridgeLine::decode(data[pos..pos + BRIDGE_BYTES].try_into().unwrap());
+            pos += BRIDGE_BYTES;
+        }
+        // See if there's a nonzero date in the Bucket Reachability
+        // Credential
+        let date = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
+        let (optP, optQ) = if date > 0 {
+            (
+                CompressedRistretto::from_slice(&data[pos + 4..pos + 36])
+                    .expect("Unable to extract P from bucket")
+                    .decompress(),
+                CompressedRistretto::from_slice(&data[pos + 36..])
+                    .expect("Unable to extract Q from bucket")
+                    .decompress(),
+            )
+        } else {
+            (None, None)
+        };
+        if let (Some(P), Some(Q)) = (optP, optQ) {
+            let date_attr: Scalar = date.into();
+            let mut B = BucketReachability::using_pubkey(reachability_pub);
+            B.date = Some(date_attr);
+            B.bucket = Some(*bucket_attr);
+            B.MAC.P = P;
+            B.MAC.Q = Q;
+
+            (bridges, Some(B))
+        } else {
+            (bridges, None)
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+#[serde(try_from = "Vec<u8>", into = "Vec<u8>")]
+pub struct EncryptedBucket([u8; ENC_BUCKET_BYTES]);
+
+impl From<EncryptedBucket> for Vec<u8> {
+    fn from(e: EncryptedBucket) -> Vec<u8> {
+        e.0.into()
+    }
+}
+
+#[derive(thiserror::Error, Debug)]
+#[error("wrong slice length")]
+pub struct WrongSliceLengthError;
+
+impl TryFrom<Vec<u8>> for EncryptedBucket {
+    type Error = WrongSliceLengthError;
+    fn try_from(v: Vec<u8>) -> Result<EncryptedBucket, Self::Error> {
+        Ok(EncryptedBucket(
+            *Box::<[u8; ENC_BUCKET_BYTES]>::try_from(v).map_err(|_| WrongSliceLengthError)?,
+        ))
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct K {
+    encbucket: EncryptedBucket,
+}
+
+/// 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
+/// 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
+    pub counter: u32,
+    /// The keys of all buckets, indexed by counter, that are still part of the bridge table.
+    pub keys: HashMap<u32, [u8; 16]>,
+    /// All buckets, indexed by counter corresponding to the key above, that are
+    /// part of the bridge table.
+    pub buckets: HashMap<u32, [BridgeLine; MAX_BRIDGES_PER_BUCKET]>,
+    pub encbuckets: HashMap<u32, EncryptedBucket>,
+    /// Individual bridges that are reachable.
+    #[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
+    pub reachable: HashMap<BridgeLine, Vec<(u32, usize)>>,
+    /// 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
+    /// removed from this set and used for that purpose.
+    pub spares: HashSet<u32>,
+    /// 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<BridgeLine>,
+    // 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<u32>,
+    // 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.
+    pub blocked_keys: Vec<(u32, u32)>,
+    // 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 at least each day so that the Bucket
+    /// Reachability credentials in the buckets can be refreshed.
+    pub date_last_enc: u32,
+}
+
+// Invariant: the lengths of the keys and bucket hashmap are the same.
+// The encbuckets hashmap only gets updated when encrypt_table is called.
+
+impl BridgeTable {
+    /// Get the number of buckets in the bridge table
+    #[cfg(any(feature = "bridgeauth", test))]
+    pub fn num_buckets(&self) -> usize {
+        self.buckets.len()
+    }
+
+    /// Insert a new bucket into the bridge table, returning its index
+    #[cfg(any(feature = "bridgeauth", test))]
+    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::rngs::OsRng;
+        let mut key: [u8; 16] = [0; 16];
+        rng.fill_bytes(&mut key);
+        self.keys.insert(index, key);
+        self.buckets.insert(index, *bucket);
+        // TODO: maybe we don't need this if the hashtable can keep track of available bridges
+        // Mark the new bridges as available
+        for (i, b) in bucket.iter().enumerate() {
+            if b.port > 0 {
+                if let Some(v) = self.reachable.get_mut(b) {
+                    v.push((index, i));
+                } else {
+                    let v = vec![(index, i)];
+                    self.reachable.insert(*b, v);
+                }
+            }
+        }
+    }
+    /// Create the vector of encrypted buckets from the keys and buckets
+    /// 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
+    /// enough (at least MIN_BUCKET_REACHABILITY) bridges in the bucket
+    /// are reachable.
+    #[cfg(any(feature = "bridgeauth", test))]
+    pub fn encrypt_table(&mut self, today: u32, reachability_priv: &CMZPrivkey<G>) {
+        let rng = &mut rand::thread_rng();
+        self.encbuckets.clear();
+        for (uid, key) in self.keys.iter() {
+            let bucket = self.buckets.get(uid).unwrap();
+            let mut encbucket: [u8; ENC_BUCKET_BYTES] = [0; ENC_BUCKET_BYTES];
+            let plainbucket: [u8; BUCKET_BYTES] = BridgeLine::bucket_encode(
+                rng,
+                bucket,
+                &self.reachable,
+                today,
+                &to_scalar(*uid, key),
+                reachability_priv,
+            );
+            // Set the AES key
+            let aeskey = GenericArray::from_slice(key);
+            // Pick a random nonce
+            let mut noncebytes: [u8; 12] = [0; 12];
+            rng.fill_bytes(&mut noncebytes);
+            let nonce = GenericArray::from_slice(&noncebytes);
+            // Encrypt
+            let cipher = Aes128Gcm::new(aeskey);
+            let ciphertext: Vec<u8> = cipher.encrypt(nonce, plainbucket.as_ref()).unwrap();
+            encbucket[0..12].copy_from_slice(&noncebytes);
+            encbucket[12..].copy_from_slice(ciphertext.as_slice());
+            let k = EncryptedBucket(encbucket);
+            self.encbuckets.insert(*uid, k);
+        }
+        self.date_last_enc = today;
+    }
+
+    /// Decrypt an individual encrypted bucket, given its id, key, and
+    /// the encrypted bucket itself
+    pub fn decrypt_bucket(
+        id: u32,
+        key: &[u8; 16],
+        encbucket: &EncryptedBucket,
+        reachability_pub: &CMZPubkey<G>,
+    ) -> Result<Bucket, aead::Error> {
+        // Set the nonce and the key
+        let k = K {
+            encbucket: *encbucket,
+        };
+        let nonce = GenericArray::from_slice(&k.encbucket.0[0..12]);
+        let aeskey = GenericArray::from_slice(key);
+        // Decrypt
+        let cipher = Aes128Gcm::new(aeskey);
+        let plaintext: Vec<u8> = cipher.decrypt(nonce, k.encbucket.0[12..].as_ref())?;
+        // Convert the plaintext bytes to an array of BridgeLines
+        Ok(BridgeLine::bucket_decode(
+            plaintext.as_slice().try_into().unwrap(),
+            &to_scalar(id, key),
+            reachability_pub,
+        ))
+    }
+
+    /// Decrypt an individual encrypted bucket, given its id and key
+    #[cfg(any(feature = "bridgeauth", test))]
+    pub fn decrypt_bucket_id(
+        &self,
+        id: u32,
+        key: &[u8; 16],
+        reachability_pub: &CMZPubkey<G>,
+    ) -> Result<Bucket, aead::Error> {
+        let encbucket: &EncryptedBucket = match self.encbuckets.get(&id) {
+            Some(encbucket) => encbucket,
+            None => panic!("Provided ID not found"),
+        };
+        BridgeTable::decrypt_bucket(id, key, encbucket, reachability_pub)
+    }
+}
+
+// Unit tests that require access to the testing-only function
+// BridgeLine::random()
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use super::*;
+    use crate::mock_auth::random;
+    use cmz::cmz_group_init;
+    use sha2::Sha512;
+
+    #[test]
+    fn test_bridge_table() -> Result<(), aead::Error> {
+        // Create private keys for the Bucket Reachability credentials
+        let mut rng = rand::thread_rng();
+        cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+        let (reachability_priv, reachability_pub) = BucketReachability::gen_keys(&mut rng, true);
+        // Create an empty bridge table
+        let mut btable: BridgeTable = Default::default();
+        // Make 20 buckets with one random bridge each
+        for _ in 0..20 {
+            let bucket: [BridgeLine; 3] = [random(), Default::default(), Default::default()];
+            btable.counter += 1;
+            btable.new_bucket(btable.counter, &bucket);
+        }
+        // And 20 more with three random bridges each
+        for _ in 0..20 {
+            let bucket: [BridgeLine; 3] = [random(), random(), random()];
+            btable.counter += 1;
+            btable.new_bucket(btable.counter, &bucket);
+        }
+        let today: u32 = time::OffsetDateTime::now_utc()
+            .date()
+            .to_julian_day()
+            .try_into()
+            .unwrap();
+        // Create the encrypted bridge table
+        btable.encrypt_table(today, &reachability_priv);
+        // Try to decrypt a 1-bridge bucket
+        let key7 = btable.keys.get(&7u32).unwrap();
+        let bucket7 = btable.decrypt_bucket_id(7, key7, &reachability_pub)?;
+        println!("bucket 7 = {:?}", bucket7);
+        // Try to decrypt a 3-bridge bucket
+        let key24 = btable.keys.get(&24u32).unwrap();
+        let bucket24 = btable.decrypt_bucket_id(24, key24, &reachability_pub)?;
+        println!("bucket 24 = {:?}", bucket24);
+        // Try to decrypt a bucket with the wrong key
+        let key12 = btable.keys.get(&12u32).unwrap();
+        let res = btable
+            .decrypt_bucket_id(15, key12, &reachability_pub)
+            .unwrap_err();
+        println!("bucket key mismatch = {:?}", res);
+        Ok(())
+    }
+}
+
+/// Convert an id and key to a Scalar attribute
+pub fn to_scalar(id: u32, key: &[u8; 16]) -> Scalar {
+    let mut b: [u8; 32] = [0; 32];
+    // b is a little-endian representation of the Scalar; put the key in
+    // the low 16 bytes, and the id in the next 4 bytes.
+    b[0..16].copy_from_slice(key);
+    b[16..20].copy_from_slice(&id.to_le_bytes());
+    // This cannot fail, since we're only using the low 20 bytes of b
+    Scalar::from_canonical_bytes(b).unwrap()
+}
+
+/// Convert a Scalar attribute to an id and key if possible
+pub fn from_scalar(s: Scalar) -> Result<(u32, [u8; 16]), aead::Error> {
+    // Check that the top 12 bytes of the Scalar are 0
+    let sbytes = s.as_bytes();
+    if sbytes[20..].ct_eq(&[0u8; 12]).unwrap_u8() == 0 {
+        return Err(aead::Error);
+    }
+    let id = u32::from_le_bytes(sbytes[16..20].try_into().unwrap());
+    let mut key: [u8; 16] = [0; 16];
+    key.copy_from_slice(&sbytes[..16]);
+    Ok((id, key))
+}

+ 48 - 0
crates/lox-extensions/src/dup_filter.rs

@@ -0,0 +1,48 @@
+/*! Filter duplicate shows of credentials and open invitations by id
+(which will typically be a Scalar).
+
+This implementation just keeps the table of seen ids in memory, but a
+production one would of course use a disk-backed database. */
+
+use std::collections::HashSet;
+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.
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct DupFilter<IdType: Hash + Eq + Copy + Serialize> {
+    seen_table: HashSet<IdType>,
+}
+
+/// A return type indicating whether the item was fresh (not previously
+/// seen) or previously seen
+#[derive(PartialEq, Eq, Debug)]
+pub enum SeenType {
+    Fresh,
+    Seen,
+}
+
+impl<IdType: Hash + Eq + Copy + Serialize> DupFilter<IdType> {
+    /// Check to see if the id is in the seen table, but do not add it
+    /// to the seen table.  Return Seen if it is already in the table,
+    /// Fresh if not.
+    pub fn check(&self, id: &IdType) -> SeenType {
+        if self.seen_table.contains(id) {
+            SeenType::Seen
+        } else {
+            SeenType::Fresh
+        }
+    }
+
+    /// As atomically as possible, check to see if the id is in the seen
+    /// table, and add it if not.  Return Fresh if it was not already
+    /// in the table, and Seen if it was.
+    pub fn filter(&mut self, id: &IdType) -> SeenType {
+        match self.seen_table.insert(*id) {
+            true => SeenType::Fresh,
+            false => SeenType::Seen,
+        }
+    }
+}

+ 1003 - 0
crates/lox-extensions/src/lib.rs

@@ -0,0 +1,1003 @@
+/*! Implementation of a new style of bridge authority for Tor that
+allows users to invite other users, while protecting the social graph
+from the bridge authority itself.
+
+We use uCMZ credentials (Orr`u, 2024 https://eprint.iacr.org/2024/1552.pdf) which improves issuer efficiency
+over our original CMZ14 credential (GGM version, which is more efficient, but
+makes a stronger security assumption) implementation: "Algebraic MACs and
+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. */
+
+// We want Scalars to be lowercase letters, and Points and credentials
+// to be capital letters
+#![allow(non_snake_case)]
+
+#[cfg(feature = "bridgeauth")]
+use ed25519_dalek::{Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey};
+use subtle::ConstantTimeEq;
+
+#[cfg(feature = "bridgeauth")]
+use chrono::{DateTime, Duration, Utc};
+#[cfg(feature = "bridgeauth")]
+use cmz::*;
+use curve25519_dalek::ristretto::RistrettoPoint as G;
+use group::Group;
+#[cfg(feature = "bridgeauth")]
+use rand::{rngs::OsRng, CryptoRng, Rng, RngCore};
+#[cfg(feature = "bridgeauth")]
+use std::collections::HashMap;
+type Scalar = <G as Group>::Scalar;
+#[cfg(feature = "bridgeauth")]
+use sha2::Sha512;
+
+pub mod bridge_table;
+pub mod dup_filter;
+pub mod lox_creds;
+pub mod migration_table;
+pub mod mock_auth;
+pub mod proto {
+    pub mod blockage_migration;
+    pub mod check_blockage;
+    pub mod errors;
+    pub mod issue_invite;
+    pub mod level_up;
+    pub mod migration;
+    pub mod open_invite;
+    pub mod redeem_invite;
+    pub mod trust_promotion;
+    pub mod update_cred;
+    pub mod update_invite;
+}
+
+#[cfg(feature = "bridgeauth")]
+use bridge_table::{
+    BridgeLine, BridgeTable, EncryptedBucket, MAX_BRIDGES_PER_BUCKET, MIN_BUCKET_REACHABILITY,
+};
+#[cfg(feature = "bridgeauth")]
+use lox_creds::*;
+#[cfg(feature = "bridgeauth")]
+use migration_table::{MigrationTable, MigrationType};
+#[cfg(feature = "bridgeauth")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "bridgeauth")]
+use std::collections::HashSet;
+#[cfg(any(feature = "bridgeauth", test))]
+use thiserror::Error;
+
+// 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
+// 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)]
+#[cfg(feature = "bridgeauth")]
+pub enum ReplaceSuccess {
+    NotFound = 0,
+    NotReplaced = 1,
+    Replaced = 2,
+    Removed = 3,
+}
+
+/// 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)]
+#[cfg(feature = "bridgeauth")]
+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)]
+#[cfg(any(feature = "bridgeauth", test))]
+pub enum OpenInvitationError {
+    #[error("The maximum number of bridges has already been distributed today, please try again tomorrow!")]
+    ExceededMaxBridges,
+
+    #[error("There are no bridges available for open invitations.")]
+    NoBridgesAvailable,
+}
+
+#[derive(Error, Debug)]
+#[cfg(feature = "bridgeauth")]
+pub enum BridgeTableError {
+    #[error("The bucket corresponding to key {0} was not in the bridge table")]
+    MissingBucket(u32),
+}
+
+/// 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
+pub const MAX_DAILY_BRIDGES: u32 = 100;
+
+/// 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)]
+#[cfg(feature = "bridgeauth")]
+pub struct BridgeDb {
+    /// The keypair for signing open invitations
+    keypair: SigningKey,
+    /// The public key for verifying open invitations
+    pub pubkey: VerifyingKey,
+    /// The set of open-invitation buckets
+    openinv_buckets: HashSet<u32>,
+    /// The set of open invitation buckets that have been distributed
+    distributed_buckets: Vec<u32>,
+    #[serde(skip)]
+    today: DateTime<Utc>,
+    pub current_k: u32,
+    pub daily_bridges_distributed: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg(feature = "bridgeauth")]
+pub struct OldKeyStore {
+    //    /// Most recently outdated lox secret and private keys for verifying update_cred credentials
+    priv_key: CMZPrivkey<G>,
+    //    /// The public key for verifying update_cred credentials
+    pub pub_key: CMZPubkey<G>,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[cfg(feature = "bridgeauth")]
+pub struct OldKeys {
+    /// Most recently outdated lox secret and private keys for verifying update_cred credentials
+    lox_keys: Vec<OldKeyStore>,
+    /// Most recently outdated open_invitation VerifyingKey for verifying update_openinv tokens
+    bridgedb_key: Vec<VerifyingKey>,
+    /// Most recently outdated invitation secret and private keys for verifying update_inv credentials
+    invitation_keys: Vec<OldKeyStore>,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[cfg(feature = "bridgeauth")]
+pub struct OldFilters {
+    /// Most recently outdated lox id filter
+    lox_filter: Vec<dup_filter::DupFilter<Scalar>>,
+    /// Most recently outdated open invitation filter
+    openinv_filter: Vec<dup_filter::DupFilter<Scalar>>,
+    /// Most recently outdated invitation filter
+    invitation_filter: Vec<dup_filter::DupFilter<Scalar>>,
+}
+
+/// An open invitation is a [u8; OPENINV_LENGTH] where the first 32
+/// bytes are the serialization of a random Scalar (the invitation id),
+/// the next 4 bytes are a little-endian bucket number, and the last
+/// SIGNATURE_LENGTH bytes are the signature on the first 36 bytes.
+pub const OPENINV_LENGTH: usize = 32 // the length of the random
+                                     // invitation id (a Scalar)
+    + 4 // the length of the u32 for the bucket number
+    + ed25519_dalek::SIGNATURE_LENGTH; // the length of the signature
+
+#[cfg(feature = "bridgeauth")]
+impl Default for BridgeDb {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeDb {
+    /// Create the BridgeDb.
+    pub fn new() -> Self {
+        let mut csprng = OsRng {};
+        let keypair = SigningKey::generate(&mut csprng);
+        let pubkey = keypair.verifying_key();
+        Self {
+            keypair,
+            pubkey,
+            openinv_buckets: Default::default(),
+            distributed_buckets: Default::default(),
+            today: Utc::now(),
+            current_k: 0,
+            daily_bridges_distributed: 0,
+        }
+    }
+
+    pub fn openinv_length(&mut self) -> usize {
+        self.openinv_buckets.len()
+    }
+
+    /// Rotate Open Invitation keys
+    pub fn rotate_open_inv_keys(&mut self) -> VerifyingKey {
+        let mut csprng = OsRng {};
+        self.keypair = SigningKey::generate(&mut csprng);
+        self.pubkey = self.keypair.verifying_key();
+        self.pubkey
+    }
+
+    /// Insert an open-invitation bucket into the set
+    pub fn insert_openinv(&mut self, bucket: u32) {
+        self.openinv_buckets.insert(bucket);
+    }
+
+    /// Remove an open-invitation bucket from the set
+    pub fn remove_openinv(&mut self, bucket: &u32) {
+        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!");
+            self.openinv_buckets.remove(bucket);
+        } else if self.distributed_buckets.contains(bucket) {
+            self.distributed_buckets.retain(|&x| x != *bucket);
+        }
+    }
+
+    /// Mark a bucket as distributed
+    pub fn mark_distributed(&mut self, bucket: u32) {
+        self.distributed_buckets.push(bucket);
+    }
+
+    /// Produce an open invitation such that the next k users, where k is <
+    /// OPENINV_K, will receive the same open invitation bucket
+    /// chosen randomly from the set of open-invitation buckets.
+    pub fn invite(&mut self) -> Result<[u8; OPENINV_LENGTH], OpenInvitationError> {
+        let mut res: [u8; OPENINV_LENGTH] = [0; OPENINV_LENGTH];
+        let mut rng = rand::rngs::OsRng;
+        // Choose a random invitation id (a Scalar) and serialize it
+        let id = Scalar::random(&mut rng);
+        res[0..32].copy_from_slice(&id.to_bytes());
+        let bucket_num: u32;
+        if Utc::now() >= (self.today + Duration::days(1)) {
+            self.today = Utc::now();
+            self.daily_bridges_distributed = 0;
+        }
+        if self.daily_bridges_distributed < MAX_DAILY_BRIDGES {
+            if self.current_k < OPENINV_K && !self.distributed_buckets.is_empty() {
+                bucket_num = *self.distributed_buckets.last().unwrap();
+                self.current_k += 1;
+            } else {
+                if self.openinv_buckets.is_empty() {
+                    return Err(OpenInvitationError::NoBridgesAvailable);
+                }
+                // Choose a random bucket number (from the set of open
+                // invitation buckets) and serialize it
+                let openinv_vec: Vec<&u32> = self.openinv_buckets.iter().collect();
+                bucket_num = *openinv_vec[rng.gen_range(0..openinv_vec.len())];
+                self.mark_distributed(bucket_num);
+                self.remove_openinv(&bucket_num);
+                self.current_k = 1;
+                self.daily_bridges_distributed += 1;
+            }
+            res[32..(32 + 4)].copy_from_slice(&bucket_num.to_le_bytes());
+            // Sign the first 36 bytes and serialize it
+            let sig = self.keypair.sign(&res[0..(32 + 4)]);
+            res[(32 + 4)..].copy_from_slice(&sig.to_bytes());
+            Ok(res)
+        } else {
+            Err(OpenInvitationError::ExceededMaxBridges)
+        }
+    }
+
+    /// 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(
+        invitation: [u8; OPENINV_LENGTH],
+        pubkey: VerifyingKey,
+    ) -> Result<(Scalar, u32), SignatureError> {
+        // 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 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());
+        if s.is_some().into() {
+            Ok((s.unwrap(), bucket))
+        } else {
+            // It should never happen that there's a valid signature on
+            // an invalid serialization of a Scalar, but check anyway.
+            Err(SignatureError::new())
+        }
+    }
+}
+
+/// The bridge authority. This will typically be a singleton object.
+#[cfg(feature = "bridgeauth")]
+#[derive(Default, Debug, Serialize, Deserialize)]
+pub struct BridgeAuth {
+    /// The private key for the main Lox credential
+    lox_priv: CMZPrivkey<G>,
+    /// The public key for the main Lox credential
+    pub lox_pub: CMZPubkey<G>,
+    /// The private key for migration credentials
+    migration_priv: CMZPrivkey<G>,
+    /// The public key for migration credentials
+    pub migration_pub: CMZPubkey<G>,
+    /// The private key for migration key credentials
+    migrationkey_priv: CMZPrivkey<G>,
+    /// The public key for migration key credentials
+    pub migrationkey_pub: CMZPubkey<G>,
+    /// The private key for bucket reachability credentials
+    reachability_priv: CMZPrivkey<G>,
+    /// The public key for bucket reachability credentials
+    pub reachability_pub: CMZPubkey<G>,
+    /// The private key for invitation credentials
+    invitation_priv: CMZPrivkey<G>,
+    /// The public key for invitation credentials
+    pub invitation_pub: CMZPubkey<G>,
+
+    /// The public key of the BridgeDb issuing open invitations
+    pub bridgedb_pub: VerifyingKey,
+
+    /// The bridge table
+    bridge_table: BridgeTable,
+
+    // Map of bridge fingerprint to values needed to verify TP reports
+    //pub tp_bridge_infos: HashMap<String, BridgeVerificationInfo>,
+    /// The migration tables
+    trustup_migration_table: MigrationTable,
+    blockage_migration_table: MigrationTable,
+
+    /// Duplicate filter for open invitations
+    bridgedb_pub_filter: dup_filter::DupFilter<Scalar>,
+    /// Duplicate filter for Lox credential ids
+    id_filter: dup_filter::DupFilter<Scalar>,
+    /// Duplicate filter for Invitation credential ids
+    inv_id_filter: dup_filter::DupFilter<Scalar>,
+    /// Duplicate filter for trust promotions (from untrusted level 0 to
+    /// trusted level 1)
+    trust_promotion_filter: dup_filter::DupFilter<Scalar>,
+    // Outdated Lox Keys to be populated with the old Lox private and public keys
+    // after a key rotation
+    old_keys: OldKeys,
+    old_filters: OldFilters,
+
+    /// For testing only: offset of the true time to the simulated time
+    #[serde(skip)]
+    time_offset: time::Duration,
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn new(bridgedb_pub: VerifyingKey, rng: &mut (impl CryptoRng + RngCore)) -> Self {
+        // Initialization
+        cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+        // Create the private and public keys for each of the types of
+        // credential with 'true' to indicate uCMZ
+        let (lox_priv, lox_pub) = Lox::gen_keys(rng, true);
+        let (migration_priv, migration_pub) = Migration::gen_keys(rng, true);
+        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);
+        Self {
+            lox_priv,
+            lox_pub,
+            migration_priv,
+            migration_pub,
+            migrationkey_priv,
+            migrationkey_pub,
+            reachability_priv,
+            reachability_pub,
+            invitation_priv,
+            invitation_pub,
+            bridgedb_pub,
+            bridge_table: Default::default(),
+            //            tp_bridge_infos: HashMap::<String, BridgeVerificationInfo>::new(),
+            trustup_migration_table: MigrationTable::new(MigrationType::TrustUpgrade),
+            blockage_migration_table: MigrationTable::new(MigrationType::Blockage),
+            bridgedb_pub_filter: Default::default(),
+            id_filter: Default::default(),
+            inv_id_filter: Default::default(),
+            trust_promotion_filter: Default::default(),
+            time_offset: time::Duration::ZERO,
+            old_keys: Default::default(),
+            old_filters: Default::default(),
+        }
+    }
+
+    pub fn rotate_lox_keys(&mut self, rng: &mut (impl CryptoRng + RngCore)) {
+        let (updated_lox_priv, updated_lox_pub) = Lox::gen_keys(rng, true);
+        // Store the old keys until the next key rotation (this should happen no more than 511 days after the
+        // last rotation to ensure that all credentials issued with the old key can be updated
+        self.old_keys.lox_keys.push(OldKeyStore {
+            priv_key: self.lox_priv.clone(),
+            pub_key: self.lox_pub.clone(),
+        });
+        // Move the old lox id filter to the old_lox_id_filter
+        self.old_filters.lox_filter.push(self.id_filter.clone());
+        // TODO: Commit to the new keys and post the commitment somewhere public that can be verified
+        // by users, ideally
+        self.lox_priv = updated_lox_priv;
+        self.lox_pub = updated_lox_pub;
+        self.id_filter = Default::default();
+    }
+
+    pub fn rotate_invitation_keys(&mut self, rng: &mut (impl CryptoRng + RngCore)) {
+        let (updated_invitation_priv, updated_invitation_pub) = Invitation::gen_keys(rng, true);
+        // Store the old keys until the next key rotation (this should happen no more than 511 days after the
+        // last rotation to ensure that all credentials issued with the old key can be updated
+        self.old_keys.invitation_keys.push(OldKeyStore {
+            priv_key: self.invitation_priv.clone(),
+            pub_key: self.invitation_pub.clone(),
+        });
+        // Move the old invitation id filter to the old_invitation_id_filter
+        self.old_filters
+            .invitation_filter
+            .push(self.inv_id_filter.clone());
+        // TODO: Commit to the new keys and post the commitment somewhere public that can be verified
+        // by users, ideally
+        self.invitation_priv = updated_invitation_priv;
+        self.invitation_pub = updated_invitation_pub;
+        self.inv_id_filter = Default::default();
+    }
+
+    pub fn rotate_bridgedb_keys(&mut self, new_bridgedb_pub: VerifyingKey) {
+        // Store the old verifying key until the next key rotation (this should happen no more often than the
+        // we would reasonably expect a user to redeem an open invitation token to ensure that all invitations
+        // issued with the old key can be updated)
+        self.old_keys.bridgedb_key.push(self.bridgedb_pub);
+        // Move the old lox id filter to the old_lox_id_filter
+        self.old_filters
+            .openinv_filter
+            .push(self.bridgedb_pub_filter.clone());
+        // TODO: Commit to the new keys and post the commitment somewhere public that can be verified
+        // by users, ideally
+        self.bridgedb_pub = new_bridgedb_pub;
+        self.bridgedb_pub_filter = Default::default();
+    }
+
+    /// 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
+    /// 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(
+        &mut self,
+        bridges: [BridgeLine; MAX_BRIDGES_PER_BUCKET],
+        bdb: &mut BridgeDb,
+    ) -> Result<(), NoAvailableIDError> {
+        let bindex = self.find_next_available_key(bdb)?;
+        self.bridge_table.new_bucket(bindex, &bridges);
+        let mut single = [BridgeLine::default(); MAX_BRIDGES_PER_BUCKET];
+        for b in bridges.iter() {
+            let sindex = self.find_next_available_key(bdb)?;
+            single[0] = *b;
+            self.bridge_table.new_bucket(sindex, &single);
+            self.bridge_table.open_inv_keys.push((sindex, self.today()));
+            bdb.insert_openinv(sindex);
+            self.trustup_migration_table.table.insert(sindex, bindex);
+        }
+        Ok(())
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.bridge_table.buckets.is_empty()
+    }
+
+    pub fn reachable_length(&self) -> usize {
+        self.bridge_table.reachable.len()
+    }
+
+    pub fn unallocated_length(&self) -> usize {
+        self.bridge_table.unallocated_bridges.len()
+    }
+
+    pub fn spares_length(&self) -> usize {
+        self.bridge_table.spares.len()
+    }
+
+    pub fn openinv_length(&self, bdb: &mut BridgeDb) -> usize {
+        bdb.openinv_length()
+    }
+
+    /// Insert a hot spare bucket of bridges
+    pub fn add_spare_bucket(
+        &mut self,
+        bucket: [BridgeLine; MAX_BRIDGES_PER_BUCKET],
+        bdb: &mut BridgeDb,
+    ) -> Result<(), NoAvailableIDError> {
+        let index = self.find_next_available_key(bdb)?;
+        self.bridge_table.new_bucket(index, &bucket);
+        self.bridge_table.spares.insert(index);
+        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<u64>,
+    ) -> Vec<BridgeLine> {
+        let mut unaccounted_for: Vec<BridgeLine> = Vec::new();
+        for (k, _v) in self.bridge_table.reachable.clone() {
+            if !accounted_for_bridges.contains(&k.uid_fingerprint) {
+                unaccounted_for.push(k);
+            }
+        }
+        unaccounted_for
+    }
+
+    /// Allocate single left over bridges to an open invitation bucket
+    pub fn allocate_bridges(
+        &mut self,
+        distributor_bridges: &mut Vec<BridgeLine>,
+        bdb: &mut BridgeDb,
+    ) {
+        while let Some(bridge) = distributor_bridges.pop() {
+            self.bridge_table.unallocated_bridges.push(bridge);
+        }
+        while self.bridge_table.unallocated_bridges.len() >= MAX_BRIDGES_PER_BUCKET {
+            let mut bucket = [BridgeLine::default(); MAX_BRIDGES_PER_BUCKET];
+            for bridge in bucket.iter_mut() {
+                *bridge = self.bridge_table.unallocated_bridges.pop().unwrap();
+            }
+            match self.add_openinv_bridges(bucket, bdb) {
+                Ok(_) => continue,
+                Err(e) => {
+                    println!("Error: {:?}", e);
+                    for bridge in bucket {
+                        self.bridge_table.unallocated_bridges.push(bridge);
+                    }
+                }
+            }
+        }
+    }
+
+    // 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. 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
+        let reachable_bridges = self.bridge_table.reachable.clone();
+        for reachable_bridge in reachable_bridges {
+            if reachable_bridge.0.uid_fingerprint == bridge.uid_fingerprint {
+                // Now we must remove the old bridge from the table and insert the new bridge in its place
+                // i.e., in the same bucket and with the same permissions.
+                let positions = self.bridge_table.reachable.get(&reachable_bridge.0);
+                if let Some(v) = positions {
+                    for (bucketnum, offset) in v.iter() {
+                        let mut bridgelines = match self.bridge_table.buckets.get(bucketnum) {
+                            Some(bridgelines) => *bridgelines,
+                            None => return res,
+                        };
+                        assert!(bridgelines[*offset] == reachable_bridge.0);
+                        bridgelines[*offset] = *bridge;
+                        self.bridge_table.buckets.insert(*bucketnum, bridgelines);
+                        /*                   #[cfg(feature = "blockage-detection")]
+                        let (fingerprint_str, bucket) =
+                            self.get_tp_bucket_and_fingerprint(bridge, bucketnum);
+                        // Add bucket to existing entry or add new entry
+                        #[cfg(feature = "blockage-detection")]
+                        match self.tp_bridge_infos.get_mut(&fingerprint_str) {
+                            Some(info) => {
+                                info.buckets.insert(bucket);
+                            }
+                            None => {
+                                let mut buckets = HashSet::<Scalar>::new();
+                                buckets.insert(bucket);
+                                self.tp_bridge_infos.insert(
+                                    fingerprint_str,
+                                    BridgeVerificationInfo {
+                                        bridge_line: *bridge,
+                                        buckets,
+                                    },
+                                );
+                            }
+                        }; */
+                        if !self.bridge_table.buckets.contains_key(bucketnum) {
+                            return res;
+                        }
+                    }
+                    res = true;
+                } else {
+                    return res;
+                }
+                // We must also remove the old bridge from the reachable bridges table
+                // and add the new bridge
+                self.bridge_table.reachable.remove(&reachable_bridge.0);
+                self.bridge_table
+                    .reachable
+                    .insert(*bridge, reachable_bridge.1);
+                return res;
+            }
+        }
+        // Also check the unallocated bridges just in case there is a bridge that should be updated there
+        let unallocated_bridges = self.bridge_table.unallocated_bridges.clone();
+        for (i, unallocated_bridge) in unallocated_bridges.iter().enumerate() {
+            if unallocated_bridge.uid_fingerprint == bridge.uid_fingerprint {
+                // Now we must remove the old bridge from the unallocated bridges and insert the new bridge
+                // in its place
+                self.bridge_table.unallocated_bridges.remove(i);
+                self.bridge_table.unallocated_bridges.push(*bridge);
+                res = true;
+            }
+        }
+        // If this is returned, we assume that the bridge wasn't found in the bridge table
+        // and therefore should be treated as a "new bridge"
+        res
+    }
+
+    // Repurpose a bucket of spares into unallocated bridges
+    pub fn dissolve_spare_bucket(&mut self, key: u32) -> Result<(), BridgeTableError> {
+        self.bridge_table.spares.remove(&key);
+        // Get the actual bridges from the spare bucket
+        let spare_bucket = self
+            .bridge_table
+            .buckets
+            .remove(&key)
+            .ok_or(BridgeTableError::MissingBucket(key))?;
+        for bridge in spare_bucket.iter() {
+            self.bridge_table.unallocated_bridges.push(*bridge);
+            // Mark bucket as unreachable while it is unallocated
+            self.bridge_table.reachable.remove(bridge);
+        }
+        self.bridge_table.keys.remove(&key);
+        self.bridge_table.recycleable_keys.push(key);
+        Ok(())
+    }
+
+    // Removes an unallocated bridge and returns it if it was present
+    pub fn remove_unallocated(&mut self, bridge: &BridgeLine) -> Option<BridgeLine> {
+        //    #[cfg(feature = "blockage-detection")]
+        //    let fingerprint_str = self.fingerprint_hasher(bridge.unhashed_fingerprint);
+        match self
+            .bridge_table
+            .unallocated_bridges
+            .iter()
+            .position(|x| x == bridge)
+        {
+            Some(index) => Some({
+                //          #[cfg(feature = "blockage-detection")]
+                //        self.tp_bridge_infos.remove_entry(&fingerprint_str);
+                self.bridge_table.unallocated_bridges.swap_remove(index)
+            }),
+            None => None,
+        }
+    }
+
+    /// 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,
+        available_bridge: Option<BridgeLine>,
+    ) -> ReplaceSuccess {
+        let reachable_bridges = &self.bridge_table.reachable.clone();
+        let Some(positions) = reachable_bridges.get(bridge) else {
+            match self.remove_unallocated(bridge) {
+                Some(_) => {
+                    return ReplaceSuccess::Removed;
+                }
+                None => {
+                    return ReplaceSuccess::NotFound;
+                }
+            }
+        };
+        // Check if the bridge is in a spare bucket first, if it is, dissolve the bucket
+        if let Some(spare) = self
+            .bridge_table
+            .spares
+            .iter()
+            .find(|x| positions.iter().any(|(bucketnum, _)| &bucketnum == x))
+            .cloned()
+        {
+            let Ok(_) = self.dissolve_spare_bucket(spare) else {
+                return ReplaceSuccess::NotReplaced;
+            };
+            // Next Check if the bridge is in the unallocated bridges and remove the bridge if so
+            // Bridges in spare buckets should have been moved to the unallocated bridges
+            match self.remove_unallocated(bridge) {
+                Some(_) => {
+                    return ReplaceSuccess::Removed;
+                }
+                None => {
+                    return ReplaceSuccess::NotFound;
+                }
+            }
+        }
+        // select replacement:
+        //   - first try the given bridge
+        //   - second try to pick one from the set of available bridges
+        //   - third dissolve a spare bucket to create more available bridges
+        let Some(replacement) = available_bridge.or_else(|| {
+            self.bridge_table.unallocated_bridges.pop().or_else(|| {
+                let spare = self
+                    .bridge_table
+                    .spares
+                    .iter()
+                    // in case bridge is a spare, avoid replacing it with itself
+                    .find(|x| !positions.iter().any(|(bucketnum, _)| &bucketnum == x))
+                    .cloned()?;
+                let Ok(_) = self.dissolve_spare_bucket(spare) else {
+                    return None;
+                };
+                self.bridge_table.unallocated_bridges.pop()
+            })
+        }) else {
+            // If there are no available bridges that can be assigned here, the only thing
+            // that can be done is return an indication that updating the gone bridge
+            // didn't work.
+            // In this case, we do not mark the bridge as unreachable or remove it from the
+            // reachable bridges so that we can still find it when a new bridge does become available
+            println!("No available bridges");
+            return ReplaceSuccess::NotReplaced;
+        };
+        for (bucketnum, offset) in positions.iter() {
+            let mut bridgelines = match self.bridge_table.buckets.get(bucketnum) {
+                Some(bridgelines) => *bridgelines,
+                None => return ReplaceSuccess::NotFound,
+            };
+            assert!(bridgelines[*offset] == *bridge);
+            bridgelines[*offset] = replacement;
+            self.bridge_table.buckets.insert(*bucketnum, bridgelines);
+            // Remove the bridge from the reachable bridges and add new bridge
+            self.bridge_table
+                .reachable
+                .insert(replacement, positions.clone());
+            // Remove the bridge from the bucket
+            self.bridge_table.reachable.remove(bridge);
+        }
+        ReplaceSuccess::Replaced
+    }
+
+    /*    pub fn get_bridge_verification_info(
+            &mut self,
+            bridge_str: &String,
+        ) -> Option<&BridgeVerificationInfo> {
+            self.tp_bridge_infos.get(bridge_str)
+        }
+
+        // Remove Bridge Info for blocked bridge and return the bridgeline with the given fingerprint
+        pub fn block_by_string(&mut self, bridge_str: &String) -> Option<BridgeLine> {
+            if let Some(bridge_verification_info) = self.tp_bridge_infos.remove(bridge_str) {
+                return Some(bridge_verification_info.bridge_line);
+            }
+            None
+        }
+    */
+
+    /// Mark a bridge as blocked
+    ///
+    /// This bridge will be removed from each of the buckets that
+    /// 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
+    /// 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
+    /// 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
+    /// there was none available.
+    pub fn bridge_blocked(&mut self, bridge: &BridgeLine, bdb: &mut BridgeDb) -> bool {
+        let mut res: bool = true;
+        if self.remove_unallocated(bridge).is_some() {
+            return true;
+        }
+        if let Some(positions) = self.bridge_table.reachable.get(bridge) {
+            for (bucketnum, offset) in positions.iter() {
+                // Count how many bridges in this bucket are reachable
+                let mut bucket = match self.bridge_table.buckets.get(bucketnum) {
+                    Some(bridgelines) => *bridgelines,
+                    None => return false, // This should not happen
+                };
+                // Remove the bridge from the bucket
+                assert!(bucket[*offset] == *bridge);
+                bucket[*offset] = BridgeLine::default();
+
+                // If this is an open invitation bucket, there is only one bridge, remove bucket
+                if bdb.openinv_buckets.contains(bucketnum)
+                    || bdb.distributed_buckets.contains(bucketnum)
+                {
+                    bdb.remove_blocked_or_expired_buckets(bucketnum);
+                    self.trustup_migration_table.table.remove(bucketnum);
+                    continue;
+                }
+
+                // If this bucket still has an acceptable number of bridges, continue
+                let numreachable = bucket
+                    .iter()
+                    .filter(|br| self.bridge_table.reachable.contains_key(br))
+                    .count();
+                if numreachable != MIN_BUCKET_REACHABILITY {
+                    // No
+                    continue;
+                }
+
+                // Remove any trust upgrade migrations to this bucket
+                self.trustup_migration_table
+                    .table
+                    .retain(|_, &mut v| v != *bucketnum);
+
+                // If there are no spares, delete blockage migrations leading to this bucket
+                if self.bridge_table.spares.is_empty() {
+                    res = false;
+                    self.blockage_migration_table
+                        .table
+                        .retain(|_, &mut v| v != *bucketnum);
+                    continue;
+                }
+                // Get the first spare and remove it from the spares
+                // set.
+                let spare = *self.bridge_table.spares.iter().next().unwrap();
+                self.bridge_table.spares.remove(&spare);
+                self.bridge_table
+                    .blocked_keys
+                    .push((*bucketnum, self.today()));
+                // Add a blockage migration from this bucket to the spare
+                self.blockage_migration_table
+                    .table
+                    .insert(*bucketnum, spare);
+                // Change any blockage migrations with this bucket
+                // as the destination to the spare
+                for (_, v) in self.blockage_migration_table.table.iter_mut() {
+                    if *v == *bucketnum {
+                        *v = spare;
+                    }
+                }
+            }
+        }
+        self.bridge_table.reachable.remove(bridge);
+
+        res
+    }
+
+    // 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
+    // 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<u32, NoAvailableIDError> {
+        self.clean_up_expired_buckets(bdb);
+        if self.bridge_table.recycleable_keys.is_empty() {
+            let mut test_index = 1;
+            let mut test_counter = self.bridge_table.counter.wrapping_add(test_index);
+            let mut i = 0;
+            while self.bridge_table.buckets.contains_key(&test_counter) && i < 5000 {
+                test_index += 1;
+                test_counter = self.bridge_table.counter.wrapping_add(test_index);
+                i += 1;
+                if i == 5000 {
+                    return Err(NoAvailableIDError::ExhaustedIndexer);
+                }
+            }
+            self.bridge_table.counter = self.bridge_table.counter.wrapping_add(test_index);
+            Ok(self.bridge_table.counter)
+        } else {
+            Ok(self.bridge_table.recycleable_keys.pop().unwrap())
+        }
+    }
+
+    // This function looks for and removes buckets so their indexes can be reused
+    // This should include buckets that have been blocked for a sufficiently long period
+    // that we no longer want to allow migration to, or else, open-entry buckets that
+    // have been unblocked long enough to become trusted and who's users' credentials
+    // would have expired (after EXPIRY_DATE)
+    pub fn clean_up_expired_buckets(&mut self, bdb: &mut BridgeDb) {
+        // First check if there are any blocked indexes that are old enough to be replaced
+        self.clean_up_blocked();
+        // Next do the same for open_invitations buckets
+        self.clean_up_open_entry(bdb);
+    }
+
+    /// Cleans up exipred blocked buckets
+    fn clean_up_blocked(&mut self) {
+        // If there are expired blockages, separate them from the fresh blockages
+        #[allow(clippy::type_complexity)]
+        let (expired, fresh): (Vec<(u32, u32)>, Vec<(u32, u32)>) = self
+            .bridge_table
+            .blocked_keys
+            .iter()
+            .partition(|&x| x.1 + EXPIRY_DATE < self.today());
+        for item in expired {
+            let key = item.0;
+            // check each single bridge line and ensure none are still marked as reachable.
+            // if any are still reachable, remove from reachable bridges.
+            // When syncing resources, we will likely have to reallocate this bridge but if it hasn't already been
+            // blocked, this might be fine?
+            let bridgelines = self.bridge_table.buckets.get(&key).unwrap();
+            for bridgeline in bridgelines {
+                // If the bridge hasn't been set to default, assume it's still reachable
+                if bridgeline.port > 0 {
+                    // Move to unallocated bridges
+                    self.bridge_table.unallocated_bridges.push(*bridgeline);
+                    // Make sure bridge is removed from reachable bridges
+                    self.bridge_table.reachable.remove(bridgeline);
+                }
+            }
+            // Then remove the bucket and keys at the specified index
+            self.bridge_table.buckets.remove(&key);
+            self.bridge_table.keys.remove(&key);
+            //and add them to the recyclable keys
+            self.bridge_table.recycleable_keys.push(key);
+            // Remove the expired blocked bucket from the blockage migration table,
+            // assuming that anyone that has still not attempted to migrate from their
+            // blocked bridge after the EXPIRY_DATE probably doesn't still need to migrate.
+            self.blockage_migration_table.table.retain(|&k, _| k != key);
+        }
+        // Finally, update the blocked_keys vector to only include the fresh keys
+        self.bridge_table.blocked_keys = fresh
+    }
+
+    /// Cleans up expired open invitation buckets
+    fn clean_up_open_entry(&mut self, bdb: &mut BridgeDb) {
+        // Separate exipred from fresh open invitation indexes
+        #[allow(clippy::type_complexity)]
+        let (expired, fresh): (Vec<(u32, u32)>, Vec<(u32, u32)>) = self
+            .bridge_table
+            .open_inv_keys
+            .iter()
+            .partition(|&x| x.1 + EXPIRY_DATE < self.today());
+        for item in expired {
+            let key = item.0;
+            // We should check that the items were actually distributed before they are removed
+            if !bdb.distributed_buckets.contains(&key) {
+                // TODO: Add prometheus metric for this?
+                println!("This bucket was not actually distributed!");
+            }
+            bdb.remove_blocked_or_expired_buckets(&key);
+            // Remove any trust upgrade migrations from this
+            // bucket
+            self.trustup_migration_table.table.retain(|&k, _| k != key);
+            self.bridge_table.buckets.remove(&key);
+            self.bridge_table.keys.remove(&key);
+            //and add them to the recyclable keys
+            self.bridge_table.recycleable_keys.push(key);
+        }
+        // update the open_inv_keys vector to only include the fresh keys
+        self.bridge_table.open_inv_keys = fresh
+    }
+
+    /// 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
+        (time::OffsetDateTime::now_utc().date() + self.time_offset)
+            .to_julian_day()
+            .try_into()
+            .unwrap()
+    }
+
+    /// Get today's (real or simulated) date as a DateTime<Utc> value
+    pub fn today_date(&self) -> DateTime<Utc> {
+        Utc::now()
+    }
+
+    /// Get a reference to the encrypted bridge table.
+    ///
+    /// Be sure to call this function when you want the latest version
+    /// of the table, since it will put fresh Bucket Reachability
+    /// credentials in the buckets each day.
+    pub fn enc_bridge_table(&mut self) -> &HashMap<u32, EncryptedBucket> {
+        let today = self.today();
+        if self.bridge_table.date_last_enc != today {
+            self.bridge_table
+                .encrypt_table(today, &self.reachability_priv);
+        }
+        &self.bridge_table.encbuckets
+    }
+
+    // For testing only: manually advance the day by the given number
+    // of days
+    #[cfg(feature = "test")]
+    pub fn advance_days(&mut self, days: u16) {
+        self.time_offset += time::Duration::days(days.into());
+    }
+}
+
+pub fn scalar_u32(s: &Scalar) -> Option<u32> {
+    // Check that the top 28 bytes of the Scalar are 0
+    let sbytes: &[u8; 32] = s.as_bytes();
+    if sbytes[4..].ct_eq(&[0u8; 28]).unwrap_u8() == 0 {
+        return None;
+    }
+    Some(u32::from_le_bytes(sbytes[..4].try_into().unwrap()))
+}

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

@@ -0,0 +1,88 @@
+// The various credentials used by the system.
+
+use cmz::*;
+use curve25519_dalek::ristretto::RistrettoPoint as G;
+use group::{ff, Group};
+use rand::RngCore;
+
+// 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
+// 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
+// been blocked).
+// Annotated to "M"
+CMZ! { Migration:
+    lox_id,
+    from_bucket,
+    to_bucket,
+    migration_type
+}
+
+// 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
+// Julian date of when this user was changed to the current trust
+// level.
+// Annotated to "L"
+CMZ! { Lox:
+    id,
+    bucket,
+    trust_level,
+    level_since,
+    invites_remaining,
+    blockages
+}
+
+// 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
+// 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
+// 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
+// credential with their new bucket, without the BA learning either
+// their old or new buckets.
+// Annotated to "K"
+CMZ! { MigrationKey:
+    lox_id,
+    from_bucket
+}
+
+// The Bucket Reachability credential.
+//
+// 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
+// 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.
+// Annotated to "B"
+CMZ! { BucketReachability:
+    date,
+    bucket
+}
+
+// The Invitation credential.
+//
+// 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_
+// 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.
+// Annotated to "I"
+CMZ! { Invitation:
+    inv_id,
+    date,
+    bucket,
+    blockages
+}

+ 280 - 0
crates/lox-extensions/src/migration_table.rs

@@ -0,0 +1,280 @@
+/*! The migration table.
+
+This is a table listing pairs of (from_bucket_id, to_bucket_id).  A pair
+in this table indicates that a user with a Lox credential containing
+from_bucket_id (and possibly meeting other conditions as well) is
+entitled to exchange their credential for one with to_bucket_id.  (Note
+that the credentials contain the bucket attributes, which include both
+the id and the bucket decryption key, but the table just contains the
+bucket ids.) */
+
+use aes_gcm::aead::{generic_array::GenericArray, Aead};
+use aes_gcm::{Aes128Gcm, KeyInit};
+use cmz::*;
+use curve25519_dalek::ristretto::CompressedRistretto;
+use ff::PrimeField;
+use group::{WnafBase, WnafScalar};
+use rand::RngCore;
+use serde_with::serde_as;
+use sha2::Digest;
+use sha2::Sha256;
+
+use std::collections::HashMap;
+
+#[cfg(feature = "bridgeauth")]
+use super::bridge_table;
+use super::lox_creds::{Migration, MigrationKey};
+use super::{Scalar, G};
+use serde::{Deserialize, Serialize};
+
+pub const WNAF_SIZE: usize = 6;
+
+/// Each (plaintext) entry in the returned migration table is serialized
+/// into this many bytes
+pub const MIGRATION_BYTES: usize = 96;
+
+/// The size of an encrypted entry in the returned migration table
+pub const ENC_MIGRATION_BYTES: usize = MIGRATION_BYTES + 12 + 16;
+
+/// A table of encrypted Migration credentials; the encryption keys
+/// are formed from the possible values of Qk (the decrypted form of
+/// EncQk)
+#[serde_as]
+#[derive(Deserialize, Serialize)]
+pub struct EncMigrationTable {
+    #[serde_as(as = "Vec<(_,[_; ENC_MIGRATION_BYTES])>")]
+    pub mig_table: HashMap<[u8; 16], [u8; ENC_MIGRATION_BYTES]>,
+}
+
+/// The type of migration table: TrustUpgrade is for migrations from
+/// untrusted (level 0) 1-bridge buckets to trusted (level 1) 3-bridge
+/// buckets.  Blockage is for migrations that drop you down two levels
+/// (level 3 to 1, level 4 to 2) because the bridges in your current
+/// bucket were blocked.
+pub enum MigrationType {
+    TrustUpgrade,
+    Blockage,
+}
+
+impl From<MigrationType> for u128 {
+    /// Convert a MigrationType into the Scalar value that represents
+    /// it in the Migration credential
+    fn from(m: MigrationType) -> Self {
+        match m {
+            MigrationType::TrustUpgrade => 0u32,
+            MigrationType::Blockage => 1u32,
+        }
+        .into()
+    }
+}
+
+/// The migration table
+#[derive(Default, Debug, Serialize, Deserialize)]
+//#[cfg(feature = "bridgeauth")]
+pub struct MigrationTable {
+    pub table: HashMap<u32, u32>,
+    pub migration_type: Scalar,
+}
+
+/// Create an encrypted Migration credential for returning to the user
+/// in the trust promotion protocol.
+///
+/// Given the attributes of a Migration credential, produce a serialized
+/// version (containing only the to_bucket and the MAC, since the
+/// receiver will already know the id and from_bucket), encrypted with
+/// H2(id, from_bucket, Qk), for the Qk portion of the MAC on the
+/// corresponding Migration Key credential (with fixed Pk, given as a
+/// precomputed multiplication table).  Return the label H1(id,
+/// from_attr_i, Qk_i) and the encrypted Migration credential.  H1 and
+/// H2 are the first 16 bytes and the second 16 bytes respectively of
+/// the SHA256 hash of the input.
+//#[cfg(feature = "bridgeauth")]
+pub fn encrypt_cred(
+    id: Scalar,
+    from_bucket: Scalar,
+    to_bucket: Scalar,
+    migration_type: Scalar,
+    Pktable: &WnafBase<G, WNAF_SIZE>,
+    migration_priv: &CMZPrivkey<G>,
+    migrationkey_priv: &CMZPrivkey<G>,
+) -> ([u8; 16], [u8; ENC_MIGRATION_BYTES]) {
+    let mut rng = rand::thread_rng();
+
+    // Compute the Migration Key credential MAC Qk
+    // This is done automatically but how do we use the same Pktable?
+    let mut K = MigrationKey::using_privkey(migrationkey_priv);
+    K.lox_id = Some(id);
+    K.from_bucket = Some(from_bucket);
+    let coeff: Scalar = K.compute_MAC_coeff(migrationkey_priv).unwrap();
+    K.MAC.Q = Pktable * &WnafScalar::new(&coeff);
+
+    // Compute a MAC (P, Q) on the Migration credential
+    let mut M = Migration::using_privkey(migration_priv);
+    M.lox_id = Some(id);
+    M.from_bucket = Some(from_bucket);
+    M.to_bucket = Some(to_bucket);
+    M.migration_type = Some(migration_type);
+    let _ = M.create_MAC(&mut rng, migration_priv);
+
+    // Serialize (to_bucket, P, Q)
+    let mut credbytes: [u8; MIGRATION_BYTES] = [0; MIGRATION_BYTES];
+    credbytes[0..32].copy_from_slice(to_bucket.as_bytes());
+    credbytes[32..64].copy_from_slice(M.MAC.P.compress().as_bytes());
+    credbytes[64..].copy_from_slice(M.MAC.Q.compress().as_bytes());
+
+    // Pick a random nonce
+    let mut noncebytes: [u8; 12] = [0; 12];
+    rng.fill_bytes(&mut noncebytes);
+    let nonce = GenericArray::from_slice(&noncebytes);
+
+    // Compute the hash of (id, from_bucket, Qk)
+    let mut hasher = Sha256::new();
+    hasher.update(id.as_bytes());
+    hasher.update(from_bucket.as_bytes());
+    hasher.update(K.MAC.Q.compress().as_bytes());
+    let fullhash = hasher.finalize();
+
+    // Create the encryption key from the 2nd half of the hash
+    let aeskey = GenericArray::from_slice(&fullhash[16..]);
+    // Encrypt
+    let cipher = Aes128Gcm::new(aeskey);
+    let ciphertext: Vec<u8> = cipher.encrypt(nonce, credbytes.as_ref()).unwrap();
+    let mut enccredbytes: [u8; ENC_MIGRATION_BYTES] = [0; ENC_MIGRATION_BYTES];
+    enccredbytes[..12].copy_from_slice(&noncebytes);
+    enccredbytes[12..].copy_from_slice(ciphertext.as_slice());
+
+    // Use the first half of the above hash as the label
+    let mut label: [u8; 16] = [0; 16];
+    label[..].copy_from_slice(&fullhash[..16]);
+
+    (label, enccredbytes)
+}
+
+/// Create an encrypted Migration credential for returning to the user
+/// in the trust promotion protocol, given the ids of the from and to
+/// buckets, and the migration type, and using a BridgeTable to get the
+/// bucket keys.
+///
+/// Otherwise the same as encrypt_cred, above, except it returns an
+/// Option in case the passed ids were invalid.
+#[cfg(feature = "bridgeauth")]
+#[allow(clippy::too_many_arguments)]
+pub fn encrypt_cred_ids(
+    id: Scalar,
+    from_id: u32,
+    to_id: u32,
+    migration_type: Scalar,
+    bridgetable: &bridge_table::BridgeTable,
+    Pktable: &WnafBase<G, WNAF_SIZE>,
+    migration_priv: &CMZPrivkey<G>,
+    migrationkey_priv: &CMZPrivkey<G>,
+) -> Option<([u8; 16], [u8; ENC_MIGRATION_BYTES])> {
+    // Look up the bucket keys and form the attributes (Scalars)
+    let fromkey = bridgetable.keys.get(&from_id)?;
+    let tokey = bridgetable.keys.get(&to_id)?;
+    Some(encrypt_cred(
+        id,
+        bridge_table::to_scalar(from_id, fromkey),
+        bridge_table::to_scalar(to_id, tokey),
+        migration_type,
+        Pktable,
+        migration_priv,
+        migrationkey_priv,
+    ))
+}
+
+#[cfg(feature = "bridgeauth")]
+impl MigrationTable {
+    /// Create a MigrationTable of the given MigrationType
+    pub fn new(table_type: MigrationType) -> Self {
+        Self {
+            table: Default::default(),
+            migration_type: Scalar::from_u128(table_type.into()),
+        }
+    }
+
+    /// For each entry in the MigrationTable, use encrypt_cred_ids to
+    /// produce an entry in an output HashMap (from labels to encrypted
+    /// Migration credentials).
+    pub fn encrypt_table(
+        &self,
+        id: Scalar,
+        bridgetable: &bridge_table::BridgeTable,
+        Pktable: &WnafBase<G, WNAF_SIZE>,
+        migration_priv: &CMZPrivkey<G>,
+        migrationkey_priv: &CMZPrivkey<G>,
+    ) -> HashMap<[u8; 16], [u8; ENC_MIGRATION_BYTES]> {
+        self.table
+            .iter()
+            .filter_map(|(from_id, to_id)| {
+                encrypt_cred_ids(
+                    id,
+                    *from_id,
+                    *to_id,
+                    self.migration_type,
+                    bridgetable,
+                    Pktable,
+                    migration_priv,
+                    migrationkey_priv,
+                )
+            })
+            .collect()
+    }
+}
+
+/// Decrypt an encrypted Migration credential given Qk, the known
+/// attributes id and from_bucket for the Migration credential as well
+/// as the known migration type, and a HashMap mapping labels to
+/// ciphertexts.
+pub fn decrypt_cred(
+    mk_cred: MigrationKey,
+    migration_type: MigrationType,
+    migration_pubkey: CMZPubkey<G>,
+    enc_migration_table: &HashMap<[u8; 16], [u8; ENC_MIGRATION_BYTES]>,
+) -> Option<Migration> {
+    // Compute the hash of (id, from_bucket, Qk)
+    let mut hasher = Sha256::new();
+    hasher.update(mk_cred.lox_id.unwrap().as_bytes());
+    hasher.update(mk_cred.from_bucket.unwrap().as_bytes());
+    hasher.update(mk_cred.MAC.Q.compress().as_bytes());
+    let fullhash = hasher.finalize();
+
+    // Use the first half of the above hash as the label
+    let mut label: [u8; 16] = [0; 16];
+    label[..].copy_from_slice(&fullhash[..16]);
+
+    // Look up the label in the HashMap
+    let ciphertext = enc_migration_table.get(&label)?;
+
+    // Create the decryption key from the 2nd half of the hash
+    let aeskey = GenericArray::from_slice(&fullhash[16..]);
+
+    // Decrypt
+    let nonce = GenericArray::from_slice(&ciphertext[..12]);
+    let cipher = Aes128Gcm::new(aeskey);
+    let plaintext: Vec<u8> = match cipher.decrypt(nonce, ciphertext[12..].as_ref()) {
+        Ok(v) => v,
+        Err(_) => return None,
+    };
+    let plaintextbytes = plaintext.as_slice();
+    let mut to_bucket_bytes: [u8; 32] = [0; 32];
+    to_bucket_bytes.copy_from_slice(&plaintextbytes[..32]);
+    let to_bucket = Scalar::from_bytes_mod_order(to_bucket_bytes);
+    let rng = &mut rand::thread_rng();
+    let r = Scalar::random(rng);
+    let P = r * CompressedRistretto::from_slice(&plaintextbytes[32..64])
+        .expect("Unable to extract P from bucket")
+        .decompress()?;
+    let Q = r * CompressedRistretto::from_slice(&plaintextbytes[64..])
+        .expect("Unable to extract Q from bucket")
+        .decompress()?;
+
+    let mut M = Migration::using_pubkey(&migration_pubkey);
+    M.MAC.P = P;
+    M.MAC.Q = Q;
+    M.lox_id = mk_cred.lox_id;
+    M.from_bucket = mk_cred.from_bucket;
+    M.to_bucket = Some(to_bucket);
+    M.migration_type = Some(Scalar::from_u128(migration_type.into()));
+    Some(M)
+}

+ 324 - 0
crates/lox-extensions/src/mock_auth.rs

@@ -0,0 +1,324 @@
+#[cfg(all(test, feature = "bridgeauth"))]
+use super::proto::{
+    blockage_migration, check_blockage, issue_invite,
+    level_up::{self, LEVEL_INTERVAL},
+    migration, open_invite,
+    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};
+
+#[cfg(all(test, feature = "bridgeauth"))]
+use rand::RngCore;
+
+#[cfg(all(test, feature = "bridgeauth"))]
+#[allow(unused_imports)]
+use base64::{engine::general_purpose, Engine as _};
+
+#[derive(Default)]
+#[cfg(all(test, feature = "bridgeauth"))]
+pub struct TestHarness {
+    pub bdb: BridgeDb,
+    pub ba: BridgeAuth,
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+impl TestHarness {
+    pub fn new() -> Self {
+        TestHarness::new_buckets(5, 5)
+    }
+
+    pub fn new_buckets(num_buckets: u16, hot_spare: u16) -> Self {
+        // Create a BridegDb
+        let mut bdb = BridgeDb::new();
+        let mut rng = rand::thread_rng();
+        // Create a BridgeAuth
+        let mut ba = BridgeAuth::new(bdb.pubkey.clone(), &mut rng);
+
+        // Make 3 x num_buckets open invitation bridges, in sets of 3
+        for _ in 0..num_buckets {
+            let bucket = [random(), random(), random()];
+            let _ = ba.add_openinv_bridges(bucket, &mut bdb);
+        }
+        // Add hot_spare more hot spare buckets
+        for _ in 0..hot_spare {
+            let bucket = [random(), random(), random()];
+            let _ = ba.add_spare_bucket(bucket, &mut bdb);
+        }
+        // Create the encrypted bridge table
+        ba.enc_bridge_table();
+
+        Self { bdb, ba }
+    }
+
+    pub fn advance_days(&mut self, days: u32) {
+        if days > 0 {
+            self.ba.time_offset += time::Duration::days(days.into());
+        }
+    }
+
+    /// Verify the two MACs on a Lox credential
+    pub fn verify_lox(&self, cred: &lox_creds::Lox) {
+        assert!(
+            cred.verify_MAC(&self.ba.lox_priv).is_ok(),
+            "Lox cred's MAC should verify"
+        );
+    }
+
+    /// Verify the MAC on a Migration credential
+    pub fn verify_migration(&self, cred: &lox_creds::Migration) {
+        assert!(
+            cred.verify_MAC(&self.ba.migration_priv).is_ok(),
+            "Migration cred's MAC should verify"
+        );
+    }
+
+    /// Verify the MAC on a Bucket Reachability credential
+    pub fn verify_reachability(&self, cred: &lox_creds::BucketReachability) {
+        assert!(
+            cred.verify_MAC(&self.ba.reachability_priv).is_ok(),
+            "Reachability cred's MAC should verify"
+        );
+    }
+
+    /// Verify the MAC on a Invitation credential
+    pub fn verify_invitation(&mut self, cred: &lox_creds::Invitation) {
+        assert!(
+            cred.verify_MAC(&self.ba.invitation_priv).is_ok(),
+            "Invitation cred's MAC should verify"
+        );
+    }
+
+    pub fn open_invite(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+        invite: &[u8; OPENINV_LENGTH],
+    ) -> Lox {
+        let open_invitation_request = open_invite::request(rng, self.ba.lox_pub.clone());
+        assert!(
+            open_invitation_request.is_ok(),
+            "Open invitation request should succeed"
+        );
+        let (request, client_state) = open_invitation_request.unwrap();
+        let open_invitation_response = self.ba.open_invitation(request, invite);
+        assert!(
+            open_invitation_response.is_ok(),
+            "Open invitation response from server should succeed"
+        );
+        let (response, _) = open_invitation_response.unwrap();
+        let lox_cred = open_invite::handle_response(client_state, response);
+        assert!(lox_cred.is_ok(), "Handle response should succeed");
+        lox_cred.unwrap()
+    }
+
+    pub fn trust_promotion(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+        cred: Lox,
+    ) -> Migration {
+        self.advance_days((UNTRUSTED_INTERVAL + 1).try_into().unwrap());
+        let trust_promo_request =
+            trust_promotion::request(rng, cred, self.ba.migrationkey_pub.clone(), self.ba.today());
+        assert!(
+            trust_promo_request.is_ok(),
+            "Trust Promotion request should succeed"
+        );
+        let (tp_request, tp_client_state) = trust_promo_request.unwrap();
+        let trust_promo_response = self.ba.handle_trust_promotion(tp_request);
+        assert!(
+            trust_promo_response.is_ok(),
+            "Trust promotion response from server should succeed"
+        );
+        let (response, enc) = trust_promo_response.unwrap();
+        let mig_cred = trust_promotion::handle_response(
+            self.ba.migration_pub.clone(),
+            tp_client_state,
+            response,
+            enc,
+        );
+        assert!(mig_cred.is_ok(), "Handle response should succeed");
+        mig_cred.unwrap()
+    }
+
+    pub fn migration(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+        cred: Lox,
+        mig_cred: Migration,
+    ) -> Lox {
+        let migration_request = migration::request(rng, cred, mig_cred);
+        assert!(
+            migration_request.is_ok(),
+            "Migration request should succeed"
+        );
+        let (mig_request, mig_client_state) = migration_request.unwrap();
+        let migration_response = self.ba.handle_migration(mig_request);
+        assert!(
+            migration_response.is_ok(),
+            "Migration response from server should succeed"
+        );
+        let response = migration_response.unwrap();
+        let new_cred = migration::handle_response(mig_client_state, response);
+        assert!(new_cred.is_ok(), "Handle response should succeed");
+        new_cred.unwrap()
+    }
+
+    fn reach_cred(&mut self, cred: Lox) -> BucketReachability {
+        let (id, key) = bridge_table::from_scalar(cred.bucket.unwrap()).unwrap();
+        let encbuckets = self.ba.enc_bridge_table().clone();
+        let reach_pub = self.ba.reachability_pub.clone();
+        let bucket = bridge_table::BridgeTable::decrypt_bucket(
+            id,
+            &key,
+            encbuckets.get(&id).unwrap(),
+            &reach_pub,
+        )
+        .unwrap();
+        bucket.1.unwrap()
+    }
+
+    pub fn level_up(&mut self, rng: &mut (impl CryptoRng + RngCore), cred: Lox) -> Lox {
+        let trust_level: u32 = scalar_u32(&cred.trust_level.unwrap()).unwrap();
+        self.advance_days(LEVEL_INTERVAL[trust_level as usize] + 1);
+        let reachcred = self.reach_cred(cred.clone());
+        let level_up_request = level_up::request(rng, cred.clone(), reachcred, self.ba.today());
+        assert!(level_up_request.is_ok(), "Level up request should succeed");
+        let (level_up_request, level_up_client_state) = level_up_request.unwrap();
+        let level_up_response = self.ba.handle_level_up(level_up_request);
+        assert!(
+            level_up_response.is_ok(),
+            "Level up response from server should succeed"
+        );
+        let response = level_up_response.unwrap();
+        let new_cred = level_up::handle_response(level_up_client_state, response);
+        assert!(new_cred.is_ok(), "Handle response should succeed");
+        new_cred.unwrap()
+    }
+
+    pub fn issue_invite(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+        cred: Lox,
+    ) -> (Invitation, Lox) {
+        let reachcred = self.reach_cred(cred.clone());
+        let issue_invite_request = issue_invite::request(
+            rng,
+            cred.clone(),
+            reachcred,
+            self.ba.invitation_pub.clone(),
+            self.ba.today(),
+        );
+        let (issue_invite_request, issue_invite_client_state) = issue_invite_request.unwrap();
+        let issue_invite_response = self.ba.handle_issue_invite(issue_invite_request);
+        assert!(
+            issue_invite_response.is_ok(),
+            "Issue Invite response from server should succeed"
+        );
+        let response = issue_invite_response.unwrap();
+        let i_cred = issue_invite::handle_response(issue_invite_client_state, response);
+        assert!(i_cred.is_ok(), "Handle response should succeed");
+        i_cred.unwrap()
+    }
+
+    pub fn block_bridges(&mut self, cred: Lox) {
+        // Get our bridges
+        let (id, key) = bridge_table::from_scalar(cred.bucket.unwrap()).unwrap();
+        let encbuckets = self.ba.enc_bridge_table().clone();
+        let bucket = bridge_table::BridgeTable::decrypt_bucket(
+            id,
+            &key,
+            encbuckets.get(&id).unwrap(),
+            &self.ba.reachability_pub.clone(),
+        )
+        .unwrap();
+        // We should have a Bridge Reachability credential
+        assert!(bucket.1.is_some());
+        // Oh, no!  Two of our bridges are blocked!
+        self.ba.bridge_blocked(&bucket.0[0], &mut self.bdb);
+        self.ba.bridge_blocked(&bucket.0[2], &mut self.bdb);
+        self.advance_days(1);
+    }
+
+    pub fn check_blockage(&mut self, rng: &mut (impl CryptoRng + RngCore), cred: Lox) -> Migration {
+        let check_blockage_request =
+            check_blockage::request(rng, cred, self.ba.migrationkey_pub.clone());
+        assert!(
+            check_blockage_request.is_ok(),
+            "Check blockage request should succeed"
+        );
+        let (check_blockage_request, check_blockage_client_state) = check_blockage_request.unwrap();
+        let check_blockage_response = self.ba.handle_check_blockage(check_blockage_request);
+        assert!(
+            check_blockage_response.is_ok(),
+            "Check blockage response from server should succeed"
+        );
+        let (response, enc) = check_blockage_response.unwrap();
+        let mig_cred = check_blockage::handle_response(
+            self.ba.migration_pub.clone(),
+            check_blockage_client_state,
+            response,
+            enc,
+        );
+        assert!(mig_cred.is_ok(), "Handle response should succeed");
+        mig_cred.unwrap()
+    }
+
+    pub fn blockage_migration(
+        &mut self,
+        rng: &mut (impl CryptoRng + RngCore),
+        cred: Lox,
+        mig_cred: Migration,
+    ) -> Lox {
+        let migration_request = blockage_migration::request(rng, cred, mig_cred);
+        assert!(
+            migration_request.is_ok(),
+            "Migration request should succeed"
+        );
+        let (mig_request, mig_client_state) = migration_request.unwrap();
+        let migration_response = self.ba.handle_blockage_migration(mig_request);
+        assert!(
+            migration_response.is_ok(),
+            "Migration response from server should succeed"
+        );
+        let response = migration_response.unwrap();
+        let new_cred = blockage_migration::handle_response(mig_client_state, response);
+        assert!(new_cred.is_ok(), "Handle response should succeed");
+        new_cred.unwrap()
+    }
+}
+
+/// Create a random BridgeLine for testing
+#[cfg(all(test, feature = "bridgeauth"))]
+pub fn random() -> BridgeLine {
+    let mut rng = rand::rngs::OsRng;
+    let mut res: BridgeLine = Default::default();
+    // Pick a random 4-byte address
+    let mut addr: [u8; 4] = [0; 4];
+    rng.fill_bytes(&mut addr);
+    // If the leading byte is 224 or more, that's not a valid IPv4
+    // address.  Choose an IPv6 address instead (but don't worry too
+    // much about it being well formed).
+    if addr[0] >= 224 {
+        rng.fill_bytes(&mut res.addr);
+    } else {
+        // Store an IPv4 address as a v4-mapped IPv6 address
+        res.addr[10] = 255;
+        res.addr[11] = 255;
+        res.addr[12..16].copy_from_slice(&addr);
+    };
+    let ports: [u16; 4] = [443, 4433, 8080, 43079];
+    let portidx = (rng.next_u32() % 4) as usize;
+    res.port = ports[portidx];
+    res.uid_fingerprint = rng.next_u64();
+    let mut cert: [u8; 52] = [0; 52];
+    rng.fill_bytes(&mut cert);
+    let infostr: String = format!(
+        "obfs4 cert={}, iat-mode=0",
+        general_purpose::STANDARD_NO_PAD.encode(cert)
+    );
+    res.info[..infostr.len()].copy_from_slice(infostr.as_bytes());
+    res
+}

+ 198 - 0
crates/lox-extensions/src/proto/blockage_migration.rs

@@ -0,0 +1,198 @@
+/*! A module for the protocol for the user of trust level 3 or higher to
+migrate from one bucket to another because their current bucket has been
+blocked.  Their trust level will go down by 2.
+
+The user presents their current Lox credential:
+
+- id: revealed
+- bucket: hidden
+- trust_level: revealed to be 3 or higher
+- level_since: hidden
+- invites_remaining: hidden
+- blockages: hidden
+
+and a Migration credential:
+
+- id: revealed as the same as the Lox credential id above
+- from_bucket: hidden, but proved in ZK that it's the same as the
+  bucket in the Lox credential above
+- to_bucket: hidden
+
+and a new Lox credential to be issued:
+
+- id: jointly chosen by the user and BA
+- bucket: hidden, but proved in ZK that it's the same as the to_bucket
+  in the Migration credential above
+- trust_level: revealed to be 2 less than the trust_level above
+- level_since: set by the server to today
+- invites_remaining: implicit to both the client and server as LEVEL_INVITATIONS for the new trust level
+- blockages: hidden, but proved in ZK that it's one more than the
+  blockages above
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, Scalar, G};
+use super::check_blockage::MIN_TRUST_LEVEL;
+use super::errors::CredentialError;
+use super::level_up::LEVEL_INVITATIONS;
+#[cfg(feature = "bridgeauth")]
+use super::level_up::MAX_LEVEL;
+pub use crate::lox_creds::{Lox, Migration};
+#[cfg(feature = "bridgeauth")]
+use crate::migration_table::MigrationType;
+use cmz::*;
+#[cfg(feature = "bridgeauth")]
+use ff::PrimeField;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"blockage_migration";
+
+muCMZProtocol! { blockage_migration,
+    [ L: Lox { id: R, bucket: H, trust_level: R, level_since: H, invites_remaining: H, blockages: H },
+    M: Migration { lox_id: R, from_bucket: H, to_bucket: H, migration_type: I } ],
+    N: Lox {id: J, bucket: H, trust_level: I, level_since: S, invites_remaining: I, blockages: H },
+    L.id = M.lox_id,
+    L.bucket = M.from_bucket,
+    N.bucket = M.to_bucket,
+    N.blockages = L.blockages + 1,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    M: Migration,
+) -> Result<(blockage_migration::Request, blockage_migration::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    // Ensure that the credenials can be correctly shown; that is, the
+    // ids match and the Lox credential bucket matches the Migration
+    // credential from_bucket
+    if L.id.is_some_and(|i| i != M.lox_id.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
+    if L.bucket.is_some_and(|b| b != M.from_bucket.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
+    // The trust level must be at least MIN_TRUST_LEVEL
+    let level: u32 = match scalar_u32(&L.trust_level.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("trust_level"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if level < MIN_TRUST_LEVEL {
+        return Err(CredentialError::InvalidField(
+            String::from("trust_level"),
+            format!("level {:?} not in range", level),
+        ));
+    }
+
+    let mut N = Lox::using_pubkey(L.get_pubkey());
+    N.bucket = M.to_bucket;
+    N.trust_level = Some((level - 2).into());
+    // The invites remaining is the appropriate number for the new
+    // level (note that LEVEL_INVITATIONS[i] is the number of
+    // invitations for moving from level i to level i+1)
+    N.invites_remaining = Some(LEVEL_INVITATIONS[(level - 3) as usize].into());
+    N.blockages = Some(L.blockages.unwrap() + Scalar::ONE);
+
+    match blockage_migration::prepare(rng, SESSION_ID, &L, &M, N) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_blockage_migration(
+        &mut self,
+        req: blockage_migration::Request,
+    ) -> Result<blockage_migration::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = blockage_migration::Request::try_from(&reqbytes[..]).unwrap();
+
+        let today = self.today();
+        match blockage_migration::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, M: &mut Migration, N: &mut Lox| {
+                let level: u32 = match scalar_u32(&L.trust_level.unwrap()) {
+                    Some(v) if v >= MIN_TRUST_LEVEL && v as usize <= MAX_LEVEL => v,
+                    _ => {
+                        return Err(CMZError::RevealAttrMissing(
+                            "level",
+                            "Could not be converted to u32 or value not in range",
+                        ));
+                    }
+                };
+                if L.id.is_some_and(|b| b != M.lox_id.unwrap()) {
+                    return Err(CMZError::IssProofFailed);
+                }
+                L.set_privkey(&self.lox_priv);
+                M.set_privkey(&self.migration_priv);
+                N.set_privkey(&self.lox_priv);
+                M.migration_type = Some(Scalar::from_u128(MigrationType::Blockage.into()));
+                N.trust_level = Some((level - 2).into());
+                N.level_since = Some(today.into());
+                N.invites_remaining = Some(LEVEL_INVITATIONS[(level - 3) as usize].into());
+                Ok(())
+            },
+            |L: &Lox, _M: &Migration, _N: &Lox| {
+                if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", ""));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_L_issuer, _M_isser, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: blockage_migration::ClientState,
+    rep: blockage_migration::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = blockage_migration::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_blockage_migration() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mut mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        let lox_cred_1 = th.level_up(rng, lox_cred.clone());
+        let lox_cred_2 = th.level_up(rng, lox_cred_1.clone());
+        let lox_cred_3 = th.level_up(rng, lox_cred_2.clone());
+        th.block_bridges(lox_cred_3.clone());
+        mig_cred = th.check_blockage(rng, lox_cred_3.clone());
+        lox_cred = th.blockage_migration(rng, lox_cred_3.clone(), mig_cred.clone());
+        th.verify_lox(&lox_cred);
+    }
+}

+ 211 - 0
crates/lox-extensions/src/proto/check_blockage.rs

@@ -0,0 +1,211 @@
+/*! A module for the protocol for the user to check for the availability
+of a migration credential they can use in order to move to a new bucket
+if theirs has been blocked.
+
+The user presents their current Lox credential:
+- id: revealed
+- bucket: blinded
+- trust_level: revealed to be 3 or above
+- level_since: blinded
+- invites_remaining: blinded
+- blockages: blinded
+
+They are allowed to to this as long as they are level 3 or above.  If
+they have too many blockages (but are level 3 or above), they will be
+allowed to perform this migration, but will not be able to advance to
+level 3 in their new bucket, so this will be their last allowed
+migration without rejoining the system either with a new invitation or
+an open invitation.
+
+They will receive in return the encrypted MAC (Pk, EncQk) for their
+implicit Migration Key credential with attributes id and bucket,
+along with a HashMap of encrypted Migration credentials.  For each
+(from_i, to_i) in the BA's migration list, there will be an entry in
+the HashMap with key H1(id, from_attr_i, Qk_i) and value
+Enc_{H2(id, from_attr_i, Qk_i)}(to_attr_i, P_i, Q_i).  Here H1 and H2
+are the first 16 bytes and the second 16 bytes respectively of the
+SHA256 hash of the input, P_i and Q_i are a MAC on the Migration
+credential with attributes id, from_attr_i, and to_attr_i. Qk_i is the
+value EncQk would decrypt to if bucket were equal to from_attr_i. */
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, G};
+use super::errors::CredentialError;
+use super::level_up::MAX_LEVEL;
+use crate::lox_creds::{Lox, Migration, MigrationKey};
+#[cfg(feature = "bridgeauth")]
+use crate::migration_table::WNAF_SIZE;
+use crate::migration_table::{self, EncMigrationTable};
+use cmz::*;
+use group::Group;
+#[cfg(feature = "bridgeauth")]
+use group::WnafBase;
+use rand::{CryptoRng, RngCore};
+use serde_with::serde_as;
+use sha2::Sha512;
+
+/// The minimum trust level a Lox credential must have to be allowed to
+/// perform this protocol.
+pub const MIN_TRUST_LEVEL: u32 = 3;
+const SESSION_ID: &[u8] = b"check_blockage";
+
+muCMZProtocol! { check_blockage,
+    L: Lox { id: R, bucket: H, trust_level: R, level_since: H, invites_remaining: H, blockages: H },
+    M: MigrationKey { lox_id: R, from_bucket: H},
+    M.lox_id = L.id,
+    M.from_bucket = L.bucket,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    migkey_pubkeys: CMZPubkey<G>,
+) -> Result<(check_blockage::Request, check_blockage::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    // Ensure that the credentials can be correctly shown; that is, the
+    // ids match and the Lox credential bucket matches the Migration
+    // credential from_bucket
+    if L.id.is_none() {
+        return Err(CredentialError::CredentialMismatch);
+    }
+    // Ensure the credential can be correctly shown: it must be the case
+    // that trust_level >= MIN_TRUST_LEVEL
+    if let Some(tl) = L.trust_level {
+        let level: u32 = match scalar_u32(&tl) {
+            Some(v) => v,
+            None => {
+                return Err(CredentialError::InvalidField(
+                    String::from("trust_level"),
+                    String::from("could not be converted to u32"),
+                ))
+            }
+        };
+        if !(MIN_TRUST_LEVEL..=MAX_LEVEL as u32).contains(&level) {
+            return Err(CredentialError::InvalidField(
+                String::from("trust_level"),
+                format!("level {:?} not in range", level),
+            ));
+        }
+    }
+    let mut M = MigrationKey::using_pubkey(&migkey_pubkeys);
+    M.lox_id = L.id;
+    M.from_bucket = L.bucket;
+
+    match check_blockage::prepare(rng, SESSION_ID, &L, M) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[allow(clippy::type_complexity)]
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_check_blockage(
+        &mut self,
+        req: check_blockage::Request,
+    ) -> Result<(check_blockage::Reply, EncMigrationTable), CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = check_blockage::Request::try_from(&reqbytes[..]).unwrap();
+
+        match check_blockage::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, M: &mut MigrationKey| {
+                // Ensure the credential can be correctly shown: it must be the case
+                // that trust_level >= MIN_TRUST_LEVEL
+                if let Some(tl) = L.trust_level {
+                    let level: u32 = match scalar_u32(&tl) {
+                        Some(v) => v,
+                        None => {
+                            return Err(CMZError::RevealAttrMissing(
+                                "trust_level",
+                                "could not be converted to u32",
+                            ))
+                        }
+                    };
+                    if !(MIN_TRUST_LEVEL..=MAX_LEVEL as u32).contains(&level) {
+                        return Err(CMZError::RevealAttrMissing(
+                            "trust_level",
+                            "level not in range",
+                        ));
+                    }
+                };
+                L.set_privkey(&self.lox_priv);
+                M.set_privkey(&self.migrationkey_priv);
+                Ok(())
+            },
+            |L: &Lox, _M: &MigrationKey| {
+                if self.id_filter.check(&L.id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (L_issuer, M_issuer))) => {
+                let Pktable: WnafBase<G, WNAF_SIZE> = WnafBase::new(M_issuer.MAC.P);
+                let enc_migration_table = EncMigrationTable {
+                    mig_table: self.blockage_migration_table.encrypt_table(
+                        L_issuer.id.unwrap(),
+                        &self.bridge_table,
+                        &Pktable,
+                        &self.migration_priv,
+                        &self.migrationkey_priv,
+                    ),
+                };
+                Ok((response, enc_migration_table))
+            }
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    migration_pubkey: CMZPubkey<G>,
+    state: check_blockage::ClientState,
+    rep: check_blockage::Reply,
+    enc_migration_table: EncMigrationTable,
+) -> Result<Migration, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = check_blockage::Reply::try_from(&replybytes[..]).unwrap();
+    let migkey = match state.finalize(recvreply) {
+        Ok(cred) => cred,
+        Err(_e) => return Err(CMZError::Unknown),
+    };
+
+    match migration_table::decrypt_cred(
+        migkey,
+        migration_table::MigrationType::Blockage,
+        migration_pubkey,
+        &enc_migration_table.mig_table,
+    ) {
+        Some(cred) => Ok(cred),
+        None => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_check_blockage() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mut mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        let lox_cred_1 = th.level_up(rng, lox_cred.clone());
+        let lox_cred_2 = th.level_up(rng, lox_cred_1.clone());
+        let lox_cred_3 = th.level_up(rng, lox_cred_2.clone());
+        th.block_bridges(lox_cred_3.clone());
+        mig_cred = th.check_blockage(rng, lox_cred_3.clone());
+        th.verify_migration(&mig_cred);
+    }
+}

+ 21 - 0
crates/lox-extensions/src/proto/errors.rs

@@ -0,0 +1,21 @@
+use thiserror::Error;
+
+/// 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 CredentialError {
+    #[error("time threshold for operation will not be met for {0} more days")]
+    TimeThresholdNotMet(u32),
+    #[error("credential has expired")]
+    CredentialExpired,
+    #[error("invalid field {0}: {1}")]
+    InvalidField(String, String),
+    #[error("exceeded blockages threshold")]
+    ExceededBlockagesThreshold,
+    #[error("credential has no available invitations")]
+    NoInvitationsRemaining,
+    #[error("supplied credentials do not match")]
+    CredentialMismatch,
+    #[error("CMZ Error")]
+    CMZError(cmz::CMZError),
+}

+ 211 - 0
crates/lox-extensions/src/proto/issue_invite.rs

@@ -0,0 +1,211 @@
+/*! A module for the protocol for a user to request the issuing of an
+Invitation credential they can pass to someone they know.
+
+They are allowed to do this as long as their current Lox credentials has
+a non-zero "invites_remaining" attribute (which will be decreased by
+one), and they have a Bucket Reachability credential for their current
+bucket and today's date.  (Such credentials are placed daily in the
+encrypted bridge table.)
+
+The user presents their current Lox credential:
+- id: revealed
+- bucket: hidden
+- trust_level: hidden
+- level_since: hidden
+- invites_remaining: hidden, but proved in ZK that it's not zero
+- blockages: hidden
+
+and a Bucket Reachability credential:
+- date: revealed to be today
+- bucket: hidden, but proved in ZK that it's the same as in the Lox
+  credential above
+
+and a new Lox credential to be issued:
+
+- id: jointly chosen by the user and BA
+- bucket: hidden, but proved in ZK that it's the same as in the Lox
+  credential above
+- trust_level: hidden, but proved in ZK that it's the same as in the
+  Lox credential above
+- level_since: hidden, but proved in ZK that it's the same as in the
+  Lox credential above
+- invites_remaining: hidden, but proved in ZK that it's one less than
+  the number in the Lox credential above
+- blockages: hidden, but proved in ZK that it's the same as in the
+  Lox credential above
+
+and a new Invitation credential to be issued:
+
+- inv_id: jointly chosen by the user and BA
+- date: selected by the server to be today's date
+- bucket: hidden, but proved in ZK that it's the same as in the Lox
+  credential above
+- blockages: hidden, but proved in ZK that it's the same as in the Lox
+  credential above
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, Scalar, G};
+use super::errors::CredentialError;
+use crate::lox_creds::{BucketReachability, Invitation, Lox};
+use cmz::*;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"issue_invite";
+
+/// Invitations must be used within this many days of being issued.
+/// Note that if you change this number to be larger than 15, you must
+/// also add bits to the zero knowledge proof.
+pub const INVITATION_EXPIRY: u32 = 15;
+
+muCMZProtocol! { issue_invite,
+    [L: Lox {id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H}, B: BucketReachability { date: R, bucket: H } ],
+    [ I: Invitation { inv_id: J, date: S, bucket: H, blockages: H }, N: Lox {id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H }],
+    L.bucket = B.bucket,
+    L.invites_remaining != 0,
+    N.bucket = L.bucket,
+    N.trust_level = L.trust_level,
+    N.level_since = L.level_since,
+    N.invites_remaining = L.invites_remaining - 1,
+    N.blockages = L.blockages,
+    I.bucket = L.bucket,
+    I.blockages = L.blockages
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    B: BucketReachability,
+    inv_pub: CMZPubkey<G>,
+    today: u32,
+) -> Result<(issue_invite::Request, issue_invite::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    // Ensure the credential can be correctly shown: it must be the case
+    // that invites_remaining not be 0
+    if let Some(invites_remaining) = L.invites_remaining {
+        if invites_remaining == Scalar::ZERO {
+            return Err(CredentialError::NoInvitationsRemaining);
+        }
+    } else {
+        return Err(CredentialError::InvalidField(
+            String::from("invites_remaining"),
+            String::from("None"),
+        ));
+    }
+
+    // The buckets in the Lox and Bucket Reachability credentials have
+    // to match
+    if L.bucket.is_some_and(|b| b != B.bucket.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+    // The Bucket Reachability credential has to be dated today
+    let reach_date: u32 = match scalar_u32(&B.date.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("date"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if reach_date != today {
+        return Err(CredentialError::InvalidField(
+            String::from("date"),
+            String::from("reachability credential must be generated today"),
+        ));
+    }
+
+    let mut I: Invitation = Invitation::using_pubkey(&inv_pub);
+    I.inv_id = Some(Scalar::random(rng));
+    I.date = Some(today.into());
+    I.bucket = L.bucket;
+    I.blockages = L.blockages;
+
+    let mut N: Lox = 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 = Some(L.invites_remaining.unwrap() - Scalar::ONE);
+    N.blockages = L.blockages;
+
+    match issue_invite::prepare(rng, SESSION_ID, &L, &B, I, N) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_issue_invite(
+        &mut self,
+        req: issue_invite::Request,
+    ) -> Result<issue_invite::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = issue_invite::Request::try_from(&reqbytes[..]).unwrap();
+        let today = self.today();
+        match issue_invite::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, B: &mut BucketReachability, I: &mut Invitation, N: &mut Lox| {
+                L.set_privkey(&self.lox_priv);
+                B.set_privkey(&self.reachability_priv);
+                I.set_privkey(&self.invitation_priv);
+                N.set_privkey(&self.lox_priv);
+                if B.date.is_some_and(|b| b != today.into()) {
+                    return Err(CMZError::RevealAttrMissing("date", "not today"));
+                }
+                I.date = Some(today.into());
+                Ok(())
+            },
+            |L: &Lox, _B: &BucketReachability, _I: &Invitation, _N: &Lox| {
+                if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_L_issuer, _B_issuer, _I_issuer, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: issue_invite::ClientState,
+    rep: issue_invite::Reply,
+) -> Result<(Invitation, Lox), CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = issue_invite::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(creds) => Ok(creds),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_issue_invite() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        lox_cred = th.level_up(rng, lox_cred.clone());
+        let issue_invite_cred = th.issue_invite(rng, lox_cred.clone());
+        th.verify_lox(&issue_invite_cred.clone().1);
+        th.verify_invitation(&issue_invite_cred.clone().0);
+    }
+}

+ 287 - 0
crates/lox-extensions/src/proto/level_up.rs

@@ -0,0 +1,287 @@
+/*! A module for the protocol for the user to increase their trust level
+(from a level at least 1; use the trust promotion protocol to go from
+untrusted (level 0) to minimally trusted (level 1).
+
+They are allowed to do this as long as some amount of time (depending on
+their current level) has elapsed since their last level change, and they
+have a Bucket Reachability credential for their current bucket and
+today's date.  (Such credentials are placed daily in the encrypted
+bridge table.)
+
+The user presents their current Lox credential:
+- id: revealed
+- bucket: blinded
+- trust_level: revealed, and must be at least 1
+- level_since: blinded, but proved in ZK that it's at least the
+  appropriate number of days ago
+- invites_remaining: blinded
+- blockages: blinded, but proved in ZK that it's at most the appropriate
+  blockage limit for the target trust level
+
+and a Bucket Reachability credential:
+- date: revealed to be today
+- bucket: blinded, but proved in ZK that it's the same as in the Lox
+  credential above
+
+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: revealed to be one more than the trust level above
+- level_since: today
+- invites_remaining: revealed to be the number of invites for the new
+  level (note that the invites_remaining from the previous credential
+  are _not_ carried over)
+- blockages: blinded, but proved in ZK that it's the same as in the
+  Lox credential above
+
+*/
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, G};
+use super::errors::CredentialError;
+use crate::lox_creds::{BucketReachability, Lox};
+use cmz::*;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"level_up";
+
+/// The maximum trust level in the system.  A user can run this level
+/// upgrade protocol when they're already at the max level; they will
+/// get a fresh invites_remaining batch, and reset their level_since
+/// field to today's date, but will remain in the max level.
+pub const MAX_LEVEL: usize = 4;
+
+/// LEVEL_INTERVAL\[i\] for i >= 1 is the minimum number of days a user
+/// must be at trust level i before advancing to level i+1 (or as above,
+/// remain at level i if i == MAX_LEVEL).  Note that the
+/// LEVEL_INTERVAL\[0\] entry is a dummy; the trust_promotion protocol
+/// is used instead of this one to move from level 0 to level 1.
+pub const LEVEL_INTERVAL: [u32; MAX_LEVEL + 1] = [0, 14, 28, 56, 84];
+
+/// LEVEL_INVITATIONS\[i\] for i >= 1 is the number of invitations a
+/// user will be eligible to issue upon advancing from level i to level
+/// i+1.  Again the LEVEL_INVITATIONS\[0\] entry is a dummy, as for
+/// LEVEL_INTERVAL.
+pub const LEVEL_INVITATIONS: [u32; MAX_LEVEL + 1] = [0, 2, 4, 6, 8];
+
+/// MAX_BLOCKAGES\[i\] for i >= 1 is the maximum number of bucket
+/// blockages this credential is allowed to have recorded in order to
+/// advance from level i to level i+1.  Again the LEVEL_INVITATIONS\[0\]
+/// entry is a dummy, as for LEVEL_INTERVAL.
+// If you change this to have a number greater than 7, you need to add
+// one or more bits to the ZKP.
+pub const MAX_BLOCKAGES: [u32; MAX_LEVEL + 1] = [0, 4, 3, 2, 2];
+
+muCMZProtocol! { level_up<credential_expiry, eligibility_max_age, max_blockage, today>,
+    [ L: Lox { id: R, bucket: H, trust_level: R, level_since: H, invites_remaining: H, blockages: H },
+    B: BucketReachability { date: R, bucket: H } ],
+    N: Lox {id: J, bucket: H, trust_level: R, level_since: S, invites_remaining: I, blockages: H },
+    (credential_expiry..=eligibility_max_age).contains(L.level_since),
+    (0..=max_blockage).contains(L.blockages),
+    B.date = today,
+    B.bucket = L.bucket,
+    N.bucket = L.bucket,
+    N.trust_level = L.trust_level + 1,
+    N.blockages = L.blockages,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    B: BucketReachability,
+    today: u32,
+) -> Result<(level_up::Request, level_up::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+    // Ensure the credential can be correctly shown: it must be the case
+    // that level_since + LEVEL_INTERVAL[level] <= today.
+    let level_since: u32 = match scalar_u32(&L.level_since.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("level_since"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    // The trust level has to be at least 1
+    let trust_level: u32 = match scalar_u32(&L.trust_level.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("trust_level"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if trust_level < 1 || (trust_level as usize) > MAX_LEVEL {
+        return Err(CredentialError::InvalidField(
+            String::from("trust_level"),
+            format!("level {:?} not in range", trust_level),
+        ));
+    }
+    // The trust level has to be no higher than the highest level
+    let level_interval: u32 = match LEVEL_INTERVAL.get(trust_level as usize) {
+        Some(&v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("trust_level"),
+                format!("level {:?} not in range", trust_level),
+            ))
+        }
+    };
+    if level_since + level_interval > today {
+        return Err(CredentialError::TimeThresholdNotMet(
+            level_since + level_interval - today,
+        ));
+    }
+    // The credential can't be _too_ old
+    let diffdays = today - (level_since + level_interval);
+    if diffdays > 511 {
+        return Err(CredentialError::CredentialExpired);
+    }
+    // The current number of blockages
+    let blockages = match scalar_u32(&L.blockages.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("blockages"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if blockages > MAX_BLOCKAGES[trust_level as usize] {
+        return Err(CredentialError::ExceededBlockagesThreshold);
+    }
+    // The buckets in the Lox and Bucket Reachability credentials have
+    // to match
+    if L.bucket.is_some_and(|b| b != B.bucket.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+    // The Bucket Reachability credential has to be dated today
+    let reach_date: u32 = match scalar_u32(&B.date.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("date"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if reach_date != today {
+        return Err(CredentialError::InvalidField(
+            String::from("date"),
+            String::from("reachability credential must be generated today"),
+        ));
+    }
+    // The new trust level
+    let new_level = if (trust_level as usize) < MAX_LEVEL {
+        trust_level + 1
+    } else {
+        trust_level
+    };
+    let mut N = Lox::using_pubkey(L.get_pubkey());
+    N.bucket = L.bucket;
+    N.trust_level = Some(new_level.into());
+    N.invites_remaining = Some(LEVEL_INVITATIONS[trust_level as usize].into());
+    N.blockages = L.blockages;
+    let eligibility_max_age = today - (LEVEL_INTERVAL[trust_level as usize]);
+
+    let params = level_up::Params {
+        credential_expiry: (eligibility_max_age - 511).into(),
+        eligibility_max_age: eligibility_max_age.into(),
+        max_blockage: MAX_BLOCKAGES[new_level as usize].into(),
+        today: today.into(),
+    };
+    match level_up::prepare(rng, SESSION_ID, &L, &B, N, &params) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_level_up(
+        &mut self,
+        req: level_up::Request,
+    ) -> Result<level_up::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = level_up::Request::try_from(&reqbytes[..]).unwrap();
+        let today = self.today();
+        match level_up::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, B: &mut BucketReachability, N: &mut Lox| {
+                let trust_level: u32 = match scalar_u32(&L.trust_level.unwrap()) {
+                    Some(v) if v as usize >= 1 && v as usize <= MAX_LEVEL => v,
+                    _ => {
+                        // This error should be improved i.e., InvalidAttr and the type
+                        // with a description
+                        return Err(CMZError::RevealAttrMissing(
+                            "trust_level",
+                            "Could not be converted to u32 or value not in range",
+                        ));
+                    }
+                };
+                let eligibility_max_age: u32 = today - LEVEL_INTERVAL[trust_level as usize];
+                L.set_privkey(&self.lox_priv);
+                B.set_privkey(&self.reachability_priv);
+                N.set_privkey(&self.lox_priv);
+                N.trust_level = Some((trust_level + 1).into());
+                N.level_since = Some(today.into());
+                N.invites_remaining = Some(LEVEL_INVITATIONS[trust_level as usize].into());
+                Ok(level_up::Params {
+                    credential_expiry: (eligibility_max_age - 511).into(),
+                    eligibility_max_age: eligibility_max_age.into(),
+                    max_blockage: MAX_BLOCKAGES[(trust_level + 1) as usize].into(),
+                    today: today.into(),
+                })
+            },
+            |L: &Lox, _B: &BucketReachability, _N: &Lox| {
+                if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", ""));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_L_issuer, _B_isser, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: level_up::ClientState,
+    rep: level_up::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = level_up::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_level_up() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        lox_cred = th.level_up(rng, lox_cred.clone());
+        th.verify_lox(&lox_cred);
+    }
+}

+ 177 - 0
crates/lox-extensions/src/proto/migration.rs

@@ -0,0 +1,177 @@
+/*! A module for the protocol for the user to migrate from one bucket to
+another and change trust level from untrusted (trust level 0) to trusted
+(trust level 1).
+
+The user presents their current Lox credential:
+
+- id: revealed
+- bucket: blinded
+- trust_level: revealed to be 0
+- level_since: blinded
+- invites_remaining: revealed to be 0
+- blockages: revealed to be 0
+
+and a Migration credential:
+
+- id: revealed as the same as the Lox credential id above
+- from_bucket: blinded, but proved in ZK that it's the same as the
+  bucket in the Lox credential above
+- to_bucket: blinded
+
+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 the to_bucket
+  in the Migration credential above
+- trust_level: 1
+- level_since: today
+- invites_remaining: 0
+- blockages: 0
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{Scalar, G};
+use super::errors::CredentialError;
+use crate::lox_creds::{Lox, Migration};
+#[cfg(feature = "bridgeauth")]
+use crate::migration_table::MigrationType;
+#[cfg(feature = "bridgeauth")]
+use crate::scalar_u32;
+use cmz::*;
+#[cfg(feature = "bridgeauth")]
+use ff::PrimeField;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"migration";
+
+muCMZProtocol! { migration,
+    [ L: Lox { id: R, bucket: H, trust_level: R, level_since: H, invites_remaining: R, blockages: R },
+    M: Migration { lox_id: R, from_bucket: H, to_bucket: H, migration_type: I} ],
+    N: Lox {id: J, bucket: H, trust_level: I, level_since: S, invites_remaining: I, blockages: I },
+    L.id = M.lox_id,
+    L.bucket = M.from_bucket,
+    N.bucket = M.to_bucket,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    M: Migration,
+) -> Result<(migration::Request, migration::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    // Ensure that the credenials can be correctly shown; that is, the
+    // ids match and the Lox credential bucket matches the Migration
+    // credential from_bucket
+    if L.id.is_some_and(|i| i != M.lox_id.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
+    // This protocol only allows migrating from trust level 0 to trust
+    // level 1
+    if L.trust_level.is_some_and(|t| t != Scalar::ZERO) {
+        return Err(CredentialError::InvalidField(
+            String::from("trust_level"),
+            String::from("must be zero"),
+        ));
+    }
+
+    if L.bucket.is_some_and(|b| b != M.from_bucket.unwrap()) {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
+    let mut N = Lox::using_pubkey(L.get_pubkey());
+    N.bucket = M.to_bucket;
+    N.trust_level = Some(Scalar::ONE);
+    N.invites_remaining = Some(Scalar::ZERO);
+    N.blockages = Some(Scalar::ZERO);
+
+    match migration::prepare(rng, SESSION_ID, &L, &M, N) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_migration(
+        &mut self,
+        req: migration::Request,
+    ) -> Result<migration::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = migration::Request::try_from(&reqbytes[..]).unwrap();
+
+        let today = self.today();
+        match migration::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, M: &mut Migration, N: &mut Lox| {
+                match scalar_u32(&L.trust_level.unwrap()) {
+                    Some(v) if v as usize == 0 => v,
+                    _ => {
+                        // This error should be improved i.e., InvalidAttr and the type
+                        // with a description
+                        return Err(CMZError::RevealAttrMissing(
+                            "migration",
+                            "Could not be converted to u32 or trust_level not 0",
+                        ));
+                    }
+                };
+                L.set_privkey(&self.lox_priv);
+                M.set_privkey(&self.migration_priv);
+                N.set_privkey(&self.lox_priv);
+                M.migration_type = Some(Scalar::from_u128(MigrationType::TrustUpgrade.into()));
+                N.trust_level = Some(Scalar::ONE);
+                N.level_since = Some(today.into());
+                N.invites_remaining = Some(Scalar::ZERO);
+                N.blockages = Some(Scalar::ZERO);
+                Ok(())
+            },
+            |L: &Lox, _M: &Migration, _N: &Lox| {
+                if self.id_filter.filter(&L.id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_L_issuer, _M_isser, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: migration::ClientState,
+    rep: migration::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = migration::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_trust_migration() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        th.verify_lox(&lox_cred);
+    }
+}

+ 169 - 0
crates/lox-extensions/src/proto/open_invite.rs

@@ -0,0 +1,169 @@
+/*! A module for the protocol for the user to redeem an open invitation
+with the BA (bridge authority) to receive their initial Lox
+credential.
+
+The credential will have attributes:
+
+- id: jointly chosen by the user and BA
+- bucket: set by the BA
+- trust_level: 0
+- level_since: today
+- invites_remaining: 0
+- blockages: 0
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::{
+    bridge_table::{self, BridgeLine},
+    dup_filter::SeenType,
+    BridgeAuth, BridgeDb, OPENINV_LENGTH,
+};
+use super::super::{Scalar, G};
+use super::errors::CredentialError;
+use crate::lox_creds::Lox;
+use cmz::*;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"open_invite";
+muCMZProtocol! { open_invitation,
+    ,
+    L: Lox {id: J, bucket: S, trust_level: I, level_since: S, invites_remaining: I, blockages: I },
+}
+
+/// Prepare the open invitation request to send to the Lox Authority
+/// Note that preparing the request does not require an open invitation, but an invitation
+/// must be sent along with the prepared open_inivtation::Request to the Lox authority
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    pubkeys: CMZPubkey<G>,
+) -> Result<(open_invitation::Request, open_invitation::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    let mut L = Lox::using_pubkey(&pubkeys);
+    L.trust_level = Some(Scalar::ZERO);
+    L.invites_remaining = Some(Scalar::ZERO);
+    L.blockages = Some(Scalar::ZERO);
+    match open_invitation::prepare(rng, SESSION_ID, L) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn open_invitation(
+        &mut self,
+        req: open_invitation::Request,
+        invite: &[u8; OPENINV_LENGTH],
+    ) -> Result<(open_invitation::Reply, BridgeLine), CredentialError> {
+        // Check the signature on the open_invite, first with the old key, then with the new key.
+        // We manually match here because we're changing the Err type from SignatureError
+        // to ProofError
+        let mut rng = rand::thread_rng();
+        let mut old_token: Option<((Scalar, u32), usize)> = Default::default();
+        let invite_id: Scalar;
+        let bucket_id: u32;
+        // If there are old openinv keys, check them first
+        for (i, old_openinv_key) in self.old_keys.bridgedb_key.iter().enumerate() {
+            old_token = match BridgeDb::verify(*invite, *old_openinv_key) {
+                Ok(res) => Some((res, i)),
+                Err(_) => None,
+            };
+        }
+        // Check if verifying with the old key succeeded, if it did, check if it has been seen
+        if let Some(token) = old_token {
+            // Only proceed if the invite_id is fresh
+            (invite_id, bucket_id) = token.0;
+            if self
+                .old_filters
+                .openinv_filter
+                .get_mut(token.1)
+                .unwrap()
+                .filter(&invite_id)
+                == SeenType::Seen
+            {
+                return Err(CredentialError::CredentialExpired);
+            }
+        // If it didn't, try verifying with the new key
+        } else {
+            (invite_id, bucket_id) = match BridgeDb::verify(*invite, self.bridgedb_pub) {
+                Ok(res) => res,
+                // Also verify that the request doesn't match with an old openinv_key
+                Err(_) => {
+                    return Err(CredentialError::InvalidField(
+                        "invitation".to_string(),
+                        "pubkey".to_string(),
+                    ))
+                }
+            };
+            // Only proceed if the invite_id is fresh
+            if self.bridgedb_pub_filter.filter(&invite_id) == SeenType::Seen {
+                return Err(CredentialError::CredentialExpired);
+            }
+        }
+
+        // And also check that the bucket id is valid
+        if !self.bridge_table.buckets.contains_key(&bucket_id) {
+            return Err(CredentialError::InvalidField(
+                "invitation".to_string(),
+                "bucket".to_string(),
+            ));
+        }
+
+        let reqbytes = req.as_bytes();
+        // Create the bucket attribute (Scalar), which is a combination
+        // of the bucket id (u32) and the bucket's decryption key ([u8; 16])
+        let bucket_key = self.bridge_table.keys.get(&bucket_id).unwrap();
+        let bucket: Scalar = bridge_table::to_scalar(bucket_id, bucket_key);
+        let bridge_lines = self.bridge_table.buckets.get(&bucket_id).unwrap();
+        let bridge_line = bridge_lines[0];
+
+        let recvreq = open_invitation::Request::try_from(&reqbytes[..]).unwrap();
+        match open_invitation::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox| {
+                L.set_privkey(&self.lox_priv);
+                L.bucket = Some(bucket);
+                L.trust_level = Some(Scalar::ZERO);
+                L.level_since = Some(self.today().into());
+                L.invites_remaining = Some(Scalar::ZERO);
+                L.blockages = Some(Scalar::ZERO);
+                Ok(())
+            },
+            |_L: &Lox| Ok(()),
+        ) {
+            Ok((response, _L_issuer)) => Ok((response, bridge_line)),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: open_invitation::ClientState,
+    rep: open_invitation::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = open_invitation::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_open_invitation() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let cred = th.open_invite(rng, &invite);
+        th.verify_lox(&cred);
+    }
+}

+ 183 - 0
crates/lox-extensions/src/proto/redeem_invite.rs

@@ -0,0 +1,183 @@
+/*! A module for the protocol for a new user to redeem an Invitation
+credential.  The user will start at trust level 1 (instead of 0 for
+untrusted uninvited users).
+
+The user presents the Invitation credential:
+- id: revealed
+- date: blinded, but proved in ZK to be at most INVITATION_EXPIRY days ago
+- bucket: blinded
+- blockages: blinded
+
+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
+  Invitation credential above
+- trust_level: revealed to be 1
+- level_since: today
+- invites_remaining: revealed to be 0
+- blockages: blinded, but proved in ZK that it's the same as in the
+  Invitations credential above
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, Scalar, G};
+use super::errors::CredentialError;
+use crate::lox_creds::{Invitation, Lox};
+use crate::proto::level_up::LEVEL_INVITATIONS;
+use cmz::*;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"redeem_invite";
+
+/// Invitations must be used within this many days of being issued.
+/// Note that if you change this number to be larger than 15, you must
+/// also add bits to the zero knowledge proof.
+pub const INVITATION_EXPIRY: u32 = 15;
+
+muCMZProtocol! { redeem_invite<credential_expiry, today>,
+    [ I: Invitation { inv_id: R, date: H, bucket: H, blockages: H } ],
+    N: Lox {id: J, bucket: H, trust_level: I, level_since: S, invites_remaining: I, blockages: H },
+    (credential_expiry..=today).contains(I.date),
+    N.bucket = I.bucket,
+    N.blockages = I.blockages,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    I: Invitation,
+    lox_pubkeys: CMZPubkey<G>,
+    today: u32,
+) -> Result<(redeem_invite::Request, redeem_invite::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+    // Ensure the credential can be correctly shown: it must be the case
+    // that date + INVITATION_EXPIRY >= today.
+    let date: u32 = match scalar_u32(&I.date.unwrap()) {
+        Some(v) => v,
+        None => {
+            return Err(CredentialError::InvalidField(
+                String::from("date"),
+                String::from("could not be converted to u32"),
+            ))
+        }
+    };
+    if date + INVITATION_EXPIRY < today {
+        return Err(CredentialError::CredentialExpired);
+    }
+    let diffdays = date + INVITATION_EXPIRY - today;
+    // If diffdays > 15, then since INVITATION_EXPIRY <= 15, then date
+    // must be in the future.  Reject.
+    if diffdays > 15 {
+        return Err(CredentialError::InvalidField(
+            String::from("date"),
+            String::from("credential was created in the future"),
+        ));
+    }
+
+    let params = redeem_invite::Params {
+        credential_expiry: (today - INVITATION_EXPIRY).into(),
+        today: today.into(),
+    };
+
+    let mut N = Lox::using_pubkey(&lox_pubkeys);
+    N.bucket = I.bucket;
+    N.trust_level = Some(Scalar::ONE);
+    N.invites_remaining = Some(LEVEL_INVITATIONS[1].into());
+    N.blockages = I.blockages;
+
+    match redeem_invite::prepare(rng, SESSION_ID, &I, N, &params) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_redeem_invite(
+        &mut self,
+        req: redeem_invite::Request,
+    ) -> Result<redeem_invite::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = redeem_invite::Request::try_from(&reqbytes[..]).unwrap();
+        let today = self.today();
+        match redeem_invite::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |I: &mut Invitation, N: &mut Lox| {
+                I.set_privkey(&self.invitation_priv);
+                N.set_privkey(&self.lox_priv);
+                let eligibility_max_age: u32 = today - INVITATION_EXPIRY;
+                N.trust_level = Some(Scalar::ONE);
+                N.level_since = Some(today.into());
+                N.invites_remaining = Some(LEVEL_INVITATIONS[1].into());
+                Ok(redeem_invite::Params {
+                    credential_expiry: eligibility_max_age.into(),
+                    today: today.into(),
+                })
+            },
+            |I: &Invitation, _N: &Lox| {
+                if self.inv_id_filter.filter(&I.inv_id.unwrap()) == SeenType::Seen {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_I_isser, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: redeem_invite::ClientState,
+    rep: redeem_invite::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = redeem_invite::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+    use crate::proto::redeem_invite;
+
+    #[test]
+    fn test_redeem_invite() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        lox_cred = th.level_up(rng, lox_cred.clone());
+        let issue_invite_cred = th.issue_invite(rng, lox_cred.clone());
+        let redeem_invite_request = redeem_invite::request(
+            rng,
+            issue_invite_cred.clone().0,
+            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");
+        th.verify_lox(&r_cred.unwrap());
+    }
+}

+ 205 - 0
crates/lox-extensions/src/proto/trust_promotion.rs

@@ -0,0 +1,205 @@
+/*! A module for the protocol for the user to get promoted from
+untrusted (trust level 0) to trusted (trust level 1).
+
+They are allowed to do this as long as UNTRUSTED_INTERVAL days have
+passed since they obtained their level 0 Lox credential, and their
+bridge (level 0 users get put in a one-bridge bucket) has not been
+blocked.  (Blocked bridges in one-bridge buckets will have their entries
+removed from the bridge authority's trust_promotion table.)
+
+The user presents their current Lox credential:
+- id: revealed
+- bucket: blinded
+- trust_level: revealed to be 0
+- level_since: blinded, but proved in ZK that it's at least
+  UNTRUSTED_INTERVAL days ago
+- invites_remaining: revealed to be 0
+- blockages: revealed to be 0
+
+They will receive in return the encrypted MAC (Pk, EncQk) for their
+implicit Migration Key credential with attributes id and bucket,
+along with a HashMap of encrypted Migration credentials.  For each
+(from_i, to_i) in the BA's migration list, there will be an entry in
+the HashMap with key H1(id, from_attr_i, Qk_i) and value
+Enc_{H2(id, from_attr_i, Qk_i)}(to_attr_i, P_i, Q_i).  Here H1 and H2
+are the first 16 bytes and the second 16 bytes respectively of the
+SHA256 hash of the input, P_i and Q_i are a MAC on the Migration
+credential with attributes id, from_attr_i, and to_attr_i. Qk_i is the
+value EncQk would decrypt to if bucket were equal to from_attr_i. */
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::{scalar_u32, G};
+use super::errors::CredentialError;
+use crate::lox_creds::{Lox, Migration, MigrationKey};
+#[cfg(feature = "bridgeauth")]
+use crate::migration_table::WNAF_SIZE;
+use crate::migration_table::{self, EncMigrationTable};
+use cmz::*;
+use group::Group;
+#[cfg(feature = "bridgeauth")]
+use group::WnafBase;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"trust_promo";
+
+/// The minimum number of days a user has to be at trust level 0
+/// (untrusted) with their (single) bridge unblocked before they can
+/// move to level 1.
+///
+/// The implementation also puts an upper bound of UNTRUSTED_INTERVAL +
+/// 511 days, which is not unreasonable; we want users to be engaging
+/// with the system in order to move up trust levels.
+pub const UNTRUSTED_INTERVAL: u32 = 30;
+
+muCMZProtocol! { trust_promotion<credential_expiry, eligibility_max_age>,
+    L: Lox { id: R, bucket: H, trust_level: R, level_since: H, invites_remaining: R, blockages: R },
+    M: MigrationKey { lox_id: R, from_bucket: H} ,
+    M.lox_id = L.id,
+    M.from_bucket = L.bucket,
+    (credential_expiry..eligibility_max_age).contains(L.level_since),
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    migkey_pubkeys: CMZPubkey<G>,
+    today: u32,
+) -> Result<(trust_promotion::Request, trust_promotion::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    // Ensure that the credenials can be correctly shown; that is, the
+    // ids match and the Lox credential bucket matches the Migration
+    // credential from_bucket
+    if L.id.is_none() {
+        return Err(CredentialError::CredentialMismatch);
+    }
+
+    // This protocol only allows migrating from trust level 0 to trust
+    // level 1
+    if let Some(ls) = L.level_since {
+        let level_since = match scalar_u32(&ls) {
+            Some(v) => v,
+            None => {
+                return Err(CredentialError::InvalidField(
+                    String::from("level_since"),
+                    String::from("could not be converted to u32"),
+                ))
+            }
+        };
+        if level_since + UNTRUSTED_INTERVAL > today {
+            return Err(CredentialError::TimeThresholdNotMet(
+                level_since + UNTRUSTED_INTERVAL - today,
+            ));
+        }
+        let diffdays = today - (level_since + UNTRUSTED_INTERVAL);
+        if diffdays > 511 {
+            return Err(CredentialError::CredentialExpired);
+        }
+    }
+    let eligibility_max_age = today - UNTRUSTED_INTERVAL;
+
+    let params = trust_promotion::Params {
+        credential_expiry: (eligibility_max_age - 511).into(),
+        eligibility_max_age: eligibility_max_age.into(),
+    };
+    let mut M = MigrationKey::using_pubkey(&migkey_pubkeys);
+    M.lox_id = L.id;
+    M.from_bucket = L.bucket;
+    match trust_promotion::prepare(rng, SESSION_ID, &L, M, &params) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[allow(clippy::type_complexity)]
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_trust_promotion(
+        &mut self,
+        req: trust_promotion::Request,
+    ) -> Result<(trust_promotion::Reply, EncMigrationTable), CredentialError> {
+        let mut rng = rand::thread_rng();
+        let reqbytes = req.as_bytes();
+        let recvreq = trust_promotion::Request::try_from(&reqbytes[..]).unwrap();
+        let today = self.today();
+        match trust_promotion::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, M: &mut MigrationKey| {
+                L.set_privkey(&self.lox_priv);
+                M.set_privkey(&self.migrationkey_priv);
+                let eligibility_max_age = today - UNTRUSTED_INTERVAL;
+                Ok(trust_promotion::Params {
+                    credential_expiry: (eligibility_max_age - 511).into(),
+                    eligibility_max_age: eligibility_max_age.into(),
+                })
+            },
+            |L: &Lox, _M: &MigrationKey| {
+                if self.id_filter.check(&L.id.unwrap()) == SeenType::Seen
+                    || self.trust_promotion_filter.filter(&L.id.unwrap()) == SeenType::Seen
+                {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (L_issuer, M_issuer))) => {
+                let Pktable: WnafBase<G, WNAF_SIZE> = WnafBase::new(M_issuer.MAC.P);
+                let enc_migration_table = EncMigrationTable {
+                    mig_table: self.trustup_migration_table.encrypt_table(
+                        L_issuer.id.unwrap(),
+                        &self.bridge_table,
+                        &Pktable,
+                        &self.migration_priv,
+                        &self.migrationkey_priv,
+                    ),
+                };
+                Ok((response, enc_migration_table))
+            }
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    migration_pubkey: CMZPubkey<G>,
+    state: trust_promotion::ClientState,
+    rep: trust_promotion::Reply,
+    enc_migration_table: EncMigrationTable,
+) -> Result<Migration, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = trust_promotion::Reply::try_from(&replybytes[..]).unwrap();
+    let migkey = match state.finalize(recvreply) {
+        Ok(cred) => cred,
+        Err(_e) => return Err(CMZError::Unknown),
+    };
+    match migration_table::decrypt_cred(
+        migkey,
+        migration_table::MigrationType::TrustUpgrade,
+        migration_pubkey,
+        &enc_migration_table.mig_table,
+    ) {
+        Some(cred) => Ok(cred),
+        None => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+
+    #[test]
+    fn test_trust_promotion() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        th.verify_migration(&mig_cred);
+    }
+}

+ 184 - 0
crates/lox-extensions/src/proto/update_cred.rs

@@ -0,0 +1,184 @@
+/*! A module for the protocol for a user to request the issuing of an updated credential after a key rotation has occurred
+
+
+They are allowed to do this as long as their current Lox credential is valid
+
+The user presents their current Lox credential:
+- id: revealed
+- bucket: blinded
+- trust_level: blinded
+- level_since: blinded
+- invites_remaining: blinded
+- blockages: blinded
+
+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
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::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"update_cred";
+
+muCMZProtocol! { update_cred,
+    L: Lox { id: R, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H },
+    N: Lox {id: J, bucket: H, trust_level: H, level_since: H, invites_remaining: H, blockages: H },
+    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,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    L: Lox,
+    pubkeys: CMZPubkey<G>,
+) -> Result<(update_cred::Request, update_cred::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+    let mut N: Lox = Lox::using_pubkey(&pubkeys);
+    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;
+
+    match update_cred::prepare(&mut *rng, SESSION_ID, &L, N) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_update_cred(
+        &mut self,
+        old_pub_key: CMZPubkey<G>,
+        req: update_cred::Request,
+    ) -> Result<update_cred::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        // Both of these must be true and should be true after rotate_lox_keys is called
+        if self.old_keys.lox_keys.is_empty() || self.old_filters.lox_filter.is_empty() {
+            return Err(CredentialError::CredentialMismatch);
+        }
+
+        let reqbytes = req.as_bytes();
+        let recvreq = update_cred::Request::try_from(&reqbytes[..]).unwrap();
+        match update_cred::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |L: &mut Lox, N: &mut Lox| {
+                // calling this function will automatically use the most recent old private key for
+                // verification and the new private key for issuing.
+
+                // Recompute the "error factors" using knowledge of our own
+                // (the issuer's) outdated private key instead of knowledge of the
+                // hidden attributes
+                let old_keys = match self
+                    .old_keys
+                    .lox_keys
+                    .iter()
+                    .find(|x| x.pub_key == old_pub_key)
+                {
+                    Some(old_keys) => old_keys,
+                    None => {
+                        return Err(CMZError::RevealAttrMissing("Key", "Mismatch"));
+                    }
+                };
+                let old_priv_key = old_keys.priv_key.clone();
+                L.set_privkey(&old_priv_key);
+                N.set_privkey(&self.lox_priv);
+                Ok(())
+            },
+            |L: &Lox, _N: &Lox| {
+                let index = match self
+                    .old_keys
+                    .lox_keys
+                    .iter()
+                    .position(|x| x.pub_key == old_pub_key)
+                {
+                    Some(index) => index,
+                    None => return Err(CMZError::RevealAttrMissing("Key", "Mismatch")),
+                };
+                if self
+                    .old_filters
+                    .lox_filter
+                    .get_mut(index)
+                    .unwrap()
+                    .filter(&L.id.unwrap())
+                    == SeenType::Seen
+                {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_L_issuer, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: update_cred::ClientState,
+    rep: update_cred::Reply,
+) -> Result<Lox, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = update_cred::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+    use crate::proto::update_cred;
+
+    #[test]
+    fn test_update_cred() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let lox_cred = th.open_invite(rng, &invite);
+        let old_pub = th.ba.lox_pub.clone();
+        th.ba.rotate_lox_keys(rng);
+        let update_cred_request =
+            update_cred::request(rng, lox_cred.clone(), th.ba.lox_pub.clone());
+        assert!(
+            update_cred_request.is_ok(),
+            "Update credential request should succeed"
+        );
+        let (request, client_state) = update_cred_request.unwrap();
+        let update_cred_response = th.ba.handle_update_cred(old_pub, request);
+        assert!(
+            update_cred_response.is_ok(),
+            "Update cred response from server should succeed"
+        );
+        let response = update_cred_response.unwrap();
+        let creds = update_cred::handle_response(client_state, response);
+        assert!(creds.is_ok(), "Handle response should succeed");
+        th.verify_lox(&creds.unwrap());
+    }
+}

+ 180 - 0
crates/lox-extensions/src/proto/update_invite.rs

@@ -0,0 +1,180 @@
+/*! A module for the protocol for a user to request the issuing of an updated
+ * invitation credential after a key rotation has occurred
+
+
+The user presents their current Invitation credential:
+- id: revealed
+- date: blinded
+- bucket: blinded
+- blockages: blinded
+
+and a new Invitation credential to be issued:
+- id: jointly chosen by the user and BA
+- date: blinded, but proved in ZK that it's the same as in the invitation
+  date above
+- bucket: blinded, but proved in ZK that it's the same as in the Invitation
+  credential above
+- blockages: blinded, but proved in ZK that it's the same as in the
+  Invitation credential above
+
+*/
+
+#[cfg(feature = "bridgeauth")]
+use super::super::dup_filter::SeenType;
+#[cfg(feature = "bridgeauth")]
+use super::super::BridgeAuth;
+use super::super::G;
+use super::errors::CredentialError;
+use crate::lox_creds::Invitation;
+use cmz::*;
+use group::Group;
+use rand::{CryptoRng, RngCore};
+use sha2::Sha512;
+
+const SESSION_ID: &[u8] = b"update_invite";
+
+muCMZProtocol! { update_invite,
+    I: Invitation { inv_id: R, date: H, bucket: H, blockages: H },
+    N: Invitation { inv_id: J, date: H, bucket: H, blockages: H },
+    I.date = N.date,
+    I.bucket = N.bucket,
+    I.blockages = N.blockages,
+}
+
+pub fn request(
+    rng: &mut (impl CryptoRng + RngCore),
+    I: Invitation,
+    new_pubkeys: CMZPubkey<G>,
+) -> Result<(update_invite::Request, update_invite::ClientState), CredentialError> {
+    cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
+
+    let mut N = Invitation::using_pubkey(&new_pubkeys);
+    N.date = I.date;
+    N.bucket = I.bucket;
+    N.blockages = I.blockages;
+
+    match update_invite::prepare(&mut *rng, SESSION_ID, &I, N) {
+        Ok(req_state) => Ok(req_state),
+        Err(e) => Err(CredentialError::CMZError(e)),
+    }
+}
+
+#[cfg(feature = "bridgeauth")]
+impl BridgeAuth {
+    pub fn handle_update_invite(
+        &mut self,
+        old_pub_key: CMZPubkey<G>,
+        req: update_invite::Request,
+    ) -> Result<update_invite::Reply, CredentialError> {
+        let mut rng = rand::thread_rng();
+        // Both of these must be true and should be true after rotate_lox_keys is called
+        if self.old_keys.invitation_keys.is_empty() || self.old_filters.invitation_filter.is_empty()
+        {
+            return Err(CredentialError::CredentialMismatch);
+        }
+
+        let reqbytes = req.as_bytes();
+        let recvreq = update_invite::Request::try_from(&reqbytes[..]).unwrap();
+        match update_invite::handle(
+            &mut rng,
+            SESSION_ID,
+            recvreq,
+            |I: &mut Invitation, N: &mut Invitation| {
+                // calling this function will automatically use the most recent old private key for
+                // verification and the new private key for issuing.
+                // Recompute the "error factors" using knowledge of our own
+                // (the issuer's) outdated private key instead of knowledge of the
+                // hidden attributes
+                let old_keys = match self
+                    .old_keys
+                    .invitation_keys
+                    .iter()
+                    .find(|x| x.pub_key == old_pub_key)
+                {
+                    Some(old_keys) => old_keys,
+                    None => return Err(CMZError::RevealAttrMissing("Key", "Mismatch")),
+                };
+                let old_priv_key = old_keys.priv_key.clone();
+                I.set_privkey(&old_priv_key);
+                N.set_privkey(&self.invitation_priv);
+                Ok(())
+            },
+            |I: &Invitation, _N: &Invitation| {
+                let index = match self
+                    .old_keys
+                    .invitation_keys
+                    .iter()
+                    .position(|x| x.pub_key == old_pub_key)
+                {
+                    Some(index) => index,
+                    None => return Err(CMZError::RevealAttrMissing("Key", "Mismatch")),
+                };
+                if self
+                    .old_filters
+                    .invitation_filter
+                    .get_mut(index)
+                    .unwrap()
+                    .filter(&I.inv_id.unwrap())
+                    == SeenType::Seen
+                {
+                    return Err(CMZError::RevealAttrMissing("id", "Credential Expired"));
+                }
+                Ok(())
+            },
+        ) {
+            Ok((response, (_I_issuer, _N_issuer))) => Ok(response),
+            Err(e) => Err(CredentialError::CMZError(e)),
+        }
+    }
+}
+
+pub fn handle_response(
+    state: update_invite::ClientState,
+    rep: update_invite::Reply,
+) -> Result<Invitation, CMZError> {
+    let replybytes = rep.as_bytes();
+    let recvreply = update_invite::Reply::try_from(&replybytes[..]).unwrap();
+    match state.finalize(recvreply) {
+        Ok(cred) => Ok(cred),
+        Err(_e) => Err(CMZError::Unknown),
+    }
+}
+
+#[cfg(all(test, feature = "bridgeauth"))]
+mod tests {
+    use crate::mock_auth::TestHarness;
+    use crate::proto::update_invite;
+
+    #[test]
+    fn test_update_invite() {
+        let mut th = TestHarness::new();
+        let rng = &mut rand::thread_rng();
+        let invite = th.bdb.invite().unwrap();
+        let mut lox_cred = th.open_invite(rng, &invite);
+        let mig_cred = th.trust_promotion(rng, lox_cred.clone());
+        lox_cred = th.migration(rng, lox_cred.clone(), mig_cred.clone());
+        lox_cred = th.level_up(rng, lox_cred.clone());
+        let issue_invite_cred = th.issue_invite(rng, lox_cred.clone());
+        let old_pub = th.ba.invitation_pub.clone();
+        th.ba.rotate_invitation_keys(rng);
+        let update_invite_request = update_invite::request(
+            rng,
+            issue_invite_cred.clone().0,
+            th.ba.invitation_pub.clone(),
+        );
+        assert!(
+            update_invite_request.is_ok(),
+            "Update invitation request should succeed"
+        );
+        let (request, client_state) = update_invite_request.unwrap();
+        let update_invite_response = th.ba.handle_update_invite(old_pub, request);
+        assert!(
+            update_invite_response.is_ok(),
+            "Update invite response from server should succeed"
+        );
+        let response = update_invite_response.unwrap();
+        let creds = update_invite::handle_response(client_state, response);
+        assert!(creds.is_ok(), "Handle response should succeed");
+        th.verify_invitation(&creds.unwrap());
+    }
+}